diff --git a/internal/check-deck-click.mjs b/internal/check-deck-click.mjs index b9d3a80a6..05c9dc235 100644 --- a/internal/check-deck-click.mjs +++ b/internal/check-deck-click.mjs @@ -14,12 +14,31 @@ // // Usage: node internal/check-deck-click.mjs [stockId] [expectedPage] import { chromium } from 'playwright' +import { readFileSync, existsSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' const URL_ = process.argv[2] if (!URL_) { console.error('usage: check-deck-click.mjs [stock] [expectedPage]'); process.exit(2) } const STOCK = process.argv[3] || '' const EXPECT_PAGE = process.argv[4] || (STOCK === 'pokerLandscape' ? '3.5in 2.5in' : '2.5in 3.5in') +// ★ THE LABEL COMES FROM THE CATALOG FOR THIS PAGE'S LOCALE, never a hardcoded +// English string. This matched /card deck/i, so it could only ever run in English: +// pointed at ?lang=es it reported "no Card Deck button" and listed 28 buttons all +// reading "Baraja de tarjetas" — the UI correctly translated, the GATE unable to +// see it. The card deck is a core prayer artefact and it had ZERO coverage in the +// ten locales where a rendering bug is most likely. +const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..') +const localeOf = (u) => { try { return new URL(u).searchParams.get('lang') || 'en' } catch { return 'en' } } +const deckLabel = (loc) => { + const f = join(REPO, 'library/i18n/locales', loc, 'buttons.json') + if (!existsSync(f)) return null + try { return JSON.parse(readFileSync(f, 'utf8')).cardDeck || null } catch { return null } +} +const LOC = localeOf(URL_) +const WANT_DECK = deckLabel(LOC) || deckLabel('en') || 'Card Deck' + const b = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] }) const pg = await b.newPage({ viewport: { width: 1280, height: 1000 } }) const errs = [] @@ -27,6 +46,8 @@ pg.on('pageerror', (e) => errs.push(String(e).slice(0, 140))) await pg.goto(URL_, { waitUntil: 'load', timeout: 45000 }) await pg.waitForTimeout(9000) +await pg.evaluate((want) => { window.__WANT_DECK = want }, WANT_DECK) + await pg.evaluate(() => { window.__printed = '' const real = document.createElement.bind(document) @@ -50,7 +71,7 @@ const r = await pg.evaluate(async (stock) => { if (!host) return { error: 'no poster wall found on this page' } const sr = host.shadowRoot const btns = [...sr.querySelectorAll('.pbtn')] - const deck = btns.find((x) => /card deck/i.test(x.textContent)) + const deck = btns.find((x) => (x.textContent || '').trim().includes(window.__WANT_DECK)) if (!deck) return { error: 'no Card Deck button', buttons: btns.map((x) => x.textContent.trim()) } if (stock) { const sel = sr.querySelector('.pstock') @@ -66,6 +87,7 @@ const r = await pg.evaluate(async (stock) => { cards: doc.querySelectorAll('.card').length, qrs: doc.querySelectorAll('.qr svg').length, imgs: doc.querySelectorAll('img.ph').length, + noph: doc.querySelectorAll('.ph-none').length, page: (window.__printed.match(/@page\{size:([^;}]*)/) || [])[1] || '', } }, STOCK) @@ -84,10 +106,37 @@ if (r.page.trim() !== EXPECT_PAGE) { + (STOCK ? ' — the stock picker is not reaching the renderer' : '')) } if (r.qrs !== r.cards) fails.push(`${r.qrs} QR code(s) for ${r.cards} cards`) + +// ── "116 PORTRAITS" WAS PRINTED AND NEVER ASSERTED ────────────────────────── +// A prayer card without a face is a data tile. This gate collected the count, +// displayed it, and checked only the QR codes — so a deck of 116 cards with ZERO +// portraits passed. That is not hypothetical: card-render.js records that the deck +// once shipped with no photos at all, because image_url was never requested in +// fetchListRecords (measured 95/95 groups DO have a portrait, so the omission cost +// every card its picture). +// +// ⚠ THE COUNT CANNOT BE PINNED, for the same reason the packet's could not: roughly +// half the people groups have no portrait, and the CDN drops some. So two invariants +// instead of a number: +// 1. every card is one shape or the other — portraits + typographic === cards. +// A card that is NEITHER means the renderer emitted a third nobody checks. +// 2. a deck drawn from records that carry image_url must have SOME portraits. +// DECK_EXPECT_PORTRAITS=0 declares a deliberately photo-less deck out loud, +// rather than leaving "none" indistinguishable from "silently none". +if (r.cards && r.imgs + r.noph !== r.cards) { + fails.push(`${r.cards} cards but ${r.imgs} portrait + ${r.noph} typographic = ${r.imgs + r.noph}` + + ' — every card must be exactly one of the two') +} +if (r.cards && process.env.DECK_EXPECT_PORTRAITS !== '0' && r.imgs === 0) { + fails.push(`${r.cards} cards and NOT ONE portrait — a prayer card without a face is a data tile. ` + + 'This is what a missing image_url in the records provider looks like; set ' + + 'DECK_EXPECT_PORTRAITS=0 to declare a deliberately photo-less deck.') +} if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`) +console.log(` locale : ${LOC} (matched ${JSON.stringify(WANT_DECK)})`) console.log(` stock : ${STOCK || '(default)'}`) -console.log(` cards printed : ${r.cards} (${r.imgs} portraits, ${r.qrs} QR codes)`) +console.log(` cards printed : ${r.cards} (${r.imgs} portraits, ${r.noph} typographic, ${r.qrs} QR codes)`) console.log(` @page size : ${r.page.trim()} (want ${EXPECT_PAGE})`) console.log(` page errors : ${errs.length}`)