check:poster-sections: the presets must equal each poster type's own regions

The presets file now has to hold, per poster type, exactly an all-regions
preset plus one per region the type's adapter returns from scopes(). A region
the data drops (a stale "other") or gains then fails the check, instead of
lingering as a preset. 'country' is measured always; 'upg100-list' once its
type file is on the tree. Self-test: a preset for a region the type does not
have must fail.
This commit is contained in:
Doxa Maps 2026-10-02 08:32:35 +00:00
parent c6425ed1cf
commit 6060694545
2 changed files with 55 additions and 6 deletions

View file

@ -693,8 +693,8 @@
"check:side-lanes:self-test": "a poster-render.js copy WITHOUT the lane clamp must make check:side-lanes' crossing arm FAIL on the same seeded data (it does: 36 crossings), so the arm is shown able to fail.",
"check:poster-section": "the reusable poster section (<dxm-poster-section>): the type registry has 'country'; DX Life's /regions slugs (and every other spelling of a region) each show their countries, every country once; a withheld whole-country poster stays withheld through the type; the element's attributes map to its config; PosterSection.vue injects every name its template uses.",
"check:poster-section:self-test": "a registry without 'country', a region key without \"&\" → \"and\", and a section that uses a name it does not inject must each make check:poster-section fail.",
"check:poster-sections": "the Poster Sections page (plugin-designs/poster-sections): every panel choice (each region or All regions, each language, open or collapsed, 1-5 per row) builds <dxm-poster-section> tags that the element's own attribute reader turns back into exactly those choices; All regions is one section per region; Copy embed is the bundle script plus those same tags.",
"check:poster-sections:self-test": "a tag builder that drops columns, one that drops collapsed, and one that repeats a region under All regions must each make check:poster-sections fail.",
"check:poster-sections": "the Poster Sections page (plugin-designs/poster-sections): every panel choice (each region or All regions, each language, open or collapsed, 1-5 per row) builds <dxm-poster-section> tags that the element's own attribute reader turns back into exactly those choices; All regions is one section per region; Copy embed is the bundle script plus those same tags. The presets file holds, per poster type, exactly an all-regions preset plus one per region its adapter's scopes() returns, so a region the data drops (or gains) cannot drift from the presets.",
"check:poster-sections:self-test": "a tag builder that drops columns, one that drops collapsed, one that repeats a region under All regions, and a preset for a region the type does not have must each make check:poster-sections fail.",
"check:side-lane-photo-pixels": "BROWSER gate (browser seat; elsewhere 75 NOT MEASURED): the 60x96 side-lane sheet rasterised by poster-raster.js, measured in pixels at every photo slot. A data: photo draws; every photo on its own host stays flat, never the broken-image glyph an external <image> rasterises as.",
"check:side-lane-photo-pixels:self-test": "the same raster WITHOUT stripExternalImages must leave the external photo slots NOT flat (the glyph), else the gate proves nothing."
},

View file

