Files
workinf_Blender_Wasm/web/tests/e2e/wbv2-sculpt-stroke-production.spec.ts
mes123456 f3e21c6b98
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
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

83 lines
6.0 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
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/basic_scene.blend");
const fixtureSha256 = crypto.createHash("sha256").update(fs.readFileSync(fixture)).digest("hex");
/* One browser holds /browser exclusively, so the case drives the app's own engine
* worker on the real file-open entry instead of opening a second writer. */
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 = `sculpt-${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);
test("WBV2-P1121-000 preserves bounded Sculpt stroke Main mutation", async ({ page }, testInfo) => {
test.setTimeout(180_000);
page.setDefaultTimeout(15_000);
const receipts = process.env.WBV2_RECEIPT_DIR ?? testInfo.outputPath("cases");
fs.mkdirSync(receipts, { recursive: true });
const flow = new NativeUIWorkflow(page, receipts);
const load = (file: string) => flow.command(() => page.getByTestId("blend-file-input").setInputFiles(file));
await observeNative(page);
await page.goto("/");
await load(fixture);
const geometry = (snapshot: any, meshId: string) => {
const mesh = snapshot.meshes?.find((entry: any) => entry.id === meshId);
if (!mesh) throw new Error(`mesh missing: ${meshId}`);
return { revision: snapshot.revision, bounds: mesh.bounds };
};
let result: any = { error: { fatal: true } };
try {
const opened = await call(page, { type: "snapshot" });
const mesh = opened.snapshot.meshes?.find((entry: any) => entry.id === opened.snapshot.nodes.find((node: any) => node.type === "MESH")?.dataId);
const object = opened.snapshot.nodes?.find((node: any) => node.type === "MESH");
if (!mesh || !object) throw new Error("basic fixture has no editable Mesh");
const before = geometry(opened.snapshot, mesh.id);
const stroke = { schemaVersion: 1, meshId: mesh.id, brush: "DRAW", samples: [{ position: [0, 0, 0], normal: [0, 0, 1], radius: 100, strength: 0.5, pressure: 1, time: 0 }], symmetry: [false, false, false], mirrorObjectSpace: true };
const edited = await call(page, { type: "applyCommand", payload: { type: "sculptStroke", stroke } });
const afterEdit = geometry(edited.snapshot, mesh.id);
const undo = await call(page, { type: "applyCommand", payload: { type: "undo" } });
const afterUndo = geometry(undo.snapshot, mesh.id);
const redo = await call(page, { type: "applyCommand", payload: { type: "redo" } });
const afterRedo = geometry(redo.snapshot, mesh.id);
const attributePayload = { schemaVersion: 1, meshId: mesh.id, vertexCount: mesh.vertexCount, faceCount: mesh.faceCount, mask: Array(mesh.vertexCount).fill(0.25), faceSets: Array(mesh.faceCount).fill(2), activeFaceSet: -1, revision: redo.snapshot.revision };
const attributesEdit = await call(page, { type: "applyCommand", payload: { type: "setSculptMeshAttributes", attributes: attributePayload } });
const attributesMesh = attributesEdit.snapshot.meshes?.find((entry: any) => entry.id === mesh.id);
const malformed = await call(page, { type: "applyCommand", payload: { type: "sculptStroke", stroke: { ...stroke, samples: [{ ...stroke.samples[0], pressure: 2 }] } } }).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message }));
const afterMalformed = geometry((await call(page, { type: "snapshot" })).snapshot, mesh.id);
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(receipts, "sculpt-after.blend");
await (await download).saveAs(savedPath);
await load(savedPath);
const afterReopen = geometry((await call(page, { type: "snapshot" })).snapshot, mesh.id);
result = { fixtureSha256, meshId: mesh.id, before, afterEdit, afterUndo, afterRedo, attributesRevision: attributesEdit.snapshot.revision, attributes: { sculptMask: attributesMesh?.sculptMask, faceSets: attributesMesh?.faceSets }, malformed, afterMalformed, afterReopen, savedSha256: crypto.createHash("sha256").update(fs.readFileSync(savedPath)).digest("hex") };
} catch (error: any) { result = { error: { code: error?.code, message: error?.message, detail: String(error) } }; }
fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1121-000"), { recursive: true });
fs.writeFileSync(path.join(receipts, "sculpt-stroke-production.json"), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P1121-000", operation: "SCULPT_STROKE_MAIN_CHROMIUM", browser: "chromium", queueMutation: false, acceptance: "NOT_CLAIMED", ...result }, null, 2)}\n`);
expect(result.error).toBeUndefined();
expect(result.fixtureSha256).toBe(fixtureSha256);
expect(result.afterEdit.revision).toBe(result.before.revision + 1);
expect(result.afterEdit.bounds.max[2]).toBeGreaterThan(result.before.bounds.max[2]);
expect(result.afterUndo.bounds).toEqual(result.before.bounds);
expect(result.afterRedo.bounds).toEqual(result.afterEdit.bounds);
expect(result.attributesRevision).toBe(result.afterRedo.revision + 1);
expect(result.malformed.code).not.toBe("NO_ERROR");
expect(result.afterMalformed.bounds).toEqual(result.afterRedo.bounds);
expect(result.afterReopen.bounds).toEqual(result.afterRedo.bounds);
expect(result.afterReopen.revision).toBe(1);
expect(result.savedSha256).not.toBe(fixtureSha256);
});