Advance M8-M11 parity workflows
This commit is contained in:
218
web/tests/e2e/external-vfont.spec.ts
Normal file
218
web/tests/e2e/external-vfont.spec.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user