import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const goldenRoot = path.join(root, "tests/golden/M11-04"); const manifest = JSON.parse(fs.readFileSync(path.join(goldenRoot, "manifest.json"), "utf8")); const blend = fs.readFileSync(path.join(root, manifest.source.fixture)); const referencePng = fs.readFileSync(path.join(goldenRoot, manifest.reference.file)); async function decodeAndCompare(page: import("@playwright/test").Page, reference: Buffer, actual: Buffer) { return page.evaluate(async ({ referenceBytes, actualBytes, thresholds }) => { const { compareRenderImages } = await import("/src/three-adapter/render-image-comparison.ts"); const decode = async (bytes: number[]) => { const bitmap = await createImageBitmap(new Blob([Uint8Array.from(bytes)], { type: "image/png" }), { colorSpaceConversion: "none", premultiplyAlpha: "none", }); const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) throw new Error("2D decode context is unavailable"); context.drawImage(bitmap, 0, 0); const pixels = new Uint8Array(context.getImageData(0, 0, bitmap.width, bitmap.height).data); const result = { width: bitmap.width, height: bitmap.height, pixels }; bitmap.close(); return result; }; const [referenceFrame, actualFrame] = await Promise.all([decode(referenceBytes), decode(actualBytes)]); if (referenceFrame.width !== actualFrame.width || referenceFrame.height !== actualFrame.height) { throw new Error("render frame dimensions differ"); } return compareRenderImages( referenceFrame.pixels, actualFrame.pixels, referenceFrame.width, referenceFrame.height, thresholds, ); }, { referenceBytes: Array.from(reference), actualBytes: Array.from(actual), thresholds: manifest.thresholds }); } for (const offscreen of [false, true]) { test(`M11-04 ${offscreen ? "Offscreen" : "main-thread"} production render matches the Blender reference metrics`, async ({ page }) => { test.setTimeout(60_000); await page.goto("/"); await page.evaluate(async ({ input, useOffscreen }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts"); const { OffscreenViewportRenderer } = await import("/src/three-adapter/offscreen-viewport.ts"); document.body.replaceChildren(); document.body.style.margin = "0"; const canvas = document.createElement("canvas"); canvas.id = "m11-reference-canvas"; canvas.style.width = "256px"; canvas.style.height = "256px"; document.body.append(canvas); const client = new WebEngineClient({ timeoutMs: 30_000 }); const opened = await client.openBlend(Uint8Array.from(input).buffer); opened.snapshot.activeObjectId = null; const renderer = useOffscreen ? new OffscreenViewportRenderer(canvas) : new ViewportRenderer(canvas); renderer.setSnapshot(opened.snapshot, opened.geometryBuffers, opened.nonMeshGeometryBuffers ?? []); for (let attempt = 0; attempt < 300; attempt++) { if (!useOffscreen || (Number(canvas.dataset.rendererPixels) > 0 && canvas.dataset.renderBudgetStatus === "ready")) break; await new Promise((resolve) => setTimeout(resolve, 10)); } await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); (globalThis as typeof globalThis & { __m11Reference?: { renderer: { dispose(): void }; client: { terminate(): void } } }).__m11Reference = { renderer, client }; }, { input: Array.from(blend), useOffscreen: offscreen }); const canvas = page.locator("#m11-reference-canvas"); await expect(canvas).toHaveAttribute("data-render-budget-status", "ready"); const actualPng = await canvas.screenshot({ animations: "disabled" }); const report = await decodeAndCompare(page, referencePng, actualPng); console.log(`m11-render-reference-${offscreen ? "offscreen" : "main"}`, JSON.stringify(report)); expect(report.status).toBe("READY"); expect(report.errorCode).toBeNull(); expect(report.checks.every((item) => item.passed)).toBe(true); await page.evaluate(() => { const state = (globalThis as typeof globalThis & { __m11Reference?: { renderer: { dispose(): void }; client: { terminate(): void } } }).__m11Reference; state?.renderer.dispose(); state?.client.terminate(); delete (globalThis as typeof globalThis & { __m11Reference?: unknown }).__m11Reference; }); }); } test("M11-04 rejects a non-empty frame with the wrong composition", async ({ page }) => { await page.goto("/"); const wrong = await page.evaluate(async () => { const canvas = new OffscreenCanvas(256, 256); const context = canvas.getContext("2d")!; context.fillStyle = "rgb(58,58,58)"; context.fillRect(0, 0, 256, 256); context.fillStyle = "rgb(255,0,0)"; context.fillRect(0, 0, 32, 32); return Array.from(new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer())); }); const report = await decodeAndCompare(page, referencePng, Buffer.from(wrong)); expect(report.status).toBe("BLOCKED"); expect(report.errorCode).toBe("RENDER_REFERENCE_MISMATCH"); expect(report.checks.some((item) => !item.passed)).toBe(true); });