diff --git a/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts b/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts index b9cccf486a..186abb3e19 100644 --- a/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts +++ b/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts @@ -16,11 +16,17 @@ * so they are hermetic. */ -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { EMBEDDED_FONT_DATA } from "./fontData.generated.js"; +import { _clearGoogleFontCssCacheForTests } from "./deterministicFonts.js"; + +// The Google Fonts CSS cache is process-lifetime and keyed by URL; several +// tests here request the same family, so a leftover entry from an earlier +// test would silently skip this test's own fetchImpl. +beforeEach(() => _clearGoogleFontCssCacheForTests()); let cacheDir: string; let prevCacheEnv: string | undefined; diff --git a/packages/producer/src/services/deterministicFonts-failClosed.test.ts b/packages/producer/src/services/deterministicFonts-failClosed.test.ts index b903b50f71..ed98684ccc 100644 --- a/packages/producer/src/services/deterministicFonts-failClosed.test.ts +++ b/packages/producer/src/services/deterministicFonts-failClosed.test.ts @@ -14,8 +14,9 @@ * The tests inject `fetchImpl` so no real network call happens. */ -import { describe, expect, it } from "bun:test"; +import { beforeEach, describe, expect, it } from "bun:test"; import { + _clearGoogleFontCssCacheForTests, FONT_FETCH_FAILED, FONT_FETCH_UNAVAILABLE, FontFetchError, @@ -23,6 +24,11 @@ import { injectDeterministicFontFaces, } from "./deterministicFonts.js"; +// The Google Fonts CSS cache is process-lifetime and keyed by URL; several +// tests here request the same unresolved-font URL, so a leftover entry from +// an earlier test would silently skip this test's own fetchImpl. +beforeEach(() => _clearGoogleFontCssCacheForTests()); + // HTML that requests a font NOT in FONT_ALIASES, so the resolver falls // through to the Google Fonts fetch path. (Bundled fonts like Inter // bypass fetch entirely.) diff --git a/packages/producer/src/services/deterministicFonts.test.ts b/packages/producer/src/services/deterministicFonts.test.ts index f76fee312b..ebf6290f3c 100644 --- a/packages/producer/src/services/deterministicFonts.test.ts +++ b/packages/producer/src/services/deterministicFonts.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, it } from "bun:test"; +import { beforeEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { FONT_ALIASES, FONT_ALIAS_KEYS, + _clearGoogleFontCssCacheForTests, injectDeterministicFontFaces, } from "./deterministicFonts.js"; @@ -45,6 +49,87 @@ describe("existing font-face recognition", () => { }); }); +describe("Google Fonts CSS request caching", () => { + beforeEach(() => _clearGoogleFontCssCacheForTests()); + + it("reuses one CSS lookup across separate compiles requesting the same family", async () => { + // Same character repertoire (just reordered) so both compute the same + // Google Fonts `text=` subset and thus the same request URL — isolates + // the cache from font-text extraction, covered elsewhere. + const htmlA = `
Hello
`; + const htmlB = `olleH
`; + let requests = 0; + const fetchImpl = Object.assign( + async () => { + requests += 1; + return new Response("", { status: 404 }); + }, + { preconnect: fetch.preconnect }, + ); + await injectDeterministicFontFaces(htmlA, { fetchImpl, allowSystemFontCapture: false }); + await injectDeterministicFontFaces(htmlB, { fetchImpl, allowSystemFontCapture: false }); + expect(requests).toBe(1); + }); + + it("does not fail a concurrent caller when another caller sharing the lookup aborts", async () => { + const prevCacheEnv = process.env.HYPERFRAMES_FONT_CACHE_DIR; + const cacheDir = mkdtempSync(join(tmpdir(), "hf-font-abort-isolation-")); + process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir; + try { + const htmlA = `Hello
`; + const htmlB = `olleH
`; + const woff2Url = "https://fonts.gstatic.com/s/abortisolationfont/v1/font.woff2"; + const cssBody = + `@font-face { font-style: normal; font-weight: 400; ` + + `src: url(${woff2Url}) format('woff2'); }`; + let resolveCss: (() => void) | undefined; + // Mirrors real fetch(): the CSS request rejects if its own `init.signal` + // aborts, so this proves the shared lookup isn't wired to any one + // caller's signal. The woff2 request always succeeds immediately — + // only the shared CSS lookup is under test here. + const fetchImpl = Object.assign( + async (url: string, init?: RequestInit) => { + if (!url.includes("css2")) return new Response(new Uint8Array([0, 1, 2, 3])); + return new Promise