Files
workinf_Blender_Wasm/web/tests/e2e/recent-projects-recovery.spec.ts
mes123456 0fe8d2bb56
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 M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

199 lines
10 KiB
TypeScript

import { expect, test } from "@playwright/test";
import path from "node:path";
const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend");
test("M7-09 persists recent projects and restores UI state after a damaged-index Worker restart", async ({ page }) => {
await page.goto("/?worker-fault=storage");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
const app = page.locator(".blender-app");
await page.getByTestId("blend-file-input").setInputFiles(basicBlend);
await expect(page.getByText("Cube", { exact: true })).toBeVisible();
const download = page.waitForEvent("download");
await page.getByRole("button", { name: "保存项目" }).click();
await download;
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await page.getByRole("button", { name: "Modeling" }).click();
await page.getByRole("slider", { name: "当前帧" }).fill("12");
await expect(app).toHaveAttribute("data-current-frame", "12");
await page.locator('.tree-row.child').filter({ hasText: "Camera" }).click();
await page.locator('.tree-row.child').filter({ hasText: "Cube" }).click({ modifiers: ["Shift"] });
const selectedBefore = await app.getAttribute("data-selected-object-ids");
const revisionBefore = await app.getAttribute("data-current-main-revision");
expect(selectedBefore?.split(",")).toHaveLength(2);
await page.evaluate(async () => {
const database = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open("blender-web-metadata", 7);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const transaction = database.transaction("setting", "readwrite");
transaction.objectStore("setting").put({ id: "recent-projects:v1", value: { schemaVersion: 1, projects: [{ broken: true }] } });
await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
database.close();
});
await page.getByTestId("inject-worker-crash").click();
await expect(app).toHaveAttribute("data-worker-fault-source", "storage");
await page.getByTestId("restart-and-recover").click();
await expect(app).toHaveAttribute("data-worker-recovery-status", "SUCCEEDED", { timeout: 30_000 });
await expect(app).toHaveAttribute("data-workspace", "Modeling");
await expect(app).toHaveAttribute("data-current-frame", "12");
await expect(app).toHaveAttribute("data-selected-object-ids", selectedBefore ?? "");
await expect(app).toHaveAttribute("data-current-main-revision", revisionBefore ?? "");
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await expect(app).toHaveAttribute("data-recent-project-quarantined", "1");
const persistedIndex = await page.evaluate(async () => {
const database = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open("blender-web-metadata", 7);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const transaction = database.transaction("setting", "readonly");
const row = await new Promise<{ value?: { projects?: Array<{ projectId?: string }> } } | undefined>((resolve, reject) => {
const request = transaction.objectStore("setting").get("recent-projects:v1");
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
database.close();
return row?.value?.projects?.map((project) => project.projectId) ?? [];
});
expect(persistedIndex).toEqual(["basic_scene"]);
await page.reload();
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await page.getByTestId("recent-projects").selectOption("basic_scene");
await expect(page.getByText("Cube", { exact: true })).toBeVisible();
await expect(app).toHaveAttribute("data-project-id", "basic_scene");
await expect(page.getByTestId("engine-status")).toContainText("Recovery:");
});
test("M7-09 StorageClient keeps deterministic recent projects across Worker instances", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const base = {
schemaVersion: 1 as const,
revision: 1,
bytes: 8,
sha256: "a".repeat(64),
updatedAt: "2026-08-15T12:00:00.000Z",
backend: "unknown" as const,
};
const first = new StorageClient();
await Promise.all([
first.touchRecentProject({ ...base, projectId: "older", displayName: "Older", lastOpenedAt: "2026-08-15T11:00:00.000Z" }),
first.touchRecentProject({ ...base, projectId: "newer", displayName: "Newer", lastOpenedAt: "2026-08-15T13:00:00.000Z" }),
first.touchRecentProject({ ...base, projectId: "older", displayName: "Older latest", revision: 2, lastOpenedAt: "2026-08-15T14:00:00.000Z" }),
]);
first.terminate();
const restarted = new StorageClient();
const listed = await restarted.listRecentProjects();
restarted.terminate();
return listed;
});
expect(result.quarantined).toBe(0);
expect(result.projects.map(({ projectId, revision }) => [projectId, revision])).toEqual([["older", 2], ["newer", 1]]);
});
test("M7-10 isolates a missing recent project and removes only its reference", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
await page.evaluate(async () => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const storage = new StorageClient();
await storage.touchRecentProject({
schemaVersion: 1,
projectId: "missing_recent",
displayName: "Missing recent.blend",
revision: 1,
bytes: 8,
sha256: "a".repeat(64),
updatedAt: "2026-08-15T12:00:00.000Z",
lastOpenedAt: "2026-08-15T12:00:00.000Z",
backend: "opfs",
});
storage.terminate();
});
const app = page.locator(".blender-app");
await page.getByTestId("blend-file-input").setInputFiles(basicBlend);
await expect(page.getByText("Cube", { exact: true })).toBeVisible();
const download = page.waitForEvent("download");
await page.getByRole("button", { name: "保存项目" }).click();
await download;
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await expect(app).toHaveAttribute("data-recent-project-invalid-count", "1");
await expect(app).toHaveAttribute("data-recent-project-invalid-ids", "missing_recent");
await expect(page.getByTestId("recent-project-repair-banner")).toContainText("项目内容缺失");
await expect(page.locator('[data-testid="recent-project-repair-banner"] [data-issue-code="MISSING"]')).toBeVisible();
await page.getByTestId("remove-recent-project-missing_recent").click();
await expect(app).toHaveAttribute("data-recent-project-invalid-count", "0");
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await expect(page.getByTestId("recent-project-repair-banner")).toHaveCount(0);
const listed = await page.evaluate(async () => {
const { StorageClient } = await import("/src/storage/StorageClient.ts");
const storage = new StorageClient();
const result = await storage.listRecentProjects();
storage.terminate();
return result;
});
expect(listed.projects.map(({ projectId }) => projectId)).toEqual(["basic_scene"]);
expect(listed.issues).toEqual([]);
});
test("M7-10 isolates a hash-mismatched entry while retaining other recent projects", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
const app = page.locator(".blender-app");
await page.getByTestId("blend-file-input").setInputFiles(basicBlend);
await expect(page.getByText("Cube", { exact: true })).toBeVisible();
const download = page.waitForEvent("download");
await page.getByRole("button", { name: "保存项目" }).click();
await download;
await expect(app).toHaveAttribute("data-recent-project-ids", "basic_scene");
await page.evaluate(async () => {
const database = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open("blender-web-metadata", 7);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const transaction = database.transaction("setting", "readwrite");
const store = transaction.objectStore("setting");
const row = await new Promise<{ value?: { projects?: Array<Record<string, unknown>> } } | undefined>((resolve, reject) => {
const request = store.get("recent-projects:v1");
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const projects = (row?.value?.projects ?? []).map((project) => project.projectId === "basic_scene" ? { ...project, sha256: "f".repeat(64) } : project);
store.put({ id: "recent-projects:v1", value: { schemaVersion: 1, projects } });
await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
database.close();
});
await page.reload();
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
await expect(app).toHaveAttribute("data-recent-project-invalid-count", "1");
await expect(app).toHaveAttribute("data-recent-project-invalid-ids", "basic_scene");
await expect(app).toHaveAttribute("data-recent-project-ids", "");
await expect(page.locator('[data-testid="recent-project-repair-banner"] [data-issue-code="HASH_MISMATCH"]')).toBeVisible();
await page.getByTestId("remove-recent-project-basic_scene").click();
await expect(app).toHaveAttribute("data-recent-project-invalid-count", "0");
await expect(app).toHaveAttribute("data-recent-project-ids", "");
await expect(page.getByText("Cube", { exact: true })).toHaveCount(0);
});