- 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>
75 lines
5.2 KiB
TypeScript
75 lines
5.2 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("nested collection instances expand and select the nearest instance owner", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
await page.goto("/?engine=manual");
|
|
const result = await page.evaluate(async ({ bytes }) => {
|
|
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
try {
|
|
const open = await client.openBlend(Uint8Array.from(bytes).buffer);
|
|
const sceneName = open.snapshot.scenes?.[0]?.name ?? "Scene";
|
|
const mesh = open.snapshot.nodes.find((node) => node.name === "BasicCube");
|
|
const outerCollection = open.snapshot.collections.find((collection) => collection.name === "Collection");
|
|
if (!mesh || !outerCollection) throw new Error("fixture collection or mesh is missing");
|
|
let revision = open.snapshot.revision;
|
|
const invoke = async (operator: string, properties: Record<string, unknown>, activeName: string) => {
|
|
const payload = JSON.stringify({ operator, properties, baseRevision: revision, sceneName, areaType: "VIEW_3D", mode: "OBJECT", native: true, selectedObjects: [activeName], activeObject: activeName, meshSelection: null, nonMeshSelection: null });
|
|
const res = await client.executeEmbeddedPython(`import json, web_blender\nweb_blender.desktop_operator(json.loads(${JSON.stringify(payload)}))`, "NestedInstance.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 createdCollection = await client.applyCommand({ type: "createCollection", name: "NestedInner" } as never);
|
|
revision = createdCollection.snapshot.revision;
|
|
const inner = createdCollection.snapshot.collections.find((collection) => collection.name === "NestedInner");
|
|
if (!inner) throw new Error("inner collection was not created");
|
|
const moved = await client.applyCommand({ type: "moveObjectToCollection", objectId: mesh.id, collectionId: inner.id, keepExistingLinks: false } as never);
|
|
revision = moved.snapshot.revision;
|
|
const withInnerInstance = await invoke("object.collection_instance_add", { name: "NestedInner" }, mesh.name);
|
|
const innerInstance = withInnerInstance.nodes.find((node) => node.instanceCollectionId === inner.id);
|
|
if (!innerInstance) throw new Error("inner collection instance was not created");
|
|
const movedInstance = await client.applyCommand({ type: "moveObjectToCollection", objectId: innerInstance.id, collectionId: outerCollection.id, keepExistingLinks: false } as never);
|
|
revision = movedInstance.snapshot.revision;
|
|
const withOuterInstance = await invoke("object.collection_instance_add", { name: "Collection" }, mesh.name);
|
|
revision = withOuterInstance.revision;
|
|
const outerInstance = withOuterInstance.nodes.find((node) => node.instanceCollectionId === outerCollection.id);
|
|
if (!outerInstance) throw new Error("outer collection instance was not created");
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 320;
|
|
canvas.height = 240;
|
|
document.body.appendChild(canvas);
|
|
const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts");
|
|
const renderer = new ViewportRenderer(canvas);
|
|
renderer.setSnapshot(withOuterInstance, [], []);
|
|
const renderedCopies = Number(canvas.dataset.collectionInstanceObjects ?? "0");
|
|
const renderedHelpers = Number(canvas.dataset.renderHelpers ?? "0");
|
|
const clones: Array<{ memberId: string | null; blenderId: string | null }> = [];
|
|
renderer.scene.traverse((object) => {
|
|
if (object.userData.instanceMemberId) clones.push({ memberId: String(object.userData.instanceMemberId), blenderId: typeof object.userData.blenderId === "string" ? object.userData.blenderId : null });
|
|
});
|
|
renderer.dispose();
|
|
canvas.remove();
|
|
return {
|
|
innerCollectionId: inner.id,
|
|
innerInstanceId: innerInstance.id,
|
|
outerInstanceId: outerInstance.id,
|
|
renderedCopies,
|
|
renderedHelpers,
|
|
nestedMeshOwners: clones.filter((clone) => clone.memberId === mesh.id).map((clone) => clone.blenderId),
|
|
allClones: clones,
|
|
};
|
|
} catch (error) { const e = error as { message?: string; code?: string }; return { error: e?.message, code: e?.code }; }
|
|
finally { client.terminate(); }
|
|
}, { bytes: Array.from(fixture) });
|
|
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1425-001/verification/nested-collection-instance-production.json"), JSON.stringify({ schemaVersion: 1, browser: "chromium", scope: "collection instance inside collection instance: recursive expansion + nearest owner picking", ...result }, null, 2));
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.renderedCopies).toBeGreaterThanOrEqual(3);
|
|
expect(result.renderedHelpers).toBeGreaterThan(0);
|
|
expect(result.nestedMeshOwners).toContain(result.innerInstanceId);
|
|
expect(result.nestedMeshOwners).not.toContain(result.outerInstanceId);
|
|
});
|