From 449adfe515137fdf5a27277609e3c47bd97a3426 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 04:56:05 +0000 Subject: [PATCH] test(poster): the shared print actions must produce real documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gates usePosterPrints.js — the print actions now shared by every poster wall — by driving them in a browser with a fake records provider, so the assertion is about the DOCUMENTS rather than any bundle's API being reachable. ★ BOTH DIRECTIONS, because "the button is there" and "the button works" are different claims and this flight has repeatedly found the first true while the second was false. With a provider: a real table, correct row count, the prayer checkbox column present. WITHOUT one: phase "error" naming the missing provider, not a silent no-op and not a throw. ⚠ IT CAPTURES THE PRINTED HTML INSTEAD OF READING THE IFRAME BACK, and finding that out cost a wrong first result. 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 perfectly correct document. Intercepting what the composable hands to the frame measures what actually gets printed. Confirmed to fail before being accepted: deleting the checkbox column from the row template reports "the list print has no prayer checkbox column". --- internal/check-poster-prints.mjs | 111 +++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 112 insertions(+) create mode 100644 internal/check-poster-prints.mjs diff --git a/internal/check-poster-prints.mjs b/internal/check-poster-prints.mjs new file mode 100644 index 000000000..bc1f66657 --- /dev/null +++ b/internal/check-poster-prints.mjs @@ -0,0 +1,111 @@ +// 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 } from 'node:path' + +const REPO = process.env.DOXA_REPO + || resolve('/home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-packet') +const src = readFileSync(resolve(REPO, 'library/composables/usePosterPrints.js'), 'utf8') +const frame = readFileSync(resolve(REPO, 'library/composables/printInPlace.js'), '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('') + +const r = await pg.evaluate(async ({ src, frame }) => { + // 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 patched = src + .replace("from 'vue'", `from '${vueShim}'`) + .replace("from './printInPlace.js'", `from '${frameUrl}'`) + 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' + + // ── 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 }) +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`) +} +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(` 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.') diff --git a/package.json b/package.json index dd2a7926a..c033b877a 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "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:card-deck": "node internal/check-card-deck.mjs", "check:poster-raster": "node internal/check-poster-raster.mjs", + "check:poster-prints": "node internal/check-poster-prints.mjs", "check:provider-switch": "node internal/check-provider-switch.mjs", "check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs", "check:share-policy": "node internal/check-share-policy.mjs",