a9550858 guarded twenty gates; these six were still unguarded. Each
now exits 75 (NOT MEASURED) rather than grading another tree:
- check-legend-export-reachable, check-vision-card-numbers --url:
requireThisTree on the URL they are given.
- check-embed-basemap-paints: it picks a RANDOM port, not an
OS-assigned one, so if another agent holds it, its own server fails
to start and it would measure theirs. It now checks the leaf server's
identity, stops its own servers and exits 75. Its "a server did not
come up" path exited 1 and now exits 75.
- check-tabs-localized, check-poster-prints read library/ from
DOXA_REPO. Pointing that at another checkout now needs
DOXA_REPO_MEASURE_OTHER=1 (new requireThisRepo helper).
- check-packet-render refuses a packet file inside a DIFFERENT git
checkout (new requirePathInThisCheckout helper); a file outside any
checkout is allowed with a note.
Proven by pointing each at a wrong build: a copy of this public/ whose
manifest differs by one byte, served on its own port, and another
worktree for the file and DOXA_REPO gates. All 8 (these 6, plus
shell-scroll and upg-map-data) exit 75. On this tree they measure:
legend-export, vision-card, upg-map-data, tabs-localized and
poster-prints PASS. packet-render gets past the guard and fails on a
non-packet file, as intended. embed-basemap measures and FAILS: "the
page does NOT paint a basemap on its own baked token". That is a
product finding, not the guard, and is reported to its owner.
187 lines
11 KiB
JavaScript
187 lines
11 KiB
JavaScript
// check-poster-prints — the SHARED print actions must produce real documents.
|
|
//
|
|
// Context: the poster wall was generalised into library/components/poster/
|
|
// PosterWall.vue and two bundles migrated onto it, but the ORIGINAL 903-line
|
|
// my-upg-100-list wall — the one carrying the actual print actions — was left
|
|
// behind. So the shared component's packet path was a "coming soon" stub, and
|
|
// every print fix in the 100-list was invisible to the three bundles using the
|
|
// shared wall. usePosterPrints.js closes that: the ACTIONS are shared, the RECORD
|
|
// FETCHING stays per-bundle behind a `records` provider.
|
|
//
|
|
// ★ THE SEAM IS THE THING TO ASSERT. A wall WITH a provider must produce a real
|
|
// document; a wall WITHOUT one must degrade honestly rather than failing at click
|
|
// time. Both directions are checked, because "the button is there" and "the button
|
|
// works" are different claims — this flight has repeatedly found the first true
|
|
// and the second false.
|
|
//
|
|
// Runs the composable in a browser page (it calls document/DOM), driving it with a
|
|
// fake records provider so the assertion is about the DOCUMENTS, not about any
|
|
// bundle's API being reachable.
|
|
import { chromium } from 'playwright'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const REPO = process.env.DOXA_REPO
|
|
// Third instance of one pattern this flight (after check-zone-census and
|
|
// check-share-policy): a gate authored in one worktree hardcodes THAT worktree as its
|
|
// fallback, and check:no-abs-paths — which is inside build:unlocked — turns red for every
|
|
// other builder. DOXA_REPO still wins; only the default is made portable, using the idiom
|
|
// check-no-absolute-paths.mjs prints in its own failure text.
|
|
|| resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs).
|
|
{ const { requireThisRepo } = await import('./_serves-this-tree.mjs'); requireThisRepo(REPO, resolve(dirname(fileURLToPath(import.meta.url)), '..'), 'POSTER-PRINTS') }
|
|
const src = readFileSync(resolve(REPO, 'library/composables/usePosterPrints.js'), 'utf8')
|
|
const frame = readFileSync(resolve(REPO, 'library/composables/printInPlace.js'), 'utf8')
|
|
// The composable dynamically imports its renderers by RELATIVE path. Under a blob
|
|
// URL there is no directory to resolve '../packet/...' against, so those imports
|
|
// fail with "Failed to resolve module specifier" — a harness limitation, not a
|
|
// product bug (Vite resolves them, and the built bundle inlines both renderers).
|
|
// Read them here and substitute blob URLs so the gate exercises the REAL code.
|
|
const packetRender = readFileSync(resolve(REPO, 'library/packet/packet-render.js'), 'utf8')
|
|
const cardRender = readFileSync(resolve(REPO, 'library/packet/card-render.js'), 'utf8')
|
|
const qr = readFileSync(resolve(REPO, 'library/packet/qr.js'), 'utf8')
|
|
const strings = readFileSync(resolve(REPO, 'library/scripts/posters/packet-strings.json'), 'utf8')
|
|
|
|
const b = await chromium.launch()
|
|
const pg = await b.newPage({ viewport: { width: 1000, height: 800 } })
|
|
const errs = []
|
|
pg.on('pageerror', (e) => errs.push(String(e).slice(0, 140)))
|
|
await pg.setContent('<!DOCTYPE html><html><body></body></html>')
|
|
|
|
const r = await pg.evaluate(async ({ src, frame, packetRender, cardRender, qr, strings }) => {
|
|
// Shim the two module deps so the composable can be evaluated standalone: a tiny
|
|
// `ref`, and printInPlace's real source so the frame mechanism is the real one.
|
|
const frameUrl = URL.createObjectURL(new Blob([frame], { type: 'text/javascript' }))
|
|
const vueShim = URL.createObjectURL(new Blob([
|
|
'export const ref = (v) => ({ value: v })',
|
|
], { type: 'text/javascript' }))
|
|
const blob = (code) => URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))
|
|
const qrUrl = blob(qr)
|
|
const packetUrl = blob(packetRender.replace("from './qr.js'", `from '${qrUrl}'`))
|
|
const cardUrl = blob(cardRender.replace("from './qr.js'", `from '${qrUrl}'`))
|
|
const stringsUrl = URL.createObjectURL(new Blob([`export default ${strings}`], { type: 'text/javascript' }))
|
|
const patched = src
|
|
.replace("from 'vue'", `from '${vueShim}'`)
|
|
.replace("from './printInPlace.js'", `from '${frameUrl}'`)
|
|
.replace("'../packet/packet-render.js'", `'${packetUrl}'`)
|
|
.replace("'../packet/card-render.js'", `'${cardUrl}'`)
|
|
.replace(/'\.\.\/scripts\/posters\/packet-strings\.json'/g, `'${stringsUrl}'`)
|
|
const modUrl = URL.createObjectURL(new Blob([patched], { type: 'text/javascript' }))
|
|
const { usePosterPrints } = await import(modUrl)
|
|
|
|
const ROWS = [
|
|
{ slug: 'a', name: 'Aheu', country_code_label: 'Laos', population: 2700, religion_label: 'Animism' },
|
|
{ slug: 'b', name: 'Akha', country_code_label: 'Laos', population: 113000, religion_label: 'Animism' },
|
|
]
|
|
const out = {}
|
|
|
|
// ── with a provider ──────────────────────────────────────────────────────
|
|
// ⚠ CAPTURE THE HTML, do not read the frame back. openPrintFrame calls
|
|
// window.print() under autoprint, which BLOCKS in headless Chromium, and the
|
|
// frame self-removes on afterprint — so reading it back measured a frame that had
|
|
// already gone and reported 0 rows for a correct document. Intercept the document
|
|
// the composable hands to the frame instead: that IS what gets printed.
|
|
let printed = ''
|
|
const realCreate = document.createElement.bind(document)
|
|
document.createElement = (tag) => {
|
|
const el = realCreate(tag)
|
|
if (tag === 'iframe') {
|
|
Object.defineProperty(el, 'contentDocument', {
|
|
get: () => ({ open() {}, close() {}, write(h) { printed += h } }),
|
|
})
|
|
Object.defineProperty(el, 'contentWindow', { get: () => ({ addEventListener() {} }) })
|
|
}
|
|
return el
|
|
}
|
|
const withProv = usePosterPrints({ records: async () => ROWS, label: (i) => i.label })
|
|
await withProv.printList({ id: 'laos', label: 'Laos' })
|
|
await new Promise((r2) => setTimeout(r2, 400))
|
|
document.createElement = realCreate
|
|
const doc = new DOMParser().parseFromString(printed, 'text/html')
|
|
out.listRows = doc ? doc.querySelectorAll('tbody tr').length : 0
|
|
out.listHasCheckbox = doc ? /□/.test(doc.body.innerText) : false
|
|
out.listPhase = withProv.exportState.value?.phase ?? 'cleared'
|
|
|
|
// ── the PACKET action: same provider, the full booklet ───────────────────
|
|
// Migrated from upg-100-list.vue, where it was a second hand-maintained copy of
|
|
// the same renderer call. Asserted here so the shared version cannot regress
|
|
// silently — the packet is the product's core artefact.
|
|
const PKT = [
|
|
{ slug: 'a', name: 'Aheu', country_code_label: 'Laos', population: 2700, image_url: 'x.jpg' },
|
|
{ slug: 'b', name: 'Akha', country_code_label: 'Laos', population: 113000, image_url: 'y.jpg' },
|
|
]
|
|
printed = ''
|
|
document.createElement = (tag) => {
|
|
const el = realCreate(tag)
|
|
if (tag === 'iframe') {
|
|
Object.defineProperty(el, 'contentDocument', {
|
|
get: () => ({ open() {}, close() {}, write(h) { printed += h } }),
|
|
})
|
|
Object.defineProperty(el, 'contentWindow', { get: () => ({ addEventListener() {} }) })
|
|
}
|
|
return el
|
|
}
|
|
const pk = usePosterPrints({ records: async () => PKT, label: (i) => i.label })
|
|
await pk.printPacket({ id: 'laos', label: 'Laos' })
|
|
await new Promise((r2) => setTimeout(r2, 600))
|
|
document.createElement = realCreate
|
|
const pdoc = new DOMParser().parseFromString(printed, 'text/html')
|
|
out.packetSheets = pdoc.querySelectorAll('.sheet').length
|
|
out.packetCards = pdoc.querySelectorAll('.card').length
|
|
out.packetWheel = pdoc.querySelectorAll('svg path[fill^="hsl"]').length
|
|
// ⚠ NOT /Gadarenes/. That is an ENGLISH proper noun from the World English Bible
|
|
// text, so this assertion would go RED on a CORRECTLY TRANSLATED packet and passes
|
|
// today only because point4_passage is still English in all ten non-English blocks
|
|
// (DECIDE-MARK5-SCRIPTURE-IS-ENGLISH-IN-EVERY-TRANSLATED-PACKET). The check and the
|
|
// bug were locked together — the same fix already applied in check-packet-render
|
|
// and check-packet-browser. Assert the passage BLOCK and its length, which holds in
|
|
// any language.
|
|
out.packetMark5Len = (pdoc.querySelector('.p4-passage') || { textContent: '' }).textContent.trim().length
|
|
|
|
// ── without a provider: must FAIL HONESTLY, not throw ────────────────────
|
|
const noProv = usePosterPrints({ label: (i) => i.label })
|
|
await noProv.printList({ id: 'x', label: 'X' })
|
|
out.noProvPhase = noProv.exportState.value?.phase
|
|
out.noProvMsg = noProv.exportState.value?.message || ''
|
|
return out
|
|
}, { src, frame, packetRender, cardRender, qr, strings })
|
|
await b.close()
|
|
|
|
const fails = []
|
|
if (r.listRows !== 2) fails.push(`the list print rendered ${r.listRows} row(s), expected 2`)
|
|
if (!r.listHasCheckbox) fails.push('the list print has no prayer checkbox column')
|
|
if (r.noProvPhase !== 'error') {
|
|
fails.push(`without a records provider the phase was "${r.noProvPhase}", expected "error" — `
|
|
+ 'it must degrade honestly rather than silently doing nothing')
|
|
}
|
|
if (!/no records provider/i.test(r.noProvMsg)) {
|
|
fails.push(`the no-provider message was "${r.noProvMsg}" — it should name the missing provider`)
|
|
}
|
|
// The packet is the product's core artefact: map page + people pages + points +
|
|
// wheel. Assert its SHAPE, not merely that bytes were produced. With 2 groups
|
|
// that is 1 map + 1 people page + points + wheel = 4 sheets.
|
|
if (r.packetSheets !== 4) fails.push(`the packet printed ${r.packetSheets} sheets, expected 4`)
|
|
if (r.packetCards !== 2) fails.push(`the packet printed ${r.packetCards} people cards, expected 2`)
|
|
if (r.packetWheel !== 12) fails.push(`the prayer wheel has ${r.packetWheel} segments, expected 12`)
|
|
// The English passage is 2,141 chars; 1,200 is a floor no truncation or placeholder
|
|
// survives, with room for languages that render the same text more compactly.
|
|
if (r.packetMark5Len < 1200) {
|
|
fails.push(r.packetMark5Len === 0
|
|
? 'the packet is missing Mark 5:1-20 in full (no .p4-passage block)'
|
|
: `Mark 5:1-20 looks truncated: ${r.packetMark5Len} chars`)
|
|
}
|
|
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
|
|
|
|
console.log(` list print : ${r.listRows} rows, checkbox=${r.listHasCheckbox}`)
|
|
console.log(` without provider : phase=${r.noProvPhase} "${r.noProvMsg.slice(0, 60)}"`)
|
|
console.log(` packet : ${r.packetSheets} sheets, ${r.packetCards} cards, `
|
|
+ `${r.packetWheel} wheel segments, Mark5=${r.packetMark5Len} chars`)
|
|
console.log(` page errors : ${errs.length}`)
|
|
|
|
if (fails.length) {
|
|
console.error(`\nPOSTER-PRINTS: FAIL — ${fails.length} problem(s)`)
|
|
for (const f of fails) console.error(' - ' + f)
|
|
process.exit(1)
|
|
}
|
|
console.log('\nPOSTER-PRINTS: PASS — shared print actions produce real documents, and degrade honestly without a provider.')
|