import { test, expect } from "@playwright/test"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { NativeUIWorkflow, observeNative } from "../fixtures/native-ui-workflow"; test("Font layout and imported native font links survive Main recovery", async ({ page, browser }, testInfo) => { test.setTimeout(90000); const root = path.resolve(".."); const receiptRoot = process.env.WBV2_RECEIPT_DIR ?? testInfo.outputPath("cases"); const directory = path.join(receiptRoot, "font-assets-artifacts"); const fixture = path.join(root, "tests/files/web/nonmesh_scene.blend"); const fontFile = path.join(root, "blender-5.2.0/release/datafiles/bfont.pfb"); const native = process.env.BLENDER_BIN ?? path.join(root, "build_blender_native/bin/blender"); const wasm = path.join(root, "build_web_blender6/bin/web_engine.wasm"); const sha = (bytes: Buffer | string) => crypto.createHash("sha256").update(bytes).digest("hex"); await observeNative(page); const workflow = new NativeUIWorkflow(page, directory); const perform = async (flow: NativeUIWorkflow, saveIntermediate: boolean) => { const current = flow.page; await current.getByLabel("Font character kern", { exact: true }).fill("2"); await current.getByLabel("Font text box width", { exact: true }).fill("4.5"); await current.getByLabel("Font text box height", { exact: true }).fill("2"); await flow.click(current.getByRole("button", { name: "Apply Layout", exact: true })); const links = current.getByLabel("Font regular link", { exact: true }); const original = await links.locator("option").evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).value)); await flow.command(() => current.getByTestId("vfont-file-input").setInputFiles(fontFile)); await expect.poll(() => links.locator("option").count()).toBeGreaterThan(original.length); const imported = (await links.locator("option").evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).value))).find((value) => !original.includes(value)); expect(imported).toBeTruthy(); if (saveIntermediate) await flow.save("before_links"); await links.selectOption(imported!); await flow.click(current.getByRole("button", { name: "Apply Links", exact: true })); return imported!; }; await workflow.measured("load_baseline", async () => { await page.goto("/"); await workflow.load(fixture, "WebFontObject"); await workflow.save("before"); }); let imported = ""; await workflow.measured("layout_import_links", async () => { imported = await perform(workflow, true); await workflow.save("after"); }); await workflow.measured("reject_missing_font", async () => { const links = page.getByLabel("Font regular link", { exact: true }); await links.evaluate((element) => element.append(new Option("Missing font", "vfont:MissingLocalTestFont"))); await links.selectOption("vfont:MissingLocalTestFont"); await workflow.click(page.getByRole("button", { name: "Apply Links", exact: true }), false); await workflow.save("negative"); }); await workflow.measured("undo_redo", async () => { await workflow.click(page.getByRole("button", { name: "撤销", exact: true })); await workflow.save("undo"); await workflow.click(page.getByRole("button", { name: "重做", exact: true })); await workflow.save("redo"); }); await workflow.measured("save_reopen", async () => { await workflow.save("saved"); await workflow.load(path.join(directory, "saved.blend"), "WebFontObject"); await expect(page.getByLabel("Font regular link", { exact: true })).toHaveValue(imported); await workflow.save("reopened"); }); await workflow.measured("independent_repeat", async () => { const context = await browser.newContext({ baseURL: new URL(page.url()).origin, acceptDownloads: true }); try { const repeat = await context.newPage(); await observeNative(repeat); const flow = new NativeUIWorkflow(repeat, directory); await repeat.goto("/"); await flow.load(fixture, "WebFontObject"); await perform(flow, false); await flow.save("second"); } finally { await context.close(); } }); await workflow.measured("native_font_readback", async () => { execFileSync(native, ["--background", "--factory-startup", "--python-exit-code", "1", "--python", path.join(root, "web/tests/fixtures/font-main-snapshots.py"), "--", directory], { timeout: 30000, stdio: "pipe" }); }); const snapshots = JSON.parse(fs.readFileSync(path.join(directory, "native-font-snapshots.json"), "utf8")); const text = snapshots.after.texts.find((item: { object: string }) => item.object === "WebFontObject"); expect(text.characters[0].kern).toBe(2); expect(text.boxes[0]).toMatchObject({ width: 4.5, height: 2 }); expect(snapshots.after.fonts).toHaveLength(snapshots.before.fonts.length + 1); const added = snapshots.after.fonts.find((font: { name: string }) => !snapshots.before.fonts.some((old: { name: string }) => old.name === font.name)); expect(added.packedSha256).toBe(sha(fs.readFileSync(fontFile))); expect(text.links.font).toBe(added.name); expect(snapshots.negative).toEqual(snapshots.after); expect(snapshots.undo).toEqual(snapshots.before_links); expect(snapshots.redo).toEqual(snapshots.after); expect(snapshots.reopened).toEqual(snapshots.saved); expect(snapshots.second).toEqual(snapshots.after); await page.screenshot({ path: path.join(directory, "font-links-after.png") }); const hashes = Object.fromEntries(Object.entries(snapshots).map(([name, value]) => [name, sha(JSON.stringify(value))])); const artifacts = [native, wasm, fixture, fontFile, ...fs.readdirSync(directory).map((name) => path.join(directory, name))].map((file) => ({ path: path.relative(root, file), sha256: sha(fs.readFileSync(file)) })); const units = ["web-command:setFontAdvanced", "web-command:importVFont", "web-command:setFontLinks"]; const cases = [ { kind: "positive", units, observations: { before: hashes.before, after: hashes.after } }, { kind: "negative", units: ["web-command:setFontLinks"], observations: { before: hashes.after, after: hashes.negative } }, { kind: "undo_redo", units: ["web-command:setFontLinks"], observations: { before: hashes.before_links, after: hashes.after, undo: hashes.undo, redo: hashes.redo } }, { kind: "save_reopen", units, observations: { saved: hashes.saved, reopened: hashes.reopened } }, { kind: "determinism", units, observations: { first: hashes.after, second: hashes.second } }, ]; for (const entry of cases) { const caseId = `font-assets-${entry.kind}`; fs.writeFileSync(path.join(receiptRoot, `${caseId}.json`), JSON.stringify({ status: "PASS", runtime: "CHROMIUM_WEBENGINE", caseId, kind: entry.kind, observations: entry.observations, unitIds: entry.units, artifacts }, null, 2)); } });