doxa-maps/plugin-designs/country-posters/scripts/check-poster-section.mjs
Doxa Maps f1b2cd34f4 Re-derive country-posters.json region fields from live wagf_region
Each country's region is set to the live wagf_region its groups carry (file
label spelling); only the region field changes (stats, bbox, count, India row
untouched). The stale 'Other' bucket (18 countries) empties out and each moves
to its real region: USA and the non-Spanish Caribbean + the Guianas to North
America & Non-Spanish Caribbean; Andorra/Monaco/San Marino to Europe; Cocos,
Christmas, Niue, Norfolk, Tokelau, Wallis & Futuna to Oceania. Europe goes
27 -> 30, matching the live Regions dashboard.

check:poster-section drops 'other' from its covering scopes (the bucket is now
empty, so the /regions/other scope resolves to zero countries); the seven real
regions still cover all 175 countries exactly once.

Region-only diff (18 region lines); B25 India withheld unchanged
(check:country-sheet-one-path, poster-section, poster-browser-equiv,
poster-design-catalogue all PASS).
2026-10-02 08:38:11 +00:00

101 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
// check:poster-section — the reusable poster section (<dxm-poster-section>) and the lift behind it.
// A. the TYPE REGISTRY: 'country' is registered, an unknown type is null, a plugin can register its own.
// B. SCOPES: DX Life's /regions slugs (africa … oceania) each resolve; every spelling of a region (its
// slug, its API value, its English name) resolves to the same countries; the scopes cover all countries once.
// C. a WITHHELD whole-country poster stays withheld through the type (the card's sentence; its rows rejected,
// no records fetched).
// D. the ELEMENT's attributes map to its config (collapsible on by default, collapsible="false", collapsed).
// E. THE LIFT: every name PosterSection.vue's template uses is injected from the wall (a section never reads
// something its wall does not provide).
// --self-test: a registry without 'country', a region key without "&" → "and", and a section template that uses a
// name it does not inject must each FAIL. Loaded through Vite (the @map alias), like check:poster-wall-host-seams.
import { readFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
// 'other' dropped: the region re-derive from live wagf_region emptied the "Other" bucket (every country now carries a
// real region), so the /regions/other scope resolves to zero countries and is no longer one of the covering scopes.
const DXL = ['africa', 'asia', 'europe', 'latin-america-and-caribbean', 'middle-east', 'north-america-and-non-spanish-caribbean', 'oceania']
async function arms({ types, country, element, sectionSrc }) {
const fails = []
// A
if (!types.posterTypeNames().includes('country')) fails.push("A: the registry has no 'country' type")
if (await types.posterType('no-such-type') !== null) fails.push('A: an unknown type is not null')
types.registerPosterType('gate-fixture', { items: () => [{ id: 'x' }], title: () => 'X', actions: () => ({}) })
if (!(await types.posterType('gate-fixture'))) fails.push('A: a registered type cannot be found')
// B
const data = JSON.parse(readFileSync(join(ROOT, 'library/data/country-posters.json'), 'utf8')).countries
const seen = new Map()
for (const k of DXL) {
const ids = country.items(k, 'en').map((c) => c.id)
if (!ids.length) fails.push(`B: scope "${k}" (a DX Life /regions slug) shows no countries`)
for (const id of ids) seen.set(id, (seen.get(id) || 0) + 1)
}
const once = data.filter((c) => seen.get(c.id) === 1).length
if (once !== data.length) fails.push(`B: the scopes cover ${once} of ${data.length} countries exactly once`)
for (const [a, b, c] of [['latin-america-and-caribbean', 'latin_america_&_caribbean', 'Latin America & Caribbean'], ['middle-east', 'middle_east', 'Middle East']]) {
const s = (x) => country.items(x, 'en').map((r) => r.id).join()
if (!(s(a) && s(a) === s(b) && s(b) === s(c))) fails.push(`B: "${a}" / "${b}" / "${c}" do not resolve to the same countries`)
}
// C
const ind = country.items('asia', 'en').find((c) => c.iso3 === 'IND')
if (!ind) fails.push('C: India is not in the asia scope')
else {
let fetched = 0
const act = country.actions({ assetsBase: () => '', apiBase: () => { fetched++; return '' }, lang: () => 'en', profileConfig: null, t: (k) => k })
if (act.unsupported(ind) !== 'messages.countryPosterTooMany') fails.push("C: withheld India's card does not show messages.countryPosterTooMany")
let why = ''
try { await act.records(ind); why = 'resolved' } catch (e) { why = e && (e.i18nKey || e.message) }
if (!/countryPosterTooMany/.test(String(why))) fails.push(`C: withheld India's rows are not withheld (${why})`)
if (fetched) fails.push('C: withheld India fetched records')
}
// D
const fake = (attrs) => ({ getAttribute: (n) => (n in attrs ? attrs[n] : null), hasAttribute: (n) => n in attrs })
const d0 = element.posterSectionConfig(fake({ scope: 'africa' }))
if (!(d0.type === 'country' && d0.scope === 'africa' && d0.collapsible === true && d0.collapsed === false)) fails.push(`D: defaults wrong ${JSON.stringify(d0)}`)
if (element.posterSectionConfig(fake({ collapsible: 'false' })).collapsible !== false) fails.push('D: collapsible="false" does not turn collapsing off')
if (element.posterSectionConfig(fake({ collapsed: '' })).collapsed !== true) fails.push('D: collapsed does not start the section shut')
// E
const tpl = (sectionSrc.match(/<template>([\s\S]*)<\/template>\s*$/) || ['', ''])[1].replace(/<!--[\s\S]*?-->/g, '')
const injected = new Set(((sectionSrc.match(/const \{([\s\S]*?)\} = inject\(POSTER_WALL_CTX\)/) || ['', ''])[1]).split(/[\s,]+/).filter(Boolean))
const used = new Set()
for (const m of tpl.matchAll(/(?:[:@]|v-[a-z.-]+)[\w.-]*="([^"]*)"|\{\{([^}]*)\}\}/g)) {
for (const e of [m[1], m[2]]) { if (!e) continue
for (const id of e.replace(/'[^']*'|`[^`]*`/g, '').matchAll(/(?<![\w.$])([A-Za-z_$][\w$]*)/g)) used.add(id[1]) }
}
const local = new Set(['sec', 'z', 'item', 'n', 's', 'v', 'el', 'true', 'false', 'null', 'String', 'Number', '$event', 'in', 'label', 'zIndex'])
const missing = [...used].filter((x) => !local.has(x) && !injected.has(x))
if (missing.length) fails.push(`E: PosterSection.vue uses names it does not inject: ${missing.join(', ')}`)
return fails
}
const { createServer } = await import('vite')
const server = await createServer({ root: ROOT, configFile: false, logLevel: 'error', appType: 'custom', server: { middlewareMode: true, hmr: false, ws: false }, resolve: { alias: { '@map': join(ROOT, 'library') } } })
let code = 0
try {
const load = (p) => server.ssrLoadModule(join(ROOT, p))
const types = await load('library/components/poster/posterTypes.js')
const country = (await load('library/components/poster/types/countryPosterType.js')).default
const element = await load('library/elements/posterSectionConfig.js')
const sectionSrc = readFileSync(join(ROOT, 'library/components/poster/PosterSection.vue'), 'utf8')
const real = { types, country, element, sectionSrc }
if (process.argv.includes('--self-test')) {
const poisons = [
["a registry without 'country'", { ...real, types: { ...types, posterTypeNames: () => [] } }, /^A:/],
['a region key without "&" → "and"', { ...real, country: { ...country, items: (s, l) => (/-and-/.test(String(s)) ? [] : country.items(s, l)) } }, /^B:/],
['a section that uses a name it does not inject', { ...real, sectionSrc: sectionSrc.replace('</template>', '{{ notProvided }}</template>') }, /^E:/],
]
for (const [name, inp, rx] of poisons) {
const f = await arms(inp); const ok = f.some((x) => rx.test(x))
console.log(ok ? `✓ caught: ${name} -> ${f.find((x) => rx.test(x))}` : `✗ MISSED: ${name}`)
if (!ok) code = 1
}
} else {
const f = await arms(real)
if (f.length) { f.forEach((x) => console.log('FAIL ' + x)); code = 1 }
else console.log(`PASS check:poster-section — the 'country' type; ${DXL.length} DX Life scopes cover every country once (and their other spellings); withheld India stays withheld; the element's attributes; PosterSection injects every name it uses`)
}
} finally { await server.close() }
process.exit(code)