- 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>
72 lines
5.5 KiB
TypeScript
72 lines
5.5 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"));
|
|
const cases = [
|
|
{ operator: "armature.bone_primitive_add", properties: { name: "ProbeBone" }, grows: true },
|
|
{ operator: "armature.subdivide", properties: {}, grows: true },
|
|
{ operator: "armature.duplicate", properties: {}, grows: true },
|
|
{ operator: "armature.calculate_roll", properties: { type: "GLOBAL_POS_Z" }, grows: false },
|
|
];
|
|
test("armature edit operators change native bones", async ({ page }) => {
|
|
test.setTimeout(180_000);
|
|
await page.goto("/?engine=manual");
|
|
const results = await page.evaluate(async ({ bytes, cases }) => {
|
|
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
|
const out: Record<string, unknown>[] = [];
|
|
for (const item of cases) {
|
|
const client = new WebEngineClient({ timeoutMs: 30_000 });
|
|
const reopened = new WebEngineClient({ timeoutMs: 30_000 });
|
|
try {
|
|
const open = await client.openBlend(Uint8Array.from(bytes).buffer);
|
|
const mesh = open.snapshot.nodes.find((n) => n.type === "MESH");
|
|
let revision = open.snapshot.revision;
|
|
const invoke = async (operator: string, properties: Record<string, unknown>, mode: string, activeName: string, selected: string[]) => {
|
|
const payload = JSON.stringify({ operator, properties, baseRevision: revision, sceneName: open.snapshot.scenes?.[0]?.name ?? "Scene", areaType: "VIEW_3D", mode, native: true, selectedObjects: selected, activeObject: activeName, meshSelection: null, nonMeshSelection: null });
|
|
const source = `import json, web_blender\nweb_blender.desktop_operator(json.loads(${JSON.stringify(payload)}))`;
|
|
const res = await client.executeEmbeddedPython(source, "Armature.py", false);
|
|
revision = res.snapshot.revision;
|
|
if (res.python.state !== "DONE") throw new Error(res.python.error ?? res.python.output ?? res.python.state);
|
|
return res.snapshot;
|
|
};
|
|
const created = await invoke("object.armature_add", {}, "OBJECT", mesh?.name ?? "", mesh ? [mesh.name] : []);
|
|
const armature = created.nodes.find((n) => n.type === "ARMATURE");
|
|
if (!armature) throw new Error("armature node was not created");
|
|
await invoke("object.mode_set", { mode: "EDIT", toggle: false }, "OBJECT", armature.name, [armature.name]);
|
|
await invoke("armature.select_all", { action: "SELECT" }, "EDIT", armature.name, [armature.name]);
|
|
const boneCount = async () => {
|
|
const res = await client.executeEmbeddedPython(`import bpy\nob = bpy.data.objects.get(${JSON.stringify(armature.name)})\nprint("BONES:" + str(len(ob.data.edit_bones)))`, "Bones.py", false);
|
|
return Number(res.python.output.split("\n").find((line) => line.startsWith("BONES:"))?.slice("BONES:".length));
|
|
};
|
|
const bonesBefore = await boneCount();
|
|
await invoke(item.operator, item.properties, "EDIT", armature.name, [armature.name]);
|
|
const bonesAfter = await boneCount();
|
|
const saved = await client.saveBlend();
|
|
client.terminate();
|
|
const reopenedOpen = await reopened.openBlend(saved);
|
|
const reopenedArmature = reopenedOpen.snapshot.nodes.find((n) => n.type === "ARMATURE");
|
|
if (!reopenedArmature) throw new Error("armature was not persisted");
|
|
const reopenModePayload = JSON.stringify({ operator: "object.mode_set", properties: { mode: "EDIT", toggle: false }, baseRevision: reopenedOpen.snapshot.revision, sceneName: reopenedOpen.snapshot.scenes?.[0]?.name ?? "Scene", areaType: "VIEW_3D", mode: "OBJECT", native: true, selectedObjects: [reopenedArmature.name], activeObject: reopenedArmature.name, meshSelection: null, nonMeshSelection: null });
|
|
const reopenMode = await reopened.executeEmbeddedPython(`import json, web_blender\nweb_blender.desktop_operator(json.loads(${JSON.stringify(reopenModePayload)}))`, "ReopenMode.py", false);
|
|
if (reopenMode.python.state !== "DONE") throw new Error(reopenMode.python.error ?? reopenMode.python.output ?? "reopen edit mode failed");
|
|
const reopenCount = await reopened.executeEmbeddedPython(`import bpy
|
|
ob = bpy.data.objects.get(${JSON.stringify(reopenedArmature.name)})
|
|
print("BONES:" + str(len(ob.data.edit_bones)))`, "BonesReopen.py", false);
|
|
const reopenedBones = Number(reopenCount.python.output.split("\n").find((line) => line.startsWith("BONES:"))?.slice("BONES:".length));
|
|
out.push({ ...item, status: "FINISHED", bonesBefore, bonesAfter, reopenedBones });
|
|
} catch (error) {
|
|
const e = error as { message?: string; code?: string };
|
|
out.push({ ...item, status: "ERROR", message: e?.message, code: e?.code });
|
|
} finally { client.terminate(); reopened.terminate(); await new Promise((resolve) => setTimeout(resolve, 120)); }
|
|
}
|
|
return out;
|
|
}, { bytes: Array.from(fixture), cases });
|
|
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1425-001/verification/armature-edit-production.json"), JSON.stringify({ schemaVersion: 1, browser: "chromium", scope: "ARMATURE create -> Edit -> primitive add/subdivide/duplicate/calculate roll -> save/reopen", results }, null, 2));
|
|
expect(results.filter((item) => item.status !== "FINISHED")).toEqual([]);
|
|
for (const item of results) {
|
|
expect(item.bonesAfter).toBe(item.grows ? item.bonesBefore + 1 : item.bonesBefore);
|
|
expect(item.reopenedBones).toBe(item.bonesAfter);
|
|
}
|
|
});
|