diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 219146823..722832dbd 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -183,7 +183,7 @@ await page.evaluate((words) => { window.__EXPECTED_RHYTHMS = words }, expected) const r = await page.evaluate(() => { const body = document.body.innerText - const imgs = [...document.querySelectorAll('.card img')] + const imgs = [...document.querySelectorAll('.card img, img.ws-ph')] // profile cards, or lightweight rows (2026-09-30) // ⚠ The expected rhythm words come from the STRINGS CATALOG FOR THIS PACKET'S // LANGUAGE, never a hardcoded English list. A fixed list tests the opposite of // what i18n means: it passed a packet whose translation silently did nothing and @@ -519,6 +519,16 @@ if (r.cards && expectPhotos && r.imgs === 0) { + 'failed photo fetch, not a zone without pictures. Re-run without NOPHOTOS=1, or set ' + 'PACKET_EXPECT_PHOTOS=0 to declare this a deliberately photo-less packet.') } +// ★ THE LIGHTWEIGHT ROWS SHOW PEOPLE TOO (2026-09-30): every worksheet row carries exactly one photo cell, a +// photo or the "no photo" box, and a photo-expecting packet carries at least one photo, as the cards above. +if (r.wsRows && r.imgs + r.nophoto !== r.wsRows) { + fails.push(`${r.wsRows} worksheet rows but ${r.imgs} photo + ${r.nophoto} "no photo" = ${r.imgs + r.nophoto} — ` + + 'every row must carry exactly one of the two') +} +if (r.wsRows && expectPhotos && r.imgs === 0) { + fails.push(`${r.wsRows} worksheet rows and NOT ONE photo — the lightweight packet shows every group's photo ` + + 'too, so this is a skipped or failed photo fetch. Set PACKET_EXPECT_PHOTOS=0 to declare a photo-less packet.') +} if (r.wheelSegs !== 12) fails.push(`prayer wheel has ${r.wheelSegs} segments, expected 12`) // A SEGMENT WITHOUT ITS WORD IS A COLOUR. 12 labels, one per segment, on the wheel // sheet itself — not merely somewhere in the document. @@ -718,8 +728,9 @@ const lifeSheets = (r.lifeSheets || 0) + (r.christianSheets || 0) // ⚠ AND THE SETS PAGES (bounded, centered): back matter, DETECTED for the same reason as the rest. // Before they were counted the arithmetic only held when a fixture omitted set data. const setsSheets = r.setsSheets || 0 -// The worksheet paginates 20 rows on its first page and 24 after (packet-render WS_FIRST / WS_REST). -const wsPages = (n) => (n <= 0 ? 0 : 1 + Math.ceil(Math.max(0, n - 20) / 24)) +// The worksheet paginates 16 rows on its first page and 18 after (packet-render WS_FIRST / WS_REST; 20 and 24 +// before its rows gained a photo and a description, 2026-09-30). +const wsPages = (n) => (n <= 0 ? 0 : 1 + Math.ceil(Math.max(0, n - 16) / 18)) const listSheets = r.cards ? Math.ceil(r.cards / PER_PAGE) : wsPages(r.wsRows) if (r.wsRows && r.wsSheets !== wsPages(r.wsRows)) fails.push(`${r.wsRows} worksheet rows should take ${wsPages(r.wsRows)} page(s) but the packet has ${r.wsSheets}`) // + 2 again (2026-09-30, the second revision): point 4's Mark 5:18-20 and the Father's-heart list are back ON the diff --git a/internal/check-packet-variants.mjs b/internal/check-packet-variants.mjs index 4ac04a1de..1d4375d1c 100644 --- a/internal/check-packet-variants.mjs +++ b/internal/check-packet-variants.mjs @@ -102,6 +102,18 @@ ok(count(light, /sheet-sets-(bounded|centered)/g) === 0, 'lightweight still carr ok(count(full, /
`${g.name} is a people group in Malaysia.` }) + const pics = new Map(groups.filter((_, i) => i % 5).map((g) => [g.slug, `https://img.test/${g.slug}.jpg`])) + const lw = RD.buildPacket(zone, groups, pics, '', funnel, christian, 'lightweight', sets) + const rows = count(lw, /class="ws-row"/g), imgs = count(lw, /${esc(cutAtWord(sentence, WS_DESC_BUDGET))}`; + const where = [flat(g.country_code_label) || flat(g.country) || flat(g.country_code), // the name, not the ISO3 + Number(g.population) > 0 ? `${num(g.population)} ${t('people_suffix')}` : ''].filter(Boolean).join(' · '); + return `${esc(where)}`; + } + function worksheetPages(zone, groups, photos = new Map()) { const pages = []; - const head = `
${esc(t('light_col_group'))}${ + const head = `
${esc(t('light_col_group'))}${ PRAY_COLS.map((key) => `${esc(t(key))}`).join('')}
`; for (let start = 0, page = 0; start < groups.length; page += 1) { const per = page === 0 ? WS_FIRST : WS_REST; const slice = groups.slice(start, start + per); const rows = slice.map((g, k) => { - const where = [flat(g.country_code_label) || flat(g.country) || flat(g.country_code), // the name, not the ISO3 - Number(g.population) > 0 ? `${num(g.population)} ${t('people_suffix')}` : ''].filter(Boolean).join(' · '); - return `
${num(start + k + 1)}` - + `${esc(g.name)}${esc(where)}` + const src = photos.get(g.slug); + // esc() on the URL, as the profile cards do: a quote in an API image_url must not become markup. + const photo = src + ? `` + : `${esc(t('no_photo'))}`; + return `
${num(start + k + 1)}${photo}` + + `${esc(g.name)}${worksheetLine(g)}` + PRAY_COLS.map(() => '').join('') + '
'; }).join(''); const top = page === 0 @@ -1527,7 +1552,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de keep('christian') && christianPage(zone, christian), // NOT behind keep() — see the VARIANTS comment: dropping list pages drops people. The lightweight // packet lists the SAME groups as a worksheet (LIST_STYLE); the full packet as profiles. - ...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups) : listPages(zone, groups, photos)), + ...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups, photos) : listPages(zone, groups, photos)), // BACK MATTER: after the profiles (brief A1). Full-detail only. keep('sets') && setsPage(zone, sets), keep('wheel') && wheelPage(),