Files
workinf_Blender_Wasm/web/tests/e2e/project-action-mutex.spec.ts
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

96 lines
4.9 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend");
async function waitForEngine(page: Page): Promise<void> {
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
}
test("M7-02 rejects a repeated open before it can replace the first project owner", async ({ page }) => {
await waitForEngine(page);
const bytes = Array.from(fs.readFileSync(basicBlend));
await page.evaluate(({ blendBytes }) => {
const input = document.querySelector<HTMLInputElement>("[data-testid=blend-file-input]");
if (!input) throw new Error("blend input missing");
const dispatchOpen = (name: string): void => {
const transfer = new DataTransfer();
transfer.items.add(new File([new Uint8Array(blendBytes)], name, { type: "application/octet-stream" }));
input.files = transfer.files;
input.dispatchEvent(new Event("change", { bubbles: true }));
};
dispatchOpen("first-open.blend");
dispatchOpen("second-open.blend");
}, { blendBytes: bytes });
const app = page.locator(".blender-app");
await expect(app).toHaveAttribute("data-project-action-conflict", "USER_ACTION_CONFLICT");
await expect(app).toHaveAttribute("data-project-action-conflict-reason", "REPEATED_OPEN");
await expect(app).toHaveAttribute("data-project-action-conflict-requested", "OPEN");
await expect(app).toHaveAttribute("data-project-action-conflict-owner", "OPEN");
await expect(app).toHaveAttribute("data-user-action-open-status", "SUCCEEDED");
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
expect(await page.evaluate(() => localStorage.getItem("blender-web:last-project-id"))).toBe("first-open");
});
test("M7-02 grants only one concurrent manual save and emits one download", async ({ page }) => {
await waitForEngine(page);
await page.getByTestId("blend-file-input").setInputFiles(basicBlend);
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
let downloadCount = 0;
page.on("download", () => { downloadCount += 1; });
await page.evaluate(() => {
const save = document.querySelector<HTMLButtonElement>('button[aria-label="保存项目"]');
if (!save) throw new Error("save button missing");
save.click();
save.click();
});
const app = page.locator(".blender-app");
await expect(app).toHaveAttribute("data-project-action-conflict", "USER_ACTION_CONFLICT");
await expect(app).toHaveAttribute("data-project-action-conflict-reason", "CONCURRENT_SAVE");
await expect(app).toHaveAttribute("data-project-action-conflict-requested", "SAVE");
await expect(app).toHaveAttribute("data-project-action-conflict-owner", "SAVE");
await expect(app).toHaveAttribute("data-user-action-save-status", "SUCCEEDED");
await expect(app).toHaveAttribute("data-user-action-save-as-status", "SUCCEEDED");
await expect.poll(() => downloadCount).toBe(1);
});
test("M7-02 blocks close until save commit completes without terminating either worker", async ({ page }) => {
await waitForEngine(page);
await page.getByTestId("blend-file-input").setInputFiles(basicBlend);
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
const firstDownload = page.waitForEvent("download");
await page.evaluate(() => {
const save = document.querySelector<HTMLButtonElement>('button[aria-label="保存项目"]');
const close = document.querySelector<HTMLButtonElement>('button[aria-label="关闭项目"]');
if (!save || !close) throw new Error("project action button missing");
save.click();
close.click();
});
const app = page.locator(".blender-app");
await expect(app).toHaveAttribute("data-project-action-conflict", "USER_ACTION_CONFLICT");
await expect(app).toHaveAttribute("data-project-action-conflict-reason", "CLOSE_DURING_SAVE");
await expect(app).toHaveAttribute("data-project-action-conflict-requested", "CLOSE");
await expect(app).toHaveAttribute("data-project-action-conflict-owner", "SAVE");
await firstDownload;
await expect(app).toHaveAttribute("data-user-action-save-status", "SUCCEEDED");
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "添加立方体" }).click();
await expect(page.getByTestId("scene-stats")).toContainText("Objects 4");
const secondDownload = page.waitForEvent("download");
await page.getByRole("button", { name: "保存项目" }).click();
await secondDownload;
await expect(app).not.toHaveAttribute("data-project-action-conflict");
await page.getByRole("button", { name: "关闭项目" }).click();
await expect(page.getByTestId("scene-stats")).toContainText("Objects 0");
await expect(page.getByRole("button", { name: "关闭项目" })).toBeDisabled();
});