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 evidenceRoot = path.join(root, "tests/golden/WBV2-P15-002"); const fixturePath = path.join(root, "tests/files/web/basic_scene.blend"); const fixture = fs.readFileSync(fixturePath); const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex"); const expected = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-02/lighting-field-roundtrip.json"), "utf8")); function close(actual: unknown, expectedValue: unknown, tolerance = 1e-5): void { if (typeof expectedValue === "number") { expect(actual as number).toBeCloseTo(expectedValue, Math.max(0, Math.ceil(-Math.log10(tolerance)))); return; } if (Array.isArray(expectedValue)) { expect(actual).toHaveLength(expectedValue.length); expectedValue.forEach((value, index) => close((actual as unknown[])[index], value, tolerance)); return; } if (expectedValue && typeof expectedValue === "object") { for (const [key, value] of Object.entries(expectedValue)) close((actual as Record)[key], value, tolerance); return; } expect(actual).toEqual(expectedValue); } test("WBV2-P15-002 binds camera, light, and world fields to compiled WASM Main", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const result = await page.evaluate(async (bytes) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const client = new WebEngineClient({ timeoutMs: 30_000 }); const reopened = new WebEngineClient({ timeoutMs: 30_000 }); const digest = async (value: ArrayBuffer): Promise => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); }; try { const opened = await client.openBlend(Uint8Array.from(bytes).buffer); const camera = opened.snapshot.cameras.find((item) => item.id === "camera:Camera.001")!; const light = opened.snapshot.lights.find((item) => item.id === "light:Area")!; const world = opened.snapshot.worlds.find((item) => item.id === "world:World")!; const beforeRevision = opened.snapshot.revision; const cameraEdit = await client.applyCommand({ type: "setCameraProperties", dataId: camera.id, properties: { lensMm: 35, sensorWidthMm: 32, sensorHeightMm: 18, sensorFit: 2, shift: [0.1, -0.2], near: 0.2, far: 500, orthoScale: 8, depthOfField: { enabled: true, focusDistance: 4.5, apertureFStop: 1.8, apertureBlades: 7, apertureRotation: 0.25, apertureRatio: 1.2 } } } as any); const cameraUndo = await client.applyCommand({ type: "undo" }); const cameraRedo = await client.applyCommand({ type: "redo" }); const lightEdit = await client.applyCommand({ type: "setLightProperties", dataId: light.id, properties: { color: [0.25, 0.5, 0.75], energy: 400, exposure: 1, temperature: 5000, useTemperature: true, castsShadow: false, radius: 0.3, spotAngle: 1.1, spotBlend: 0.25, areaSize: 3, areaSizeY: 2, areaSpread: 2.4, sunAngle: 0.1 } } as any); const lightUndo = await client.applyCommand({ type: "undo" }); const lightRedo = await client.applyCommand({ type: "redo" }); const worldEdit = await client.applyCommand({ type: "setWorldProperties", dataId: world.id, properties: { color: [0.1, 0.2, 0.3], exposure: 0.5, mist: { enabled: true, type: "LINEAR", start: 2, depth: 50, intensity: 0.2, height: 3 } } } as any); const worldUndo = await client.applyCommand({ type: "undo" }); const worldRedo = await client.applyCommand({ type: "redo" }); const beforeMalformed = (await client.snapshot()).snapshot; const malformed = await client.applyCommand({ type: "setCameraProperties", dataId: camera.id, properties: { near: 10, far: 1 } } as any).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message })); const afterMalformed = (await client.snapshot()).snapshot; const saved = await client.saveBlend(); const reopenedResult = await reopened.openBlend(saved); return { browser: "chromium", sourceSha256: await digest(Uint8Array.from(bytes).buffer), beforeRevision, revisions: [beforeRevision, cameraEdit.snapshot.revision, cameraUndo.snapshot.revision, cameraRedo.snapshot.revision, lightEdit.snapshot.revision, lightUndo.snapshot.revision, lightRedo.snapshot.revision, worldEdit.snapshot.revision, worldUndo.snapshot.revision, worldRedo.snapshot.revision], malformed, malformedRevisionPreserved: afterMalformed.revision === beforeMalformed.revision, camera: reopenedResult.snapshot.cameras.find((item) => item.id === camera.id), light: reopenedResult.snapshot.lights.find((item) => item.id === light.id), world: reopenedResult.snapshot.worlds.find((item) => item.id === world.id), reopenedRevision: reopenedResult.snapshot.revision, savedSha256: await digest(saved) }; } finally { client.terminate(); reopened.terminate(); } }, Array.from(fixture)); fs.mkdirSync(evidenceRoot, { recursive: true }); const output = process.env.WBV2_LIGHTING_REPORT ?? "lighting-fields-production.json"; fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P15-002", operation: "LIGHTING_FIELDS_ENGINE_CHROMIUM", queueMutation: false, ...result }, null, 2)}\n`); expect(result.browser).toBe("chromium"); expect(result.sourceSha256).toBe(fixtureSha256); expect(result.revisions).toEqual(result.revisions.map((_, index) => result.beforeRevision + index)); expect(result.malformed.code).not.toBe("NO_ERROR"); expect(result.malformedRevisionPreserved).toBe(true); close(result.camera, Object.fromEntries(Object.entries(expected.camera).filter(([key]) => !key.startsWith("viewport")))); close(result.light, Object.fromEntries(Object.entries(expected.light).filter(([key]) => !key.startsWith("viewport")))); close(result.world, expected.world); });