Files
workinf_Blender_Wasm/web/tests/e2e/external-vfont.spec.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

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,
});
});