import { expect, test } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const fixtures = [ "tests/golden/WBV2-P1424-001/fixtures-b4-world-readback/minimal.blend", "tests/golden/WBV2-P1424-001/fixtures-b4-world-readback/production.blend", ]; for (const fixturePath of fixtures) test(`B4 generated Properties roundtrip: ${path.basename(fixturePath)}`, async ({ page }, testInfo) => { const bytes = fs.readFileSync(path.join(root, fixturePath)); await page.goto("/"); const result = await page.evaluate(async input => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const client = new WebEngineClient({ timeoutMs: 60_000 }); const reopened = new WebEngineClient({ timeoutMs: 60_000 }); try { const opened = await client.openBlend(Uint8Array.from(input).buffer); const scene = opened.snapshot.scenes[0]; const camera = opened.snapshot.cameras[0], light = opened.snapshot.lights[0], world = opened.snapshot.worlds[0]; if (!camera || !light || !world || !scene) throw new Error("B4 fixture missing camera/light/world/scene"); if (Math.abs((world.environmentStrength ?? NaN) - 2.5) > 1e-5) throw new Error(`B4 fixture strength mismatch: ${world.environmentStrength}`); const cameraEdit = await client.applyCommand({ type: "setCameraProperties", dataId: camera.id, properties: { lensMm: 42, near: 0.2, far: 800 } } as any); const lightEdit = await client.applyCommand({ type: "setLightProperties", dataId: light.id, properties: { energy: 375, radius: 0.4 } } as any); const worldEdit = await client.applyCommand({ type: "setWorldProperties", dataId: world.id, properties: { color: [0.2, 0.3, 0.4], exposure: 0.3, environmentStrength: 2.5 } } as any); const outputEdit = await client.applyCommand({ type: "setSceneRenderSettings", dataId: scene.id, properties: { resolutionPercentage: 80 } } as any); const beforeBad = (await client.snapshot()).snapshot; const bad = await client.applyCommand({ type: "setCameraProperties", dataId: camera.id, properties: { near: 9, far: 1 } } as any).then(() => "NO_ERROR").catch((error: any) => error?.code || error?.message); const afterBad = (await client.snapshot()).snapshot; const undo = await client.applyCommand({ type: "undo" } as any); const redo = await client.applyCommand({ type: "redo" } as any); const saved = await client.saveBlend(); const restored = await reopened.openBlend(saved.slice(0)); return { browser: "chromium", beforeRevision: opened.snapshot.revision, revisions: [cameraEdit.snapshot.revision, lightEdit.snapshot.revision, worldEdit.snapshot.revision, outputEdit.snapshot.revision], bad, badRevisionPreserved: afterBad.revision === beforeBad.revision, undoRevision: undo.snapshot.revision, redoRevision: redo.snapshot.revision, restored: { camera: restored.snapshot.cameras[0], light: restored.snapshot.lights[0], world: restored.snapshot.worlds[0], resolution: restored.snapshot.scenes[0].renderSettings?.resolutionPercentage } }; } finally { client.terminate(); reopened.terminate(); } }, Array.from(bytes)); expect(result.browser).toBe("chromium"); expect(result.revisions).toEqual([2, 3, 4, 5]); expect(result.bad).not.toBe("NO_ERROR"); expect(result.badRevisionPreserved).toBe(true); expect(result.undoRevision).toBe(6); expect(result.redoRevision).toBe(7); expect(result.restored.camera.lensMm).toBeCloseTo(42, 5); expect(result.restored.light.energy).toBeCloseTo(375, 5); expect(result.restored.world.exposure).toBeCloseTo(0.3, 5); expect(result.restored.resolution).toBe(80); const evidenceDir = path.join(root, "tests/golden/WBV2-P1424-001/validation-b4-generated-properties"); fs.mkdirSync(evidenceDir, { recursive: true }); fs.writeFileSync(path.join(evidenceDir, `${path.basename(fixturePath, ".blend")}.json`), JSON.stringify({ schemaVersion: 1, task: "WBV2-P1424-001", operation: "B4_GENERATED_PROPERTIES_CHROMIUM", browser: "chromium", fixturePath, fixtureSha256: crypto.createHash("sha256").update(bytes).digest("hex"), candidate: "build_web_main_desktop/b4-world-readback-use-nodes-20261027/candidate.json", status: "PASS", result }, null, 2) + "\n"); });