Files
workinf_Blender_Wasm/web/tests/e2e/wbv2-fcurve-sample-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.4 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 fixturePath = process.env.WBV2_BATCH_FIXTURE ?? "tests/files/web/generated/M16-GAP-00161-action-fcurve-samples.blend";
const fixture = path.join(root, fixturePath);
const fixtureSha256 = crypto.createHash("sha256").update(fs.readFileSync(fixture)).digest("hex");
const task = process.env.WBV2_BATCH_TASK ?? "WBV2-P1142-001";
const productionPath = process.env.WBV2_BATCH_PRODUCTION ?? `tests/golden/${task}/fcurve-sample-production.json`;
const sourceUnits = JSON.parse(process.env.WBV2_BATCH_UNITS ?? "[\"rna-struct:FCurveSample\"]") as string[];
/* 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 send = (page: Page, command: unknown) => page.evaluate(command => new Promise<any>((resolve, reject) => {
const worker = (window as unknown as { __nativeWorkflowEngine: Worker }).__nativeWorkflowEngine;
const requestId = `fcurve-${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(new Error(`${event.data.error?.code ?? "NATIVE_ERROR"}: ${event.data.error?.message ?? JSON.stringify(event.data.error)}`));
};
worker.addEventListener("message", receive);
worker.postMessage({ requestId, command });
}), command);
test("WBV2 sampled FCurve data is read from Blender Main and survives recovery/reopen", 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));
const summarize = (snapshot: any) => {
const animation = snapshot.animations?.find((entry: any) => entry.targetId === "object:WebGapAnimChannelsGroupObject");
const channel = animation?.channels?.find((entry: any) => entry.path === "location[0]");
return { animationId: animation?.id, keyframeSource: channel?.keyframeSource, frames: channel?.keyframes?.map((keyframe: any) => keyframe.frame), values: channel?.keyframes?.map((keyframe: any) => keyframe.value?.[0]), channelCount: animation?.channels?.length ?? 0 };
};
await observeNative(page);
await page.goto("/");
await load(fixture);
let result: any = { error: { fatal: true } };
try {
const opened = await send(page, { type: "snapshot" });
const before = summarize(opened.snapshot);
if (before.keyframeSource !== "SAMPLED" || before.frames?.length !== 4) throw new Error("sampled FCurve source reader data missing");
/* Desktop keeps the frame cursor out of undo history, so the case edits a real
* keyframe: removing frame 3 from LOCATION is what a Dopesheet delete does. */
const edited = await send(page, { type: "applyCommand", payload: { type: "deleteObjectKeyframe", objectId: "object:WebGapAnimChannelsGroupObject", frame: 3, property: "LOCATION" } });
const channels = (snapshot: any) => (snapshot.animations?.find((entry: any) => entry.targetId === "object:WebGapAnimChannelsGroupObject")?.channels ?? []).map((entry: any) => ({ path: entry.path, source: entry.keyframeSource, frames: entry.keyframes?.map((keyframe: any) => keyframe.frame) }));
const afterEdit = channels(edited.snapshot);
const undo = await send(page, { type: "applyCommand", payload: { type: "undo" } });
const afterUndo = channels(undo.snapshot);
const redo = await send(page, { type: "applyCommand", payload: { type: "redo" } });
const afterRedo = channels(redo.snapshot);
const malformed = await send(page, { type: "applyCommand", payload: { type: "deleteObjectKeyframe", objectId: "object:WebGapAnimChannelsGroupObject", frame: 42, property: "LOCATION" } }).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message }));
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, "fcurve-after.blend");
await (await download).saveAs(savedPath);
await load(savedPath);
const reopened = channels((await send(page, { type: "snapshot" })).snapshot);
result = { fixtureSha256, before, afterEdit, afterUndo, afterRedo, malformed, reopened, openedRevision: opened.snapshot.revision, editedRevision: edited.snapshot.revision, undoRevision: undo.snapshot.revision, redoRevision: redo.snapshot.revision, 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.dirname(path.join(root, productionPath)), { recursive: true });
fs.writeFileSync(path.join(root, productionPath), `${JSON.stringify({ schemaVersion: 1, task, operation: "FCURVE_SAMPLE_READER_MAIN_CHROMIUM", browser: "chromium", queueMutation: false, acceptance: "NOT_CLAIMED", fixture: fixturePath, fixtureSha256, sourceUnits, sourceRnaStructs: { "rna-struct:FCurveSample": "animations[].channels[].keyframes[] where keyframeSource=SAMPLED" }, ...result }, null, 2)}\n`);
expect(result.error).toBeUndefined();
expect(result.fixtureSha256).toBe(fixtureSha256);
expect(result.before.keyframeSource).toBe("SAMPLED");
expect(result.before.frames).toEqual([1, 2, 3, 4]);
expect(result.before.values).toEqual([0, 0.625, 2, 3.375]);
expect(result.editedRevision).toBe(result.openedRevision + 1);
expect(result.afterEdit).not.toEqual(result.afterUndo);
expect(result.afterUndo).toEqual(result.afterEdit.map((channel: any) => channel.path === "location[0]" ? channel : { ...channel, frames: [1, 3, 5] }));
expect(result.afterRedo).toEqual(result.afterEdit);
expect(result.malformed.code).not.toBe("NO_ERROR");
expect(result.reopened).toEqual(result.afterEdit);
expect(result.savedSha256).not.toBe(fixtureSha256);
});