diff --git a/internal/check-poster-card-fits.mjs b/internal/check-poster-card-fits.mjs index 6f8713b34..a18dc629d 100644 --- a/internal/check-poster-card-fits.mjs +++ b/internal/check-poster-card-fits.mjs @@ -32,6 +32,21 @@ const LEAF = process.argv[2] || DEFAULT_LEAF const WIDTHS = (process.env.CARD_FIT_WIDTH || '390,768,1024,1440,1920') .split(',').map((n) => Number(n.trim())).filter(Boolean) +// ★ AND SWEEP LOCALES, for exactly the same reason the widths are swept. The Driver's requirement +// was "full words visible AND support localization"; this gate only ever ran in English, which is +// the shortest of the eleven. MEASURED 2026-09-24 at 1440px, the three print labels: +// en Prayer List 76px · Card Deck 72px · Poster Map 79px +// ro Listă de rugăciune 122px · Pachet de cartonașe 133px · Harta afișului 92px (+75%) +// zh 禱告名單 56px · 禱告卡組 56px · 海报地图 56px +// Romanian is the widest RENDERED, which is the property that matters — not the longest string. +// Chinese is only 4 characters yet each glyph is near double-width, so a character-count heuristic +// would have picked the wrong worst case. The gate asserts scrollWidth > clientWidth, which is +// pixels, so the sweep has to be pixels too. +// en is always included as the control; ro and zh are the measured extremes. Override with +// CARD_FIT_LANG to check a specific locale. +const LANGS = (process.env.CARD_FIT_LANG || 'en,ro,zh') + .split(',').map((s) => s.trim()).filter(Boolean) + let reachable = false try { const r = await fetch(LEAF, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ } if (!reachable) { @@ -44,10 +59,15 @@ const WALK = `(root, out) => { out = out || []; const rec = (r) => { for (const const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] }) -async function measure(width) { +async function measure(width, lang) { const mobile = width < 500 const page = await browser.newPage({ viewport: { width, height: 1000 }, isMobile: mobile, hasTouch: mobile }) - await page.goto(LEAF, { waitUntil: 'load', timeout: 45000 }) + // ?lang= is how the app selects a locale (library/i18n/index.js:83). Appended rather than + // replaced so a caller can still pass a leaf URL that already carries query params. + const url = lang && lang !== 'en' + ? LEAF + (LEAF.includes('?') ? '&' : '?') + 'lang=' + encodeURIComponent(lang) + : LEAF + await page.goto(url, { waitUntil: 'load', timeout: 45000 }) await page.waitForTimeout(10000) // ★ HOVER IS INVISIBLE TO A STATIC PROBE, which is exactly how the blue outline stayed @@ -164,12 +184,12 @@ async function measure(width) { controls: ctrls.length, tiny: tiny.length } })()`) await page.close() - return { width, hover, ...out } + return { width, lang, hover, ...out } } const results = [] try { - for (const w of WIDTHS) results.push(await measure(w)) + for (const lang of LANGS) for (const w of WIDTHS) results.push(await measure(w, lang)) } finally { await browser.close() } @@ -177,7 +197,7 @@ try { console.log('POSTER-CARD-FITS') console.log(` leaf : ${LEAF}`) for (const r of results) { - console.log(` ${String(r.width).padStart(4)}px cards ${String(r.cards).padStart(2)}` + console.log(` ${(r.lang || 'en').padEnd(2)} ${String(r.width).padStart(4)}px cards ${String(r.cards).padStart(2)}` + ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}` + ` · neighbour ${r.intrusions}` + ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}` @@ -193,7 +213,7 @@ for (const r of results) { const fails = [] for (const r of results) { - const at = `${r.width}px` + const at = `${r.lang || 'en'} ${r.width}px` // PRECONDITION: no cards means nothing was tested at this width — not a pass. if (!r.cards) { fails.push(`${at}: no poster cards rendered — wrong url, or the wall did not mount`); continue } if (!r.rows) { fails.push(`${at}: ${r.cards} card(s) but none carry a .pfs print-options row`); continue } @@ -274,6 +294,9 @@ if (fails.length) { for (const f of fails) console.error(' - ' + f) process.exit(1) } -console.log(`\nPOSTER-CARD-FITS: PASS — at ${results.length} width(s) (${WIDTHS.join(', ')}px) every ` +// Report widths x LOCALES, not the product. "15 width(s)" misdescribes 5 widths in 3 languages, +// and a summary that overstates its own coverage is how a green line stops meaning anything. +console.log(`\nPOSTER-CARD-FITS: PASS — ${WIDTHS.length} width(s) (${WIDTHS.join(', ')}px) ` + + `x ${LANGS.length} locale(s) (${LANGS.join(', ')}) = ${results.length} combination(s). Every ` + 'print-options row sits inside its own card and below its preview, with every control still ' - + 'big enough to press.') + + 'big enough to press and its label fully readable in every locale checked.')