Files
workinf_Blender_Wasm/web/tests/e2e/console-main-undo-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

124 lines
10 KiB
TypeScript

import { expect, test } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
import { observeNative, resolveProjectReplacement } from '../fixtures/native-ui-workflow';
import { expectWebLocation, receiptAfter, webMain } from '../fixtures/console-main-workflow';
const root = path.resolve(import.meta.dirname, "../../..");
const fixture = path.resolve(root, process.env.WBV2_CASE_FIXTURE ?? "tests/files/web/basic_scene.blend");
test('Console continuity preserves typed Main through error, workspace and reopen', async ({ page }, testInfo) => {
await observeNative(page); await page.goto('/?ui=web');
await page.getByTestId('blend-file-input').setInputFiles(fixture);
await expect(page.getByTestId('engine-status')).toContainText('SceneIR');
const evidence: Record<string, unknown> = {};
const record = (stage: string, value: unknown) => { evidence[stage] = value; fs.writeFileSync(testInfo.outputPath('b1-main-readback.json'), JSON.stringify(evidence, null, 2)); };
const opened = await webMain(page), target = opened.nodes.find(node => node.type === 'MESH'); expect(target).toBeDefined(); record('opened', opened);
const name = target!.name, quotedName = JSON.stringify(name);
await page.getByRole('button', { name: 'Scripting', exact: true }).click();
await page.getByRole('button', { name: 'Connect', exact: true }).click();
await expect(page.locator('.native-workflow-controls output')).toHaveText('READY', { timeout: 30000 });
const input = page.getByLabel('Persistent Python command');
const python = async (source: string, status = 'SUCCEEDED') => { await input.fill(source); return receiptAfter(page, () => page.getByRole('button', { name: 'Execute', exact: true }).click(), status); };
const initial = await python('import bpy\nb1_context_token = 42\nprint("B1_SUM", 1 + 1)'); record('initial', initial);
await expect(page.locator('.native-workflow-output')).toContainText('B1_SUM 2');
const before = initial.objects.find(object => object.name === name); expect(before).toBeDefined();
const changedX = before!.location[0] === 3 ? 4 : 3;
const changed = await python(`bpy.data.objects[${quotedName}].location.x = ${changedX}`); record('changed', changed);
const changedLocation = changed.objects.find(object => object.name === name)!.location;
expect(changedLocation).toEqual([changedX, before!.location[1], before!.location[2]]); await expectWebLocation(page, name, changedLocation);
const undone = await receiptAfter(page, () => page.getByRole('button', { name: 'Undo Main', exact: true }).click()); record('undone', undone);
expect(undone.objects.find(object => object.name === name)!.location).toEqual(before!.location); await expectWebLocation(page, name, before!.location);
const redone = await receiptAfter(page, () => page.getByRole('button', { name: 'Redo Main', exact: true }).click()); record('redone', redone);
expect(redone.objects.find(object => object.name === name)!.location).toEqual(changedLocation); await expectWebLocation(page, name, changedLocation);
const rejected = await python('def b1_invalid(:', 'PYTHON_ERROR'); record('syntaxError', rejected);
expect(rejected.objects).toEqual(redone.objects); await expectWebLocation(page, name, changedLocation);
const draft = 'print("B1_CONTEXT", b1_context_token)'; await input.fill(draft);
await page.getByRole('button', { name: 'Layout', exact: true }).click(); await expect(input).toBeHidden();
await page.getByRole('button', { name: 'Scripting', exact: true }).click(); await expect(input).toHaveValue(draft);
await expect(page.locator('.native-workflow-controls output')).toHaveText('READY');
const resumed = await python(draft); record('workspaceResumed', resumed);
await expect(page.locator('.native-workflow-output')).toContainText('B1_CONTEXT 42'); expect(resumed.objects).toEqual(redone.objects);
const committed = await receiptAfter(page, () => page.getByRole('button', { name: 'Apply to Web Scene', exact: true }).click()); record('committed', committed);
await expectWebLocation(page, name, changedLocation);
const download = page.waitForEvent('download'); await page.getByRole('link', { name: 'session.blend', exact: true }).click();
const saved = testInfo.outputPath('b1-continuity.blend'); await (await download).saveAs(saved);
const beforeOpen = await webMain(page), previousRead = beforeOpen.requestId;
await page.getByTestId('blend-file-input').setInputFiles(saved);
await resolveProjectReplacement(page, 'cancel'); const cancelledOpen = await webMain(page); record('cancelledOpen', cancelledOpen);
expect(cancelledOpen.revision).toBe(beforeOpen.revision); expect(cancelledOpen.nodes).toEqual(beforeOpen.nodes);
await page.getByTestId('blend-file-input').setInputFiles(saved); await resolveProjectReplacement(page, 'discard');
await expect.poll(async () => (await webMain(page)).requestId).not.toBe(previousRead);
await expect(page.getByTestId('engine-status')).toContainText('SceneIR'); await expectWebLocation(page, name, changedLocation);
record('reopened', await webMain(page)); await expect(page.getByRole('treeitem').filter({ hasText: name }).first()).toBeVisible();
record('result', { status: 'PASS', target: name, before: before!.location, expected: changedLocation, saved });
});
test("Console Main mutation restores through undo/redo and save/reopen", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const client = new WebEngineClient();
const getCubeX = (snapshot: any) => snapshot.nodes.find((node: any) => node.name === "BasicCube")?.transform.translation[0];
try {
const opened = await client.openBlend(Uint8Array.from(input).buffer);
const python = await client.executeEmbeddedPython("import bpy\nbpy.data.objects['BasicCube'].location.x = 3.0\nprint(1 + 1)", "ConsoleCase.py", true);
const undone = (await client.applyCommand({ type: "undo" })).snapshot;
const redone = (await client.applyCommand({ type: "redo" })).snapshot;
const saved = await client.saveBlend();
// One browser filesystem has a single native-file writer. Restart the same
// client so the reopen runs in a fresh worker after the old one releases it.
await client.restart();
const reopenedResult = await client.openBlend(saved.slice(0));
return { output: python.python.output, before: getCubeX(opened.snapshot), mutated: getCubeX(python.snapshot), undo: getCubeX(undone), redo: getCubeX(redone), reopened: getCubeX(reopenedResult.snapshot) };
} catch (error) {
// bpy/Python failures reject with a structured report; surface its code here
// so the run records a diagnosable failure instead of a bare "Object".
const code = error && typeof error === 'object' && 'code' in error ? String((error as { code?: unknown }).code) : 'UNKNOWN';
const detail = error && typeof error === 'object' && 'message' in error ? String((error as { message?: unknown }).message) : String(error);
throw new Error(`${code}: ${detail}`);
} finally { client.terminate(); }
}, Array.from(fs.readFileSync(fixture)));
expect(typeof result.output).toBe("string"); expect(result.output).toContain("2");
for (const value of [result.before, result.mutated, result.undo, result.redo, result.reopened]) { expect(typeof value).toBe("number"); expect(Number.isFinite(value)).toBe(true); }
expect(result.mutated).toBe(3);
expect(result.undo).toBe(result.before);
expect(result.redo).toBe(result.mutated);
expect(result.reopened).toBe(result.redo);
});
test("Web Scripting workflow saves and reopens the native Main", async ({ page }, testInfo) => {
await page.goto("/?ui=web");
await page.getByTestId("blend-file-input").setInputFiles(fixture);
await expect(page.getByTestId("engine-status")).toContainText("SceneIR");
await page.getByRole("button", { name: "Scripting", exact: true }).click();
await page.getByRole("button", { name: "Connect", exact: true }).click();
await expect(page.locator(".native-workflow-controls output")).toHaveText("READY", { timeout: 30_000 });
const input = page.getByLabel("Persistent Python command");
await input.fill("import bpy\nbpy.data.objects['BasicCube'].location.x = 3\nbpy.data.objects['BasicCamera'].location.x = 3\nprint('B1_SUM', 1 + 1)\nprint('B1_X', bpy.data.objects['BasicCube'].location.x)");
await page.getByRole("button", { name: "Execute", exact: true }).click();
await expect(page.locator(".native-workflow-output")).toContainText("B1_SUM 2");
const undone = await receiptAfter(page, () => page.getByRole('button', { name: 'Undo Main', exact: true }).click());
expect(undone.objects.find(object => object.name === 'BasicCube')?.location[0]).toBe(0);
const redone = await receiptAfter(page, () => page.getByRole('button', { name: 'Redo Main', exact: true }).click());
expect(redone.objects.find(object => object.name === 'BasicCube')?.location[0]).toBe(3);
await page.getByRole("button", { name: "Apply to Web Scene", exact: true }).click();
await expect(page.locator(".native-session-panel")).toHaveAttribute("data-command-status", "SUCCEEDED", { timeout: 30_000 });
await expect(page.locator(".native-workflow-artifacts a")).toHaveCount(2);
const download = page.waitForEvent("download");
await page.getByRole("link", { name: "session.blend", exact: true }).click();
const saved = testInfo.outputPath("b1-native-session.blend");
await (await download).saveAs(saved);
page.on("dialog", dialog => void dialog.accept());
await page.getByTestId("blend-file-input").setInputFiles(saved);
await expect(page.getByTestId("engine-status")).toContainText("SceneIR", { timeout: 30_000 });
});
test("Browser-equivalent File menu saves and reopens the Main", async ({ page }, testInfo) => {
await page.goto("/?ui=web"); await page.getByTestId("blend-file-input").setInputFiles(fixture);
await expect(page.getByTestId("engine-status")).toContainText("SceneIR");
await page.getByRole("button", { name: "文件", exact: true }).click();
const download = page.waitForEvent("download"); await page.getByRole("menuitem", { name: "保存", exact: true }).click();
const file = await download; const saved = testInfo.outputPath("b1-file-menu-browser-equivalent.blend"); await file.saveAs(saved);
await page.setInputFiles("[data-testid=blend-file-input]", saved); await expect(page.getByTestId("engine-status")).toContainText("SceneIR");
});