import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { expect, test } from "@playwright/test"; const root = path.resolve(import.meta.dirname, "../../.."); const mainReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8")); const reportPath = path.join(root, "tests/golden/M12-06D/web-loss-report.json"); const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1"); const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex"); test("writes a machine GLB loss report for every persisted Main fixture", async ({ page }) => { await page.goto("/"); const fixtures = mainReport.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string } }) => ({ id: fixture.id, bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))), sourceBlendSha256: fixture.blend.sha256, })); const generated = await page.evaluate(async (input) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const digest = async (bytes: ArrayBuffer): Promise => { const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); return Array.from(hash, (value) => value.toString(16).padStart(2, "0")).join(""); }; const reports = []; for (const fixture of input) { const client = new WebEngineClient({ timeoutMs: 30_000 }); try { const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer); const assets = []; for (const image of opened.snapshot.images) { const asset = await client.requestAsset(image.assetId); if (asset.status === "packed" && asset.data && asset.mimeType) assets.push({ assetId: image.assetId, mimeType: asset.mimeType, data: asset.data }); } const worker = new Worker("/src/workers/glb-loss-report-test.worker.ts", { type: "module" }); const result = await new Promise<{ report: any; output: ArrayBuffer | null }>((resolve, reject) => { worker.onmessage = (event: MessageEvent<{ ok: boolean; report?: any; output?: ArrayBuffer | null; error?: string }>) => { worker.terminate(); if (!event.data.ok || !event.data.report) reject(new Error(event.data.error ?? "GLB loss report worker failed")); else resolve({ report: event.data.report, output: event.data.output ?? null }); }; worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; const transfer: Transferable[] = []; for (const geometry of opened.geometryBuffers) for (const value of Object.values(geometry)) if (value instanceof ArrayBuffer) transfer.push(value); for (const asset of assets) transfer.push(asset.data); for (const chunk of opened.nonMeshGeometryBuffers ?? []) for (const value of Object.values(chunk)) if (value instanceof ArrayBuffer) transfer.push(value); worker.postMessage({ snapshot: opened.snapshot, geometryBuffers: opened.geometryBuffers, assetBuffers: assets, nonMeshGeometryBuffers: opened.nonMeshGeometryBuffers ?? [] }, transfer); }); reports.push({ fixtureId: fixture.id, sourceBlendSha256: fixture.sourceBlendSha256, lossReport: result.report, output: result.output ? { byteLength: result.output.byteLength, sha256: await digest(result.output), bytes: Array.from(new Uint8Array(result.output)) } : null, }); } finally { client.terminate(); } } return reports; }, fixtures); const report = { schemaVersion: 1, task: "M12-06D", operation: "WEB_GLB_EXPORT_LOSS_REPORT", fixtureCount: generated.length, fixtures: generated, nextTask: "M12-06E", }; if (process.env.UPDATE_GLB_LOSS_REPORT === "1") { const outputRoot = path.join(root, "tests/files/web/m12_glb_web_v1"); fs.mkdirSync(outputRoot, { recursive: true }); for (const fixture of generated) { if (fixture.output?.bytes) fs.writeFileSync(path.join(outputRoot, `${fixture.fixtureId}.glb`), Buffer.from(fixture.output.bytes)); } } for (const fixture of report.fixtures) if (fixture.output?.bytes) delete fixture.output.bytes; if (process.env.UPDATE_GLB_LOSS_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } const expected = JSON.parse(fs.readFileSync(reportPath, "utf8")); expect(report).toEqual(expected); expect(report.fixtureCount).toBe(5); for (const fixture of report.fixtures) { expect(fixture.lossReport.schemaVersion).toBe(1); expect(fixture.lossReport.operation).toBe("GLB_EXPORT_LOSS_REPORT"); if (fixture.fixtureId === "mesh") { expect(fixture.lossReport.canExport).toBe(false); expect(fixture.lossReport.errorCount).toBe(1); expect(fixture.lossReport.losses.map((loss: { code: string }) => loss.code)).toEqual(["LINKED_MATERIAL_INPUT_UNEVALUATED", "SHADER_GRAPH_UNMAPPABLE"]); expect(fixture.output).toBeNull(); } else { expect(fixture.lossReport.canExport).toBe(true); expect(fixture.lossReport.errorCount).toBe(0); expect(fixture.output?.byteLength).toBeGreaterThan(128); expect(fixture.output?.sha256).toMatch(/^[0-9a-f]{64}$/); } expect(fixture.lossReport.losses).toEqual([...fixture.lossReport.losses].sort((left, right) => left.code.localeCompare(right.code) || left.severity.localeCompare(right.severity) || (left.id ?? "").localeCompare(right.id ?? "") || left.message.localeCompare(right.message))); } });