doxa-maps/plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs
Doxa Maps a44d88730f check:gospel-internet renders every panel in zh, ru and hi too
The production bar's cell E names en, ar, zh, ru and hi; the render check
measured two. It now server-renders the board in all five and requires
each panel to render with no error and the board's text to be the
locale's, not English.
2026-09-28 00:47:58 +00:00

132 lines
8.8 KiB
JavaScript

#!/usr/bin/env node
/**
* check-gospel-internet-render.mjs — every PANEL of the board renders, with the real data, in en and ar.
*
* WHY: check:profiles-mount runs a profile's own setup and never renders its children (measured: a throw
* planted in GiMap's or GiTree's setup passed it). So no gate had ever executed this board's panels.
* This one server-renders them (Vue's renderer, through vite's module loader, no browser, no WebGL):
* every panel's setup, computeds and template run against the real table, tree and resources, and the
* HTML is asserted on CONTENT the panel must show, never on the panel's own count of itself.
* onMounted does not run under SSR, so map layers are not covered here; that is the browser's (rega's).
*
* `--poison` blanks the rows handed to the panels and requires the content assertions to FAIL.
* Exit 0 pass · 1 fail · 75 not measured (vite could not load the modules).
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const PLUGIN = resolve(HERE, '..')
const REPO = resolve(PLUGIN, '../..')
const POISON = process.argv.includes('--poison')
const T = (f) => JSON.parse(readFileSync(resolve(REPO, 'data/tables', f), 'utf8'))
let server
try {
const { createServer } = await import('vite')
server = await createServer({ root: REPO, configFile: resolve(REPO, 'vite.config.js'), logLevel: 'error',
server: { middlewareMode: true, hmr: false }, appType: 'custom', optimizeDeps: { noDiscovery: true, include: [] },
// node_modules is a symlink in a worktree, so vite would inline vue as source; keep the packages external
// so the components and this harness share ONE vue instance
ssr: { external: ['vue', 'vue/server-renderer', '@vue/server-renderer', 'vue-i18n', 'pinia'] } })
} catch (e) { console.log('NOT MEASURED: vite would not start: ' + e.message); process.exit(75) }
let fails = 0
const ok = (c, m) => { if (c) console.log('ok ' + m); else { fails++; console.log('FAIL ' + m) } }
try {
const load = (p) => server.ssrLoadModule(resolve(PLUGIN, p))
const { createSSRApp, h, ref, markRaw } = await import('vue')
const { renderToString } = await import('vue/server-renderer')
const i18n = await server.ssrLoadModule(resolve(REPO, 'library/i18n/index.js'))
const M = await load('src/model.js')
const { useBoard } = await load('src/useBoard.js')
const panels = {}
for (const n of ['GiCombos', 'GiFacetRail', 'GiMap', 'GiCharts', 'GiGrid', 'GiDrawer', 'GiTree', 'GiResources']) panels[n] = (await load(`src/components/${n}.vue`)).default
const gi = T('gospel-internet.json')
const rows = POISON ? [] : M.normalizeRows(gi)
const treeDoc = T('gospel-internet-tree.json')
const res = T('gospel-internet-resources.json')
const open = rows.find((r) => r.access_detail === 'none' && r.province) || rows[0]
async function render(lang) {
const app = createSSRApp({
setup() {
const b = useBoard()
b.all.value = markRaw(rows)
b.bySlug.value = markRaw(new Map(rows.map((r) => [r.slug, r])))
b.meta.value = markRaw(gi.meta)
b.countries.value = markRaw(gi.countries || {})
b.tree.value = markRaw({ ...M.indexTree(treeDoc), meta: treeDoc.meta })
b.resources.value = markRaw(res)
b.loading.value = false
if (open) { b.openSlug.value = open.slug; b.bus.setSelection([open.slug]) }
// narrow to the largest family through the tree, as a click would
const fam = b.tree.value.nodes.filter((n) => !n.p).sort((x, y) => b.tree.value.isos.get(y.id).size - b.tree.value.isos.get(x.id).size)[0]
b.setTreeNode(fam.id)
return () => h('div', Object.values(panels).map((P) => h(P)))
},
})
// the channel the real board uses: the bundle installs createAppI18n, and ProfileLoader sets its
// locale from profile-config (never a window global, which does not exist here)
// createAppI18n reads <html lang> to detect a locale: give it the one document field it reads
globalThis.document = { documentElement: { lang } }
const appI18n = i18n.createAppI18n()
app.use(appI18n)
appI18n.global.locale.value = lang
app.provide('profileConfig', ref({ profile: 'board', lang }))
const errors = []
app.config.errorHandler = (e) => { errors.push(String(e && e.stack || e)) }
app.config.warnHandler = (m) => { if (/Unhandled error|Property .* was accessed during render but is not defined/.test(m)) errors.push(m) }
const html = await renderToString(app)
return { html, errors }
}
const en = await render('en')
ok(en.errors.length === 0, `en: every panel rendered with no error (${en.errors.length}${en.errors[0] ? ': ' + en.errors[0].slice(0, 160) : ''})`)
for (const [name, re] of [['GiFacetRail', /data-facet="access"/], ['GiGrid', /data-grid-total/], ['GiCharts', /data-region="charts"/],
['GiTree', /data-tree-legend[\s\S]*data-tree-poster/], ['GiResources', /data-resources-total/], ['GiMap', /data-region="map"/]]) ok(re.test(en.html), `en: ${name} rendered its region`)
ok(!!open && en.html.includes(open.people_group), `en: the drawer shows the opened group (${open ? open.people_group : 'no row'})`)
ok(/data-notice="approximate-pin"/.test(en.html), 'en: the drawer carries the approximate-pin notice')
// interop out: the selection opens in Gospel Resources by its ?groups= contract, carrying the slug
const gl = (en.html.match(/href="([^"]*)"[^>]*data-open-gospel/) || [])[1] || ''
ok(!!open && gl.includes('gospel-resources/resources-map/index.html') && decodeURIComponent(gl).includes('groups=' + open.slug) && !POISON,
`en: the selection links out to Gospel Resources with ?groups= (${gl.split('?')[1] || 'no link'})`)
ok(/data-tree-card/.test(en.html), 'en: the tree card shows the node the board is narrowed to')
ok(/href="https?:\/\/[^"]+"/.test(en.html.slice(en.html.indexOf('data-region="resources"'))), 'en: the resources tab lists real links')
ok(/\b\d[\d,]* of [\d,]+ people groups\b/.test(en.html), 'en: the grid prints its scope count')
// the selection strip: the kit's chips name the TREE NODE (not 120 language codes), and a combination
// carries its live count (nothing in own language x 60%+ online = 220 on the whole roster)
const fam = treeDoc.nodes.filter((n) => !n.p).sort((x, y) => 0)[0]
const famNode = (() => { const ix = M.indexTree(treeDoc); return ix.nodes.filter((n) => !n.p).sort((x, y) => ix.isos.get(y.id).size - ix.isos.get(x.id).size)[0] })()
ok(/data-chip="language"/.test(en.html) && en.html.includes(famNode.n), `en: the kit chip names the tree node (${famNode.n})`)
// the charts answer their question in words (the harness narrows to a family first, so assert the
// takeaways' FORM and that the gap charts name real groups/languages, not the roster's totals)
const plain = en.html.replace(/<!--[\s\S]*?-->/g, '')
ok(/\d[\d,]* of the \d[\d,]* with nothing live in countries/.test(plain) && !POISON, 'en: chart 3 states its takeaway')
ok(/data-chart="gaps"[\s\S]*?These \d+ alone are [\d,]+ people/.test(plain), 'en: the gap chart names its groups and totals them')
ok(/data-chart="ntNeed"[\s\S]*?languages here have no New Testament found/.test(plain), 'en: the New Testament chart states its takeaway')
ok(new RegExp('data-combo="digital-now"[^>]*>\\s*<span>[^<]+</span><span class="gic2-n">' + (POISON ? 'x' : '220')).test(en.html), 'en: a combination shows its live count (digital-now = 220)')
void fam
const ar = await render('ar')
ok(ar.errors.length === 0, `ar: every panel rendered with no error (${ar.errors.length})`)
const arWords = ['شجرة', 'بلغتهم', 'عوامل التصفية']
const found = arWords.filter((w) => ar.html.includes(w))
ok(found.length === arWords.length, `ar: the catalogue reaches the panels (${found.length}/${arWords.length} Arabic strings found)`)
ok(!ar.html.includes('Language tree: every language family'), 'ar: the English tree label is not left in place')
// cell E's other locales: every panel renders, and the board's own title is the locale's, not English
for (const lc of ['zh', 'ru', 'hi']) {
const out = await render(lc)
const cat = JSON.parse(readFileSync(resolve(REPO, 'library/i18n/locales', lc, 'gospelInternet.json'), 'utf8'))
ok(out.errors.length === 0 && out.html.includes(cat.tree.legendTitle) && !out.html.includes('Language tree: every language family'),
`${lc}: every panel rendered, translated (${out.errors.length} errors)`)
}
} catch (e) {
console.log('NOT MEASURED: ' + (e && e.stack || e).toString().split('\n').slice(0, 3).join(' | '))
await server.close()
process.exit(75)
}
await server.close()
if (POISON) { console.log(fails ? `POISON OK: ${fails} caught` : 'POISON FAILED'); process.exitCode = fails ? 0 : 1 }
else { console.log(fails ? `FAIL check-gospel-internet-render: ${fails}` : 'PASS check-gospel-internet-render'); process.exitCode = fails ? 1 : 0 }