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, 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); });