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.
132 lines
8.8 KiB
JavaScript
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 }
|