import fs from "node:fs"; import path from "node:path"; import { expect, test } from "@playwright/test"; const root = path.resolve(import.meta.dirname, "../../.."); const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8")); const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1"); function stableSnapshot(snapshot: Record) { return { objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(), meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(), materials: snapshot.materials.map((material: any) => material.id).sort(), images: snapshot.images.map((image: any) => image.id).sort(), armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(), actions: snapshot.animations.map((animation: any) => animation.id).sort(), }; } async function sha256(bytes: ArrayBuffer): Promise { const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); } test("persists desktop-imported GLB Main data through WebEngine save and reopen", async ({ page }) => { await page.goto("/"); const fixtures = report.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string }; graph: { stableIds: Record } }) => ({ id: fixture.id, bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))), sourceSha256: fixture.blend.sha256, expected: fixture.graph.stableIds, })); const result = await page.evaluate(async (input) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const output = []; const stableSnapshot = (snapshot: Record) => ({ objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(), meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(), materials: snapshot.materials.map((material: any) => material.id).sort(), images: snapshot.images.map((image: any) => image.id).sort(), armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(), actions: snapshot.animations.map((animation: any) => animation.id).sort(), }); const digestSha256 = async (bytes: ArrayBuffer): Promise => { const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); }; for (const fixture of input) { const client = new WebEngineClient({ timeoutMs: 30_000 }); try { const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer); const before = opened.snapshot; const beforeIds = stableSnapshot(before); const objectId = before.activeObjectId ?? before.nodes.find((node: any) => node.id?.startsWith("object:"))?.id; if (!objectId) throw new Error(`${fixture.id} did not produce an active Main object`); const edited = await client.applyCommand({ type: "setObjectVisibility", objectId, visible: false }); const saved = await client.saveBlend(); const savedSha256 = await digestSha256(saved); if (savedSha256 === fixture.sourceSha256) throw new Error(`${fixture.id} save did not serialize the Main visibility edit`); const reopened = await client.openBlend(saved); const after = reopened.snapshot; const afterIds = stableSnapshot(after); const afterObject = after.nodes.find((node: any) => node.id === objectId); const resources = await client.openResourceStatus(); output.push({ id: fixture.id, before: beforeIds, after: afterIds, expected: fixture.expected, revision: [before.revision, edited.snapshot.revision, after.revision], savedSha256, sourceSha256: fixture.sourceSha256, visibilityAfterReopen: afterObject?.visible, resources, }); } finally { client.terminate(); } } return output; }, fixtures); for (const item of result) { expect(item.before).toEqual(item.expected); expect(item.after).toEqual(item.before); expect(item.revision[1]).toBeGreaterThan(item.revision[0]); expect(item.revision[2]).toBeGreaterThan(0); expect(item.savedSha256).not.toEqual(item.sourceSha256); expect(item.visibilityAfterReopen).toBe(false); expect(item.resources).toMatchObject({ activeRequests: 0, liveInputBytes: 0, liveStagingFiles: 0 }); expect(item.before.objects.every((id: string) => id.startsWith("object:"))).toBe(true); expect(item.before.meshes.every((id: string) => id.startsWith("mesh:"))).toBe(true); expect(item.before.materials.every((id: string) => id.startsWith("material:"))).toBe(true); expect(item.before.images.every((id: string) => id.startsWith("image:"))).toBe(true); } expect(result).toHaveLength(5); });