From 7edc5a43dab152dfd5cb0d609a9e882d7fd4b446 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Fri, 28 Aug 2026 14:47:21 -0700 Subject: [PATCH] font-match: a browser module without its binary is the same as no browser CI resolves playwright but has no downloaded chromium; launch threw instead of falling back to the catalog ranking, and every spec gate downstream failed. Launch failures now return the no-browser path (and the browser test skips instead of asserting). AI-assisted (Claude Code). --- skill/scripts/font-match.mjs | 10 +++++++--- tests/font-match.test.mjs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/skill/scripts/font-match.mjs b/skill/scripts/font-match.mjs index ecb421bda..f69a000ac 100644 --- a/skill/scripts/font-match.mjs +++ b/skill/scripts/font-match.mjs @@ -222,6 +222,11 @@ function parseCandidates(s) { export async function renderCandidates(candidates, text, targetCapPx, { transform = 'none' } = {}) { const b = await loadBrowser(); if (!b) return null; + // A resolvable module whose browser binary is absent (CI, a fresh install + // without npx playwright install) throws at launch; that is the same + // situation as no module, and the catalog fallback owns it. + let browser; + try { browser = b.kind === 'playwright' ? await b.mod.chromium.launch() : await b.mod.launch({ headless: true }); } catch { return null; } // One stylesheet per family+weight: a combined request 400s when any one // family lacks the requested axis (Anton has no wght range), and a static // family answers only for the weights it ships. @@ -230,7 +235,6 @@ export async function renderCandidates(candidates, text, targetCapPx, { transfor const results = []; const size0 = Math.max(12, Math.round(targetCapPx * 1.4)); if (b.kind === 'playwright') { - const browser = await b.mod.chromium.launch(); const page = await browser.newPage({ viewport: { width: 1600, height: 400 }, deviceScaleFactor: 1 }); await page.setContent(html, { waitUntil: 'load' }); await page.waitForTimeout(800); @@ -264,7 +268,6 @@ export async function renderCandidates(candidates, text, targetCapPx, { transfor } await browser.close(); } else { - const browser = await b.mod.launch({ headless: true }); const page = await browser.newPage(); await page.setViewport({ width: 1600, height: 400 }); await page.setContent(html, { waitUntil: 'load' }); @@ -309,7 +312,8 @@ export async function renderProofSheet(compCrop, top, text, capPx, transform = ' const links = top.map((c) => ``).join(''); const rowsHtml = top.map((c) => `
${c.family} ${c.weight} ยท ${c.fontSizePx}px
${text}
`).join(''); const html = `${links}
COMP
${rowsHtml}`; - const browser = await b.mod.chromium.launch(); + let browser; + try { browser = await b.mod.chromium.launch(); } catch { return null; } const page = await browser.newPage({ viewport: { width: Math.min(1600, Math.max(600, compCrop.width + 24)), height: 200 } }); await page.setContent(html, { waitUntil: 'load' }); try { await page.evaluate(async () => { await document.fonts.ready; }); } catch { /* ignore */ } diff --git a/tests/font-match.test.mjs b/tests/font-match.test.mjs index 70a1ffb45..833a10401 100644 --- a/tests/font-match.test.mjs +++ b/tests/font-match.test.mjs @@ -162,7 +162,7 @@ describe('font-match', () => { it('renders and ranks candidates against a League Gothic sample (browser)', { skip: !hasPlaywright && 'playwright not resolvable' }, async () => { const results = await renderCandidates([{ family: 'League Gothic', weight: 400 }, { family: 'Inter', weight: 400 }], 'The manuals stop.', 48); - assert.ok(results, 'no browser'); + if (!results) return; // module resolves but no browser binary (CI): the catalog fallback owns it const ok = results.filter((r) => r.loaded && r.fp); if (ok.length < 2) return; // offline: Google Fonts unreachable const index = loadFontIndex();