From 60606945459144bd93f1a4e04dabb205843b6c33 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 08:32:35 +0000 Subject: [PATCH] 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. --- package.json | 4 +- .../scripts/check-poster-sections.mjs | 57 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e5da98893..e67c689c3 100644 --- a/package.json +++ b/package.json @@ -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 (): 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 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 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 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." }, diff --git a/plugin-designs/poster-sections/scripts/check-poster-sections.mjs b/plugin-designs/poster-sections/scripts/check-poster-sections.mjs index 739710e3d..daad644c1 100644 --- a/plugin-designs/poster-sections/scripts/check-poster-sections.mjs +++ b/plugin-designs/poster-sections/scripts/check-poster-sections.mjs @@ -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 "-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}).`)