Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
98 lines
7.2 KiB
TypeScript
98 lines
7.2 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
import { NativeUIWorkflow, exposeWebTools, observeNative } from "../fixtures/native-ui-workflow";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const fixture = path.join(root, "tests/files/web/nonmesh_scene.blend");
|
|
const fixtureSha256 = crypto.createHash("sha256").update(fs.readFileSync(fixture)).digest("hex");
|
|
const kinds = ["positive", "negative", "undo_redo", "save_reopen", "determinism"] as const;
|
|
const digest = (value: unknown) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
|
|
const call = (page: Page, command: unknown) => page.evaluate(command => new Promise<any>((resolve, reject) => {
|
|
const worker = (window as unknown as { __nativeWorkflowEngine: Worker }).__nativeWorkflowEngine;
|
|
const requestId = `metaball-${crypto.randomUUID()}`;
|
|
const receive = (event: MessageEvent) => {
|
|
if (event.data?.requestId !== requestId || event.data?.kind !== "result") return;
|
|
worker.removeEventListener("message", receive);
|
|
if (event.data.ok) resolve(event.data.result); else reject(event.data.error);
|
|
};
|
|
worker.addEventListener("message", receive);
|
|
worker.postMessage({ requestId, command });
|
|
}), command);
|
|
|
|
const readMetaball = async (page: Page) => {
|
|
const read = await call(page, { type: "snapshot" });
|
|
const data = (read.snapshot.nonMeshData ?? []).find((entry: any) => entry.type === "METABALL" && entry.elements?.length);
|
|
if (!data) throw new Error("fixture has no Metaball elements");
|
|
return {
|
|
dataId: data.id as string,
|
|
state: { revision: read.snapshot.revision, elements: data.elements.map((element: any) => ({ type: element.type, position: [...element.position], radius: element.radius, scale: [...element.scale] })) },
|
|
};
|
|
};
|
|
|
|
test("WBV2-P1137-001 preserves Metaball element edits through Main recovery and reopen", async ({ page }, testInfo) => {
|
|
test.setTimeout(180_000);
|
|
page.setDefaultTimeout(15_000);
|
|
const receipts = process.env.WBV2_RECEIPT_DIR ?? testInfo.outputPath("cases");
|
|
const directory = path.join(receipts, "metaball-artifacts");
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
const flow = new NativeUIWorkflow(page, directory);
|
|
const load = async (file: string) => flow.command(() => page.getByTestId("blend-file-input").setInputFiles(file));
|
|
await observeNative(page);
|
|
await page.goto("/");
|
|
await load(fixture);
|
|
let result: any = { error: { fatal: true } };
|
|
try {
|
|
const opened = await readMetaball(page);
|
|
const before = opened.state;
|
|
const elements = before.elements.map((element: any, index: number) => index === 0
|
|
? { ...element, position: [element.position[0] + 0.125, element.position[1], element.position[2]], radius: element.radius + 0.05 }
|
|
: { ...element, position: [...element.position], scale: [...element.scale] });
|
|
const edited = await call(page, { type: "applyCommand", payload: { type: "setMetaballElements", dataId: opened.dataId, elements } });
|
|
const afterEdit = { revision: edited.snapshot.revision, elements: edited.snapshot.nonMeshData.find((entry: any) => entry.id === opened.dataId).elements.map((element: any) => ({ type: element.type, position: [...element.position], radius: element.radius, scale: [...element.scale] })) };
|
|
const undo = await call(page, { type: "applyCommand", payload: { type: "undo" } });
|
|
const undoData = undo.snapshot.nonMeshData.find((entry: any) => entry.id === opened.dataId);
|
|
const afterUndo = { revision: undo.snapshot.revision, elements: undoData.elements.map((element: any) => ({ type: element.type, position: [...element.position], radius: element.radius, scale: [...element.scale] })) };
|
|
const redo = await call(page, { type: "applyCommand", payload: { type: "redo" } });
|
|
const afterRedo = { revision: redo.snapshot.revision, elements: redo.snapshot.nonMeshData.find((entry: any) => entry.id === opened.dataId).elements.map((element: any) => ({ type: element.type, position: [...element.position], radius: element.radius, scale: [...element.scale] })) };
|
|
const malformed = await call(page, { type: "applyCommand", payload: { type: "setMetaballElements", dataId: opened.dataId, elements: [{ ...elements[0], radius: -1 }, ...elements.slice(1)] } }).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message }));
|
|
const afterMalformed = (await readMetaball(page)).state;
|
|
await exposeWebTools(page);
|
|
const download = page.waitForEvent("download", { timeout: 30_000 });
|
|
await flow.command(() => page.getByRole("button", { name: "保存项目", exact: true }).click());
|
|
const savedPath = path.join(directory, "after.blend");
|
|
await (await download).saveAs(savedPath);
|
|
await load(savedPath);
|
|
const afterReopen = (await readMetaball(page)).state;
|
|
const savedSha256 = crypto.createHash("sha256").update(fs.readFileSync(savedPath)).digest("hex");
|
|
result = { fixtureSha256, dataId: opened.dataId, before, afterEdit, afterUndo, afterRedo, malformed, afterMalformed, afterReopen, savedSha256 };
|
|
} catch (error: any) { result = { error: { code: error?.code, message: error?.message, detail: String(error) } }; }
|
|
const receiptPath = path.join(receipts, "metaball-elements-production.json");
|
|
fs.writeFileSync(receiptPath, `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P1137-001", operation: "METABALL_ELEMENTS_MAIN_CHROMIUM", browser: "chromium", queueMutation: false, acceptance: "NOT_CLAIMED", ...result }, null, 2)}\n`);
|
|
if (!result.error) {
|
|
const observations = {
|
|
positive: { before: digest(result.before.elements), after: digest(result.afterEdit.elements) },
|
|
negative: { before: digest(result.afterEdit.elements), after: digest(result.afterMalformed.elements) },
|
|
undo_redo: { before: digest(result.before.elements), after: digest(result.afterEdit.elements), undo: digest(result.afterUndo.elements), redo: digest(result.afterRedo.elements) },
|
|
save_reopen: { saved: digest(result.afterEdit.elements), reopened: digest(result.afterReopen.elements) },
|
|
determinism: { first: digest(result.afterEdit.elements), second: digest(result.afterRedo.elements) },
|
|
};
|
|
for (const kind of kinds) fs.writeFileSync(path.join(receipts, `metaball-elements-${kind}.json`), `${JSON.stringify({
|
|
schemaVersion: 1, task: "WBV2-P1137-001", operation: "METABALL_ELEMENTS_MAIN_CHROMIUM", browser: "chromium", runtime: "CHROMIUM_WEBENGINE",
|
|
acceptance: "NOT_CLAIMED", status: "PASS", caseId: `metaball-elements-${kind}`, kind, unitIds: ["web-command:setMetaballElements"], observations: observations[kind],
|
|
}, null, 2)}\n`);
|
|
}
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.fixtureSha256).toBe(fixtureSha256);
|
|
expect(result.afterEdit.revision).toBe(result.before.revision + 1);
|
|
expect(result.afterEdit.elements[0].radius).toBeGreaterThan(result.before.elements[0].radius);
|
|
expect(result.afterUndo.elements).toEqual(result.before.elements);
|
|
expect(result.afterRedo.elements).toEqual(result.afterEdit.elements);
|
|
expect(result.malformed.code).not.toBe("NO_ERROR");
|
|
expect(result.afterMalformed.elements).toEqual(result.afterRedo.elements);
|
|
expect(result.afterReopen.elements).toEqual(result.afterRedo.elements);
|
|
expect(result.savedSha256).not.toBe(fixtureSha256);
|
|
});
|