@ -3,8 +3,12 @@
// For every choice the panel offers (type, one region or All regions, each language, collapsed or open, 1-5 per
// row), the tags src/embed.js builds, read back through the ELEMENT's own attribute reader (posterSectionConfig),
// give exactly those choices; "All regions" is one section per region, each once; the snippet's script and tags
// are the same tags the page renders. --self-test: a builder that drops `columns`, one that drops `collapsed`, and
// one that repeats a region under All must each FAIL.
// are the same tags the page renders.
// PRESETS: poster-sections.instances.json has, per poster type, exactly "<prefix>-all" + one preset per region its
// adapter's scopes() returns, no more, no fewer. So a region the data no longer has (a stale "other") or a new one
// cannot drift from the presets. 'country' is always measured; 'upg100-list' once its type file is on the tree.
// --self-test: a builder that drops `columns`, one that drops `collapsed`, one that repeats a region under All, and a
// presets file with a region the type does not have must each FAIL.
import { readdirSync, statSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -12,7 +16,45 @@ import { fileURLToPath } from 'node:url'
const LOC = join(dirname(fileURLToPath(import.meta.url)), '../../../library/i18n/locales')
const SUPPORTED_LOCALES = readdirSync(LOC).filter((d) => statSync(join(LOC, d)).isDirectory()).sort()
import { posterSectionConfig } from '../../../library/elements/posterSectionConfig.js'
import { readFileSync, existsSync } from 'node:fs'
import * as real from '../src/embed.js'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..')
const PRESETS = JSON.parse(readFileSync(join(ROOT, 'plugin-designs/poster-sections/poster-sections.instances.json'), 'utf8'))
// poster type → [preset id prefix, the file that defines the type's adapter (its default export has scopes())]
const TYPES = { country: ['countries', 'library/components/poster/types/countryPosterType.js'], 'upg100-list': ['upg100', 'plugin-designs/my-upg-100-list/data/upg100-poster-type.js'] }
// Each type's scopes(), loaded through Vite (the adapters import through the @map alias).
async function loadScopes() {
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') } } })
const out = {}
try {
for (const [type, [, file]] of Object.entries(TYPES)) {
if (!existsSync(join(ROOT, file))) { out[type] = null; continue }
const a = (await server.ssrLoadModule(join(ROOT, file))).default
out[type] = a.scopes()
}
} finally { await server.close() }
return out
}
function presetArm(presets, scopesByType) {
const fails = []
for (const [type, [prefix]] of Object.entries(TYPES)) {
const scopes = scopesByType[type]
const ids = presets.instances.filter((i) => i.props && i.props.type === type).map((i) => i.id)
if (scopes == null) continue // the type's file is not on this tree yet
const want = [`${prefix}-all`, ...scopes.map((s) => `${prefix}-${s}`)].sort()
const have = [...ids].sort()
const extra = have.filter((x) => !want.includes(x)), missing = want.filter((x) => !have.includes(x))
if (extra.length || missing.length) fails.push(`presets for "${type}" differ from its scopes(): extra ${extra.join(', ') || '-'}; missing ${missing.join(', ') || '-'}`)
for (const i of presets.instances.filter((x) => x.props && x.props.type === type && x.id !== `${prefix}-all`)) {
if (i.id !== `${prefix}-${i.props.scope}`) fails.push(`preset "${i.id}" shows scope "${i.props.scope}"`)
}
}
if (!presets.instances.some((i) => i.id === 'configure' && i.props && i.props.configure === true)) fails.push('no "configure" preset')
return fails
}
const SCOPES = ['africa', 'asia', 'europe', 'latin-america-and-caribbean', 'middle-east', 'north-america-and-non-spanish-caribbean', 'oceania', 'other']
const fake = (attrs) => ({ getAttribute: (n) => (n in attrs ? attrs[n] : null), hasAttribute: (n) => n in attrs })
@ -39,6 +81,8 @@ function arms(embed) {
return fails
}
const SCOPES_BY_TYPE = await loadScopes()
if (process.argv.includes('--self-test')) {
const mutants = {
'drops columns': { ...real, sectionAttrs: (c) => real.sectionAttrs(c).map(({ columns, ...a }) => a) },
@ -46,6 +90,10 @@ if (process.argv.includes('--self-test')) {
'repeats a region under All': { ...real, sectionAttrs: (c) => { const r = real.sectionAttrs(c); return c.scope ? r : [...r, r[0]] } },
}
let ok = true
const stale = { ...PRESETS, instances: [...PRESETS.instances, { id: 'countries-atlantis', props: { type: 'country', scope: 'atlantis' } }] }
const pf = presetArm(stale, SCOPES_BY_TYPE)
console.log(` mutant "a preset for a region the type does not have": ${pf.length ? 'FAILS (good)' : 'PASSES (bad)'}`)
if (!pf.length) ok = false
for (const [name, m] of Object.entries(mutants)) {
const f = arms(m)
console.log(` mutant "${name}": ${f.length ? 'FAILS (good)' : 'PASSES (bad)'}`)
@ -54,6 +102,7 @@ if (process.argv.includes('--self-test')) {
console.log(ok ? 'POSTER-SECTIONS SELF-TEST: PASS — every mutant fails the check.' : 'POSTER-SECTIONS SELF-TEST: FAIL')
process.exit(ok ? 0 : 1)
}
const fails = arms(real)
const fails = [...arms(real), ...presetArm(PRESETS, SCOPES_BY_TYPE)]
const measured = Object.entries(SCOPES_BY_TYPE).filter(([, v]) => v).map(([k, v]) => `${k} ${v.length}`).join(', ')
if (fails.length) { console.log(`POSTER-SECTIONS: FAIL (${fails.length})`); fails.forEach((f) => console.log(` - ${f}`)); process.exit(1) }
console.log(`POSTER-SECTIONS: PASS — every panel choice (${SCOPES.length + 1} region choices × ${SUPPORTED_LOCALES.length} languages × open/collapsed × 5 per row) reads back through the element unchanged; All regions = one section per region; the snippet is the script + those tags.`)
console.log(`POSTER-SECTIONS: PASS — every panel choice (${SCOPES.length + 1} region choices × ${SUPPORTED_LOCALES.length} languages × open/collapsed × 5 per row) reads back through the element unchanged; All regions = one section per region; the snippet is the script + those tags; presets = each type's scopes() + all (${measured}).`)