69 lines
4.6 KiB
JavaScript
69 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* check-vision-card-numbers — the DOXA Vision Card shows the numbers the DATA gives, nothing typed.
|
||
*
|
||
* USAGE: node internal/check-vision-card-numbers.mjs (data only, no browser, ~2 s)
|
||
* node internal/check-vision-card-numbers.mjs --url <card> (also read the RENDERED card)
|
||
* Exit 0 = every number matches · 1 = a mismatch.
|
||
*
|
||
* INDEPENDENT RECOMPUTATION. It does not import build-vision-funnel.mjs: it re-reads the raw CSV with
|
||
* its own column lookup and re-applies the published rules (HOW-THE-NUMBERS-ARE-COUNTED.md) to the
|
||
* saved DOXA list, then compares with (1) data/people-groups/vision-funnel.json, which the card
|
||
* compiles in, and (2) with --url, the numbers actually painted in the card's bars. A typed-in
|
||
* number, a stale funnel file or a rendering bug each makes it fail.
|
||
*/
|
||
import { readFileSync } from 'node:fs'
|
||
import { resolve, dirname } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||
const text = readFileSync(resolve(ROOT, 'data/people-groups/people_groups.csv'), 'utf8')
|
||
|
||
// Own CSV reader: split records on newlines outside quotes, fields on commas outside quotes.
|
||
const records = []; { let cur = ''; let q = false; for (const ch of text) { if (ch === '"') q = !q; if (ch === '\n' && !q) { records.push(cur); cur = '' } else cur += ch } if (cur.trim()) records.push(cur) }
|
||
const split = (line) => { const out = []; let f = ''; let q = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') { if (q && line[i + 1] === '"') { f += '"'; i++ } else q = !q } else if (c === ',' && !q) { out.push(f); f = '' } else if (c !== '\r') f += c } out.push(f); return out }
|
||
const head = split(records[0]).map((h) => h.replace(/^/, ''))
|
||
const col = (n) => head.indexOf(n)
|
||
const L = col('LostnessPriority'), R3 = col('ROP3Code'), ISO = col('ISOAlpha3')
|
||
const data = records.slice(1).map(split).filter((f) => f.length === head.length)
|
||
const expect = {
|
||
all: data.length,
|
||
unreached: data.filter((f) => ['0', '1', '2'].includes(f[L])).length,
|
||
frontier: data.filter((f) => f[L] === '0').length,
|
||
}
|
||
const snap = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/doxa-groups-snapshot.json'), 'utf8'))
|
||
const lost = new Map(data.map((f) => [f[R3] + '|' + f[ISO], f[L]]))
|
||
expect.doxa = snap.groups.length
|
||
expect.doxaFrontier = snap.groups.filter((g) => lost.get(g.rop3 + '|' + g.country) === '0').length
|
||
|
||
const funnel = JSON.parse(readFileSync(resolve(ROOT, 'data/people-groups/vision-funnel.json'), 'utf8'))
|
||
const got = Object.fromEntries(funnel.steps.map((s) => [s.id, s.groups]))
|
||
got.doxaFrontier = funnel.steps.find((s) => s.id === 'doxa').frontier
|
||
const bad = []
|
||
for (const k of Object.keys(expect)) {
|
||
const ok = expect[k] === got[k]
|
||
console.log((ok ? 'ok ' : 'FAIL ') + k.padEnd(13) + ' recomputed ' + String(expect[k]).padStart(6) + ' vision-funnel.json ' + got[k])
|
||
if (!ok) bad.push(k)
|
||
}
|
||
|
||
const i = process.argv.indexOf('--url')
|
||
if (i > -1) {
|
||
// Measure THIS build, or measure nothing (exit 75 = NOT MEASURED; see a9550858 / _serves-this-tree.mjs).
|
||
const { requireThisTree } = await import('./_serves-this-tree.mjs')
|
||
await requireThisTree(process.argv[i + 1], ROOT, 'VISION-CARD-NUMBERS')
|
||
const { chromium } = await import('./lib/playwright-guarded.mjs')
|
||
const b = await chromium.launch()
|
||
const p = await b.newPage({ viewport: { width: 900, height: 1000 } })
|
||
await p.goto(process.argv[i + 1], { waitUntil: 'networkidle' })
|
||
await p.waitForTimeout(1500)
|
||
const shown = await p.evaluate(() => { const el = document.querySelector('doxa-vision-card'); const r = el && el.shadowRoot; return r ? [...r.querySelectorAll('.vc-n')].map((n) => n.textContent.trim()) : [] })
|
||
const note = await p.evaluate(() => { const r = document.querySelector('doxa-vision-card')?.shadowRoot; return r ? r.querySelector('.vc-step-doxa .vc-note')?.textContent || '' : '' })
|
||
await b.close()
|
||
const want = [expect.all, expect.unreached, expect.frontier, expect.doxa].map((n) => new Intl.NumberFormat('en').format(n))
|
||
const okBars = JSON.stringify(shown) === JSON.stringify(want)
|
||
console.log((okBars ? 'ok ' : 'FAIL ') + 'rendered bars ' + JSON.stringify(shown) + (okBars ? '' : ' expected ' + JSON.stringify(want)))
|
||
const okNote = note.includes(new Intl.NumberFormat('en').format(expect.doxaFrontier))
|
||
console.log((okNote ? 'ok ' : 'FAIL ') + 'rendered DOXA note "' + note.trim() + '"')
|
||
if (!okBars) bad.push('rendered bars'); if (!okNote) bad.push('rendered note')
|
||
}
|
||
console.log(bad.length ? 'VISION-CARD-NUMBERS: FAIL — ' + bad.join(', ') : 'VISION-CARD-NUMBERS: PASS — every number matches the data')
|
||
process.exit(bad.length ? 1 : 0)
|