test(packet): gate the packet the READER gets, not the one Node writes
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
check:packet-render verifies a packet FILE from the build-time generator. This
verifies the BROWSER path — the legend's Export > Prayer packet action, which
imports library/packet/packet-render.js, fetches live records and prints
in-place.
Both callers now share that one renderer, and this gate is what proves the
shared module actually runs in a tab. A module can be perfectly correct under
Node and still fail in a browser: a bare process.env reference, a Node-only
import, a JSON import assertion. Node passing says nothing about that.
It drives the real thing: the renderer over HTTP, the app's own normalizer
(the API returns { posts, total } — hand-rolling that extraction is how a probe
ends up testing something the product never does), live records, then
page.pdf() and a PDF page count.
Measured on the browser path: 8 sheets -> 8 pages at 612x792pt, 95 people
cards, 12 wheel segments, Mark 5:1-20 in full, exactly 2 metacamp.org
attributions, 0 page errors.
I18N is asserted here because it is a product requirement, not a nicety: pass a
language and the gate reports which headings came back translated. Verified with
a partial Spanish block — "12 Puntos de Oración" and "Rueda de Oración" rendered
while Mark 5 and both attributions correctly fell back to English. Fallback is
PER KEY, so a half-finished translation renders its finished keys and never
blanks. The stub block was removed after verifying; shipping it would imply a
Spanish translation that does not exist.
Parameterised rather than pinned: PACKET_ORIGIN / PACKET_API / PACKET_ISO3, and
the language as argv. Needs a static server over the repo root.
This commit is contained in:
parent
301cc38fba
commit
097ab02458
2 changed files with 83 additions and 0 deletions
82
internal/check-packet-browser.mjs
Normal file
82
internal/check-packet-browser.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// check-packet-browser — the packet the READER gets, not the one Node writes.
|
||||
//
|
||||
// WHY A SECOND PACKET GATE. check-packet-render verifies a packet FILE produced by
|
||||
// the build-time generator. This one verifies the BROWSER path: the legend's
|
||||
// Export > Prayer packet action imports library/packet/packet-render.js, fetches
|
||||
// live records, and prints in-place. Both callers share that one renderer, and this
|
||||
// gate is what proves the shared module actually runs in a browser — a module can
|
||||
// be perfectly correct under Node and still fail in a tab (a bare `process.env`
|
||||
// reference, a Node-only import, a JSON import assertion).
|
||||
//
|
||||
// It also covers I18N, which is a product requirement rather than a nicety: pass a
|
||||
// language and the gate reports which headings came back translated. Fallback is
|
||||
// PER KEY, so a half-finished translation must render its finished keys and leave
|
||||
// the rest in English — never blank.
|
||||
//
|
||||
// Usage: node internal/check-packet-browser.mjs [lang]
|
||||
// PACKET_ORIGIN http origin serving the repo root (default http://127.0.0.1:4898)
|
||||
// PACKET_API records API (default https://pray.doxa.life)
|
||||
// PACKET_ISO3 country to build a test packet for (default LAO)
|
||||
// Needs a static server over the repo root, e.g.: python3 -m http.server 4898
|
||||
|
||||
import { chromium } from 'playwright'
|
||||
const b = await chromium.launch()
|
||||
const pg = await b.newPage({ viewport:{width:1500,height:1941} })
|
||||
const errs=[]; pg.on('pageerror',e=>errs.push(String(e).slice(0,140)))
|
||||
|
||||
const ORIGIN = process.env.PACKET_ORIGIN || 'http://127.0.0.1:4898'
|
||||
const API = process.env.PACKET_API || 'https://pray.doxa.life'
|
||||
const ISO3 = process.env.PACKET_ISO3 || 'LAO'
|
||||
const RENDER = `${ORIGIN}/library/packet/packet-render.js`
|
||||
const STRINGS = `${ORIGIN}/library/scripts/posters/packet-strings.json`
|
||||
const LANG = process.argv[2] || 'en'
|
||||
|
||||
await pg.goto(ORIGIN + '/')
|
||||
const html = await pg.evaluate(async ({RENDER, STRINGS, LANG, ORIGIN, API, ISO3}) => {
|
||||
const [{ createPacketRenderer }, strings, norm] = await Promise.all([
|
||||
import(RENDER),
|
||||
fetch(STRINGS).then(r=>r.json()),
|
||||
import(`${ORIGIN}/plugin-designs/my-upg-100-list/data/normalize.js`),
|
||||
])
|
||||
const R = createPacketRenderer({ strings, lang: LANG })
|
||||
const resp = await fetch(`${API}/api/people-groups/list?fields=slug,name,country_code,population,religion,image_url&lang=en`, {cache:'no-store'})
|
||||
// Use the SAME normalizer the app uses — the API returns { posts, total }, and
|
||||
// hand-rolling the extraction is how a probe ends up testing something the
|
||||
// product never does.
|
||||
const recs = norm.normalizeRecords(norm.extractRecords(await resp.json()))
|
||||
const groups = recs.filter(r=>String(r.country_code)===ISO3)
|
||||
.sort((a,b)=>String(a.name).localeCompare(String(b.name), LANG))
|
||||
const photos = new Map(groups.filter(g=>g.image_url).map(g=>[g.slug,g.image_url]))
|
||||
return R.buildPacket({id:ISO3.toLowerCase(),label:ISO3}, groups, photos, '')
|
||||
}, {RENDER, STRINGS, LANG, ORIGIN, API, ISO3})
|
||||
|
||||
await pg.setContent(html, { waitUntil:'load' })
|
||||
await pg.waitForTimeout(3000)
|
||||
const r = await pg.evaluate(()=>({
|
||||
sheets: document.querySelectorAll('.sheet').length,
|
||||
cards: document.querySelectorAll('.card').length,
|
||||
wheel: document.querySelectorAll('svg path[fill^="hsl"]').length,
|
||||
mark5: document.body.innerText.includes('Gadarenes'),
|
||||
metacamp: (document.body.innerText.match(/metacamp\.org/g)||[]).length,
|
||||
firstCard: (document.querySelector('.card')||{}).innerText||'',
|
||||
wheelTitle: (document.body.innerText.match(/^.*Wheel.*$/m)||[''])[0],
|
||||
pointsTitle: (document.body.innerText.match(/^.*Prayer Points.*$|^.*Puntos.*$/m)||[''])[0],
|
||||
}))
|
||||
const pdf = await pg.pdf({ preferCSSPageSize:true })
|
||||
const t = pdf.toString('latin1')
|
||||
const pages=(t.match(/\/Type\s*\/Page[^s]/g)||[]).length
|
||||
const boxes=[...t.matchAll(/MediaBox\s*\[\s*0\s+0\s+([\d.]+)\s+([\d.]+)\s*\]/g)].map(m=>[+m[1],+m[2]])
|
||||
await b.close()
|
||||
console.log(` lang : ${LANG}`)
|
||||
console.log(` sheets/PDF pages : ${r.sheets} / ${pages}`)
|
||||
console.log(` people cards : ${r.cards}`)
|
||||
console.log(` wheel segments : ${r.wheel}`)
|
||||
console.log(` Mark 5 in full : ${r.mark5}`)
|
||||
console.log(` metacamp.org : ${r.metacamp}`)
|
||||
console.log(` page size : ${boxes[0]?boxes[0].join(' x '):'?'} pt`)
|
||||
console.log(` points heading : ${r.pointsTitle.trim()}`)
|
||||
console.log(` wheel heading : ${r.wheelTitle.trim()}`)
|
||||
console.log(` page errors : ${errs.length} ${errs[0]||''}`)
|
||||
const bad = !r.sheets || pages!==r.sheets || !r.cards || r.wheel!==12 || !r.mark5 || r.metacamp!==2 || errs.length
|
||||
console.log(bad?'\nFAIL':'\nPASS — browser-rendered packet is correct')
|
||||
process.exit(bad?1:0)
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs # the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config AND the shipped config must still render today's ramp. Two sides: (B) a configured threshold must MOVE the edge — catches re-hard-coding; (A2) dashboard.config.js's own map.* must still resolve to 5/20/60/150/Infinity — catches the shipped default silently changing. A2 was MISSING at first and heartbeatThreshold:999 shipped past this gate printing 'default ramp unchanged'. A grep for '150' cannot do either job: it false-positives on the legitimate default and false-negatives on a re-hard-coding at 200. run check:heartbeat:self-test for the NEGATIVE tests (3 cases).",
|
||||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
|
||||
"check:packet-browser": "node internal/check-packet-browser.mjs",
|
||||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||||
"check:template-literal": "node internal/template-literal-check.mjs",
|
||||
"verify:map-paint": "node internal/verify-map-paint.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue