Files
workinf_Blender_Wasm/web/tests/e2e/wbv2-object-tools-production.spec.ts
mes123456 d1e6b63d54
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 feature-gap work, docs and tests
- update native web_engine sources and rebuilt wasm artifacts
- add task cards, status notes and tooling scripts
- add web e2e specs, tutorial UI sources and tutorial assets
- sync golden task manifests and audit records

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-11 10:46:55 +08:00

67 lines
5.3 KiB
TypeScript

import { test, expect } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(import.meta.dirname, "../../..");
const fixture = fs.readFileSync(path.join(root, "tests/files/web/basic_scene.blend"));
test("shape keys, vertex groups, modifier copy and apply-as-shape-key", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("/?engine=manual");
const result = await page.evaluate(async ({ bytes }) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const run = async (setup: (client: InstanceType<typeof WebEngineClient>, node: { id: string; name: string; dataId: string }) => Promise<Record<string, unknown>>) => {
const client = new WebEngineClient({ timeoutMs: 30_000 });
try {
const open = await client.openBlend(Uint8Array.from(bytes).buffer);
const node = open.snapshot.nodes.find((n) => n.name === "BasicCube");
if (!node?.dataId) throw new Error("BasicCube is missing");
return { ok: true, ...(await setup(client, node as { id: string; name: string; dataId: string })) };
} catch (error) { const e = error as { message?: string; code?: string }; return { ok: false, error: e?.message, code: e?.code }; }
finally { client.terminate(); }
};
const inspect = async (client: InstanceType<typeof WebEngineClient>) => {
const res = await client.executeEmbeddedPython(`import bpy, json
ob = bpy.data.objects.get("BasicCube")
print("OBJECT_STATE:" + json.dumps({"shapeKeys": len(ob.data.shape_keys.key_blocks) if ob.data.shape_keys else 0, "vertexGroups": len(ob.vertex_groups), "modifiers": [m.name + ":" + m.type for m in ob.modifiers]}))`, "ObjectState.py", false);
const line = res.python.output.split("\n").find((item) => item.startsWith("OBJECT_STATE:"));
return line ? JSON.parse(line.slice("OBJECT_STATE:".length)) : null;
};
const operator = async (client: InstanceType<typeof WebEngineClient>, operatorName: string, properties: Record<string, unknown>) => {
const payload = JSON.stringify({ operator: operatorName, properties, baseRevision: (await client.history()).revision, sceneName: "Scene", areaType: "VIEW_3D", mode: "OBJECT", native: true, selectedObjects: ["BasicCube"], activeObject: "BasicCube", meshSelection: null, nonMeshSelection: null });
return client.executeEmbeddedPython(`import json, web_blender\nweb_blender.desktop_operator(json.loads(${JSON.stringify(payload)}))`, "ObjectTools.py", false);
};
const shapeKeys = await run(async (client) => {
await operator(client, "object.shape_key_add", { from_mix: false });
const res = await operator(client, "object.shape_key_add", { from_mix: false });
return { pythonState: res.python.state, ...(await inspect(client) ?? {}) };
});
const vertexGroup = await run(async (client) => {
const res = await operator(client, "object.vertex_group_add", {});
return { pythonState: res.python.state, ...(await inspect(client) ?? {}) };
});
const modifierCopy = await run(async (client, node) => {
const history = await client.history();
const added = await client.applyCommand({ type: "addModifier", objectId: node.id, meshId: node.dataId, modifierType: "SUBSURF", baseRevision: history.revision } as never);
const modifier = (added.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? []).find((m) => m.type === "SUBSURF");
if (!modifier) throw new Error("SUBSURF was not published");
const res = await operator(client, "object.modifier_copy", { modifier: modifier.name });
const stack = res.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? [];
return { pythonState: res.python.state, stack: stack.map((m) => m.type) };
});
const applyAsShapeKey = await run(async (client, node) => {
const history = await client.history();
const added = await client.applyCommand({ type: "addModifier", objectId: node.id, meshId: node.dataId, modifierType: "SIMPLE_DEFORM", baseRevision: history.revision } as never);
const modifier = (added.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? []).find((m) => m.type === "SIMPLE_DEFORM");
if (!modifier) throw new Error("SIMPLE_DEFORM was not published");
const res = await operator(client, "object.modifier_apply_as_shapekey", { modifier: modifier.name, keep_modifier: true });
return { pythonState: res.python.state, ...(await inspect(client) ?? {}) };
});
return { shapeKeys, vertexGroup, modifierCopy, applyAsShapeKey };
}, { bytes: Array.from(fixture) });
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1425-001/verification/object-tools-production.json"), JSON.stringify({ schemaVersion: 1, browser: "chromium", scope: "shape keys, vertex groups, modifier copy, apply as shape key", ...result }, null, 2));
expect(result.shapeKeys).toMatchObject({ ok: true, pythonState: "DONE", shapeKeys: 2 });
expect(result.vertexGroup).toMatchObject({ ok: true, pythonState: "DONE", vertexGroups: 1 });
expect(result.modifierCopy).toMatchObject({ ok: true, pythonState: "DONE", stack: ["SUBSURF", "SUBSURF"] });
expect(result.applyAsShapeKey).toMatchObject({ ok: true, pythonState: "DONE", shapeKeys: 2 });
expect((result.applyAsShapeKey as { modifiers?: string[] }).modifiers?.some((item) => item.endsWith(":SIMPLE_DEFORM"))).toBe(true);
});