219 lines
10 KiB
TypeScript
219 lines
10 KiB
TypeScript
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<any>((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<StorageClient["putAsset"]>) => {
|
|
events.push("storage:start");
|
|
const stored = await storage.putAsset(...args);
|
|
events.push("storage:committed");
|
|
return stored;
|
|
},
|
|
};
|
|
const enginePort = {
|
|
applyCommand: async (...args: Parameters<WebEngineClient["applyCommand"]>) => {
|
|
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,
|
|
});
|
|
});
|