import { expect, test } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; const fontPath = path.resolve(import.meta.dirname, "../../../blender-5.2.0/release/datafiles/bfont.pfb"); const nonMeshBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/nonmesh_scene.blend"); test("M9-01 validates a real external font in Chromium before any storage or Main call", async ({ page }) => { await page.goto("/"); const bytes = fs.readFileSync(fontPath); const sha256 = crypto.createHash("sha256").update(bytes).digest("hex"); const result = await page.evaluate(({ bytes, sha256 }) => new Promise((resolve, reject) => { const worker = new Worker("/src/workers/external-vfont-test.worker.ts", { type: "module" }); worker.onmessage = (event) => { worker.terminate(); event.data.error ? reject(new Error(event.data.error)) : resolve(event.data); }; worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; worker.postMessage({ data, sha256 }, [data]); }), { bytes: new Uint8Array(bytes), sha256 }); expect(result.metadata).toMatchObject({ schemaVersion: 1, sourcePath: "//fonts/browser-bfont.pfb", format: "PFB", byteLength: 25181, sha256 }); expect(result.copied).toBe(true); expect(result.spoofCode).toBe("NON_MESH_BINARY_INVALID"); expect(result.stage).toBe("VALIDATED_BEFORE_STORAGE_OR_MAIN"); }); test("M9-02 commits the verified font to OPFS before creating a packed Main VFont", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const fontBytes = fs.readFileSync(fontPath); const blendBytes = fs.readFileSync(nonMeshBlend); const sha256 = crypto.createHash("sha256").update(fontBytes).digest("hex"); const result = await page.evaluate(async ({ fontBytes, blendBytes, sha256 }) => { const [{ StorageClient }, { WebEngineClient }, { importExternalVFontIntoMain }] = await Promise.all([ import("/src/storage/StorageClient.ts"), import("/src/engine-client/WebEngineClient.ts"), import("/src/fonts/external-vfont-import.ts"), ]); const projectId = "m9-vfont-browser"; const storage = new StorageClient(); const engine = new WebEngineClient({ timeoutMs: 60_000 }); const events: string[] = []; await engine.init(); const blend = blendBytes.buffer.slice(blendBytes.byteOffset, blendBytes.byteOffset + blendBytes.byteLength) as ArrayBuffer; const opened = await engine.openBlend(blend); const storagePort = { putAsset: async (...args: Parameters) => { events.push("storage:start"); const stored = await storage.putAsset(...args); events.push("storage:committed"); return stored; }, }; const enginePort = { applyCommand: async (...args: Parameters) => { events.push("main:start"); const applied = await engine.applyCommand(...args); events.push("main:committed"); return applied; }, }; const data = fontBytes.buffer.slice(fontBytes.byteOffset, fontBytes.byteOffset + fontBytes.byteLength) as ArrayBuffer; const imported = await importExternalVFontIntoMain({ projectId, request: { sourcePath: "//fonts/m9-browser-bfont.pfb", mimeType: "application/x-font-type1", byteLength: data.byteLength, sha256, data }, storage: storagePort, engine: enginePort, }); const restored = await storage.readAsset(projectId, sha256); let failedMainCalls = 0; let failureCode = ""; try { await importExternalVFontIntoMain({ projectId, request: { sourcePath: "//fonts/m9-storage-failure.pfb", mimeType: "application/x-font-type1", byteLength: restored.data.byteLength, sha256, data: restored.data.slice(0) }, storage: { putAsset: async () => { throw new Error("STORAGE_TRANSACTION: injected before asset commit"); } }, engine: { applyCommand: async () => { failedMainCalls += 1; throw new Error("unexpected Main call"); } }, }); } catch (error) { failureCode = error instanceof Error ? error.message : String(error); } const listed = await storage.listAssets(projectId); storage.terminate(); engine.terminate(); return { beforeCount: opened.snapshot.vfonts?.length ?? 0, afterCount: imported.snapshot.vfonts?.length ?? 0, events, asset: imported.asset, vfont: imported.vfont, restoredHash: restored.asset.sha256, restoredBytes: restored.data.byteLength, listedCount: listed.assets.length, failedMainCalls, failureCode, }; }, { fontBytes: new Uint8Array(fontBytes), blendBytes: new Uint8Array(blendBytes), sha256 }); expect(result.events).toEqual(["storage:start", "storage:committed", "main:start", "main:committed"]); expect(result.afterCount).toBe(result.beforeCount + 1); expect(result.asset).toMatchObject({ projectId: "m9-vfont-browser", sha256, bytes: fontBytes.byteLength, persisted: true }); expect(result.asset.path).toBe(`projects/m9-vfont-browser/assets/sha256/${sha256.slice(0, 2)}/${sha256}`); expect(result.vfont).toMatchObject({ name: "m9-browser-bfont", sourcePath: "//fonts/m9-browser-bfont.pfb", builtin: false, packed: true }); expect(result.restoredHash).toBe(sha256); expect(result.restoredBytes).toBe(fontBytes.byteLength); expect(result.listedCount).toBe(1); expect(result.failedMainCalls).toBe(0); expect(result.failureCode).toContain("STORAGE_TRANSACTION"); }); test("M9-03 replaces, undoes, saves and reopens a packed font with missing-asset closure", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const fontBytes = fs.readFileSync(fontPath); const blendBytes = fs.readFileSync(nonMeshBlend); const sha256 = crypto.createHash("sha256").update(fontBytes).digest("hex"); const result = await page.evaluate(async ({ fontBytes, blendBytes, sha256 }) => { const [{ StorageClient }, { WebEngineClient }, fontImport] = await Promise.all([ import("/src/storage/StorageClient.ts"), import("/src/engine-client/WebEngineClient.ts"), import("/src/fonts/external-vfont-import.ts"), ]); const projectId = "m9-vfont-roundtrip"; const storage = new StorageClient(); const engine = new WebEngineClient({ timeoutMs: 60_000 }); await engine.init(); const opened = await engine.openBlend(blendBytes.buffer.slice(blendBytes.byteOffset, blendBytes.byteOffset + blendBytes.byteLength)); const fontData = opened.snapshot.nonMeshData?.find((candidate) => candidate.type === "FONT" && candidate.fontLinks); if (!fontData?.fontLinks) throw new Error("font fixture has no style links"); const originalLinks = { ...fontData.fontLinks }; const imported = await fontImport.importExternalVFontIntoMain({ projectId, request: { sourcePath: "//fonts/m9-roundtrip-bfont.pfb", mimeType: "application/x-font-type1", byteLength: fontBytes.byteLength, sha256, data: fontBytes.buffer.slice(fontBytes.byteOffset, fontBytes.byteOffset + fontBytes.byteLength), }, storage, engine, }); const replaced = await fontImport.replaceExternalVFontStyleInMain({ projectId, sha256, dataId: fontData.id, vfontId: imported.vfont.id, style: "regular", snapshot: imported.snapshot, storage, engine, }); const undone = await engine.applyCommand({ type: "undo" }); const redone = await engine.applyCommand({ type: "redo" }); const undoLinks = undone.snapshot.nonMeshData?.find((candidate) => candidate.id === fontData.id)?.fontLinks; const redoLinks = redone.snapshot.nonMeshData?.find((candidate) => candidate.id === fontData.id)?.fontLinks; const savedBlend = await engine.saveBlend(); await storage.saveProject(projectId, redone.snapshot.revision, savedBlend); const root = await navigator.storage.getDirectory(); let assetDirectory = root; for (const segment of ["projects", projectId, "assets", "sha256", sha256.slice(0, 2)]) { assetDirectory = await assetDirectory.getDirectoryHandle(segment); } await assetDirectory.removeEntry(sha256); let missingCode = ""; let missingMainCalls = 0; try { await fontImport.replaceExternalVFontStyleInMain({ projectId, sha256, dataId: fontData.id, vfontId: imported.vfont.id, style: "bold", snapshot: redone.snapshot, storage, engine: { applyCommand: async () => { missingMainCalls += 1; throw new Error("unexpected Main call"); } }, }); } catch (error) { missingCode = (error as { code?: string }).code ?? ""; } engine.terminate(); const persisted = await storage.readProject(projectId); storage.terminate(); const reopenedEngine = new WebEngineClient({ timeoutMs: 60_000 }); await reopenedEngine.init(); const reopened = await reopenedEngine.openBlend(persisted.buffer); reopenedEngine.terminate(); const reopenedData = reopened.snapshot.nonMeshData?.find((candidate) => candidate.id === fontData.id); const reopenedVFont = reopened.snapshot.vfonts?.find((candidate) => candidate.id === imported.vfont.id); return { originalLinks, replacementLinks: replaced.links, undoLinks, redoLinks, importedVFont: imported.vfont, reopenedLinks: reopenedData?.fontLinks, reopenedVFont, missingCode, missingMainCalls, }; }, { fontBytes: new Uint8Array(fontBytes), blendBytes: new Uint8Array(blendBytes), sha256 }); expect(result.replacementLinks.regular).toBe(result.importedVFont.id); expect(result.undoLinks).toEqual(result.originalLinks); expect(result.redoLinks).toEqual(result.replacementLinks); expect(result.missingCode).toBe("NON_MESH_RESOURCE_MISSING"); expect(result.missingMainCalls).toBe(0); expect(result.reopenedLinks).toEqual(result.replacementLinks); expect(result.reopenedVFont).toMatchObject({ id: result.importedVFont.id, sourcePath: "//fonts/m9-roundtrip-bfont.pfb", builtin: false, packed: true, packedByteLength: fontBytes.byteLength, sha256, }); });