diff --git a/internal/check-poster-wall-host-seams.mjs b/internal/check-poster-wall-host-seams.mjs index f939410a3..1824f4f5e 100644 --- a/internal/check-poster-wall-host-seams.mjs +++ b/internal/check-poster-wall-host-seams.mjs @@ -40,6 +40,8 @@ * I (previewSrc, the generated image fails) one error shows the placeholder: there is no second base. * J (section heads) a section with people (total > 0) reads " · ", as it always has; * a section with none (a template gallery) reads "" alone: no 0 count, no " · ". + * L (the loader) a fixture component that imports a CHILD .vue by relative path loads and renders the child's text: + * PosterWall may render its sections through a child component, and the gate must still load it. * K (sectionItemCountKey) a section the callback names is counted in its own words and number ({ key, n }); * a section it answers null for keeps the default count. * embedBase.js is served to the gate as if from a BUILT tree (//.js), because @@ -47,12 +49,13 @@ * A vacuous run (no .tape-h found, or no items rendered) FAILS. */ import { readFileSync } from 'node:fs' -import { resolve, dirname, join } from 'node:path' +import { resolve, dirname, join, isAbsolute, relative, basename } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const WALL = join(ROOT, 'library/components/poster/PosterWall.vue') +const LOADER_FIXTURE = join(ROOT, 'internal/fixtures/poster-wall-host-seams/GateParent.vue') const failures = [] const fail = (m) => failures.push(m) @@ -89,7 +92,6 @@ const observers = [] } // ── PosterWall as a CLIENT component, loaded through Vite for the alias ──────────────────────── -const CLIENT_ID = '\0poster-wall-client.js' // PosterWall's fallback base comes from the bundle's own script URL (embedBase.js bundleTreeRoot), which // from source is not a built bundle URL, so the fallback would be empty and arm H vacuous. const GATE_TREE = 'https://gate.invalid/tree' @@ -101,17 +103,29 @@ const builtScriptUrl = { return code.replace(/const SCRIPT_URL = import\.meta\.url/, `const SCRIPT_URL = ${JSON.stringify(GATE_TREE + '/poster-design/poster-design.js')}`) }, } +// EVERY .vue on the way is compiled here as a CLIENT component: PosterWall, and any .vue it imports (a child component +// such as a section). Each keeps its REAL path plus a ?gate-client marker, so its own relative imports resolve as they do +// in the build. The gate runs Vite without the Vue plugin, so without this hook a child .vue import fails to LOAD and +// the gate goes red on correct code. Arm L proves the hook with a parent/child fixture. +const CLIENT = '?gate-client' const clientCompile = { - name: 'gate-poster-wall-client', - resolveId: (id) => (id === 'virtual:poster-wall-client' ? CLIENT_ID : null), + name: 'gate-client-sfc', + // 'pre': Vite's own resolver would otherwise resolve a relative './Child.vue' first and load it raw. + enforce: 'pre', + resolveId(source, importer) { + if (source === 'virtual:poster-wall-client') return WALL + CLIENT + if (source === 'virtual:gate-loader-fixture') return LOADER_FIXTURE + CLIENT + if (!/\.vue$/.test(source)) return null + const from = String(importer || '').split('?')[0] + return (isAbsolute(source) ? source : resolve(dirname(from), source)) + CLIENT + }, async load(id) { - if (id !== CLIENT_ID) return null + if (!id.endsWith(CLIENT)) return null + const file = id.slice(0, -CLIENT.length) const sfc = await import('@vue/compiler-sfc') - const { descriptor, errors } = sfc.parse(readFileSync(WALL, 'utf8'), { filename: WALL }) - if (errors.length) throw new Error(`PosterWall.vue did not parse: ${errors[0].message}`) - const out = sfc.compileScript(descriptor, { id: 'pw-gate', inlineTemplate: true }) - // Relative imports are relative to the .vue, not to this virtual id. - return out.content.replace(/from\s+(['"])\.\.\/\.\.\//g, `from $1${join(ROOT, 'library')}/`) + const { descriptor, errors } = sfc.parse(readFileSync(file, 'utf8'), { filename: file }) + if (errors.length) throw new Error(`${relative(ROOT, file)} did not parse: ${errors[0].message}`) + return sfc.compileScript(descriptor, { id: `gate-${basename(file, '.vue')}`, inlineTemplate: true }).content }, } @@ -374,6 +388,19 @@ try { if (!/\b2 lists?\b/.test(heads.Asia || '')) fail(`K: an unnamed section should keep the default count, got ${JSON.stringify(heads.Asia)}`) app.unmount() } + + // L: the gate loads a component that renders a child .vue (internal/fixtures/poster-wall-host-seams/) + { + const Parent = (await server.ssrLoadModule('virtual:gate-loader-fixture')).default + const root = node('root') + const app = createApp(Parent, { label: 'gate-loader-ok' }) + app.config.errorHandler = (e) => fail(`L: mount error: ${e?.message || e}`) + app.mount(root); await rc.nextTick() + let child = null + walk(root, (n) => { if (!child && hasClass(n, 'gate-child')) child = n }) + if (!child || textOf(child).trim() !== 'gate-loader-ok') fail(`L: the child .vue did not render through the loader (found ${child ? JSON.stringify(textOf(child)) : 'no .gate-child'})`) + app.unmount() + } } catch (e) { fail(`threw: ${e?.stack || e}`) } finally { @@ -385,5 +412,5 @@ if (failures.length) { console.error(`POSTER-WALL-HOST-SEAMS: FAIL (${failures.length})\n - ${failures.join('\n - ')}`) process.exitCode = 1 } else { - console.log(`POSTER-WALL-HOST-SEAMS: PASS (11 arms, ${buttonsSeen} stickers rendered; defaults unchanged: the Show-on-map toggle and the stored webp; headerAction emits, previewSrc is lazy, one at a time, cached and bounded)`) + console.log(`POSTER-WALL-HOST-SEAMS: PASS (12 arms, ${buttonsSeen} stickers rendered; defaults unchanged: the Show-on-map toggle and the stored webp; headerAction emits, previewSrc is lazy, one at a time, cached and bounded)`) } diff --git a/internal/fixtures/poster-wall-host-seams/GateChild.vue b/internal/fixtures/poster-wall-host-seams/GateChild.vue new file mode 100644 index 000000000..d48d95280 --- /dev/null +++ b/internal/fixtures/poster-wall-host-seams/GateChild.vue @@ -0,0 +1,7 @@ + + + diff --git a/internal/fixtures/poster-wall-host-seams/GateParent.vue b/internal/fixtures/poster-wall-host-seams/GateParent.vue new file mode 100644 index 000000000..5aaf8b172 --- /dev/null +++ b/internal/fixtures/poster-wall-host-seams/GateParent.vue @@ -0,0 +1,10 @@ + + + diff --git a/package.json b/package.json index fd8b4a2f3..05e4c24ce 100644 --- a/package.json +++ b/package.json @@ -683,7 +683,7 @@ "check:popup-pan-controls": "the popup auto-pan reserves the map's bottom control band, so a popup that clears the CLIP is also clear of the things that take the click. Measured at 390 on a 366x732 map: the pan fitted the popup inside the container, cut 0, and a real click on Full Profile still did nothing because the top element at the button's centre was div.maplibregl-ctrl-attrib-inner (and span.pin-hint-pill on the sibling pin) - a cut-height check cannot see this, because the cut really was zero. Attribution is a licence term of the basemap, so the popup may not be raised over it; the usable frame is shorter than the container and the fit test says so. Runs the SHIPPED panPlanFor, read out of the SFC with new Function (it is not exported), never a re-typed copy, and asserts where the popup ENDS UP rather than re-deriving the expected number from the same formula. Sweeps band heights 0-120px and every width the component renders at. Source-only, headless.", "check:control-inset": "bottomControlInset MEASURES the map's real bottom control band, so the pan reserves the band the controls actually occupy. check:popup-pan-controls proves the ARITHMETIC; this proves the band is real. If the inset silently returned 0 -- a selector typo, an engine renaming its control classes, a control moving outside the container -- panPlanFor would get bottomInset 0, behave exactly as it did before the fix, and check:popup-pan-controls would still pass every case, because its arithmetic is still correct: the bug back with both gates green. 8 cases against a real DOM, running the SHIPPED bottomControlInset read out of the SFC (it is not exported), never a copy: the band equals what the DOM independently says (not merely non-zero); no controls reserves 0; a zero-sized control reserves nothing; the taller of two corners wins; a wrapped attribution reserves more than one line; a control outside the container is ignored; a null container returns 0; and the MAPBOX corners count too, since the component takes a provider prop and every other case renders MapLibre names. Needs a browser: every assertion rests on getBoundingClientRect. 75 = no slot, never a pass.", "check:panel-sheets": "the shared panel geometry (library/components/poster/panelSheets.js) for posters printed on several sheets: 60x96 = 3 × 60×36 stacked and 66x66 = 2 × 36×66 side by side, 6 in joins, and a single sheet; the sheets tile the finished poster, glue slices tile and bleed slices repeat exactly the join, a bleed join prints a guide not a solid band, a screen preview always shows the red band, and the Bless 5x8ft takes its bands and marks from the module. Two-sided (4 planted defects). Headless.", - "check:poster-wall-host-seams": "PosterWall's two host seams, and that with neither set every wall behaves as before. headerAction: default null is the Show-on-map TOGGLE (aria-pressed, one window event per click, is-last, no emit); { labelKey, ariaKey } is a plain button that emits header-action with the item, no window event, no aria-pressed, and reads the label alone for an item with no count. previewSrc: default null is the stored previews/.webp and no IntersectionObserver; set, a card renders only once in view of the wall's own scroller, one render at a time, cached per page, and null / a throw / a timeout ends on the preview-unavailable placeholder. Image errors: a default wall retries the tree's own copy before the placeholder; a generated preview goes straight to it. Section heads: a section with people reads \" people groups · \" exactly as before; one with none reads its items alone. sectionItemCountKey lets a wall count one section in its own words ({ key, n }); unnamed sections keep the default. Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 17 mutants each FAIL.", + "check:poster-wall-host-seams": "PosterWall's two host seams, and that with neither set every wall behaves as before. headerAction: default null is the Show-on-map TOGGLE (aria-pressed, one window event per click, is-last, no emit); { labelKey, ariaKey } is a plain button that emits header-action with the item, no window event, no aria-pressed, and reads the label alone for an item with no count. previewSrc: default null is the stored previews/.webp and no IntersectionObserver; set, a card renders only once in view of the wall's own scroller, one render at a time, cached per page, and null / a throw / a timeout ends on the preview-unavailable placeholder. Image errors: a default wall retries the tree's own copy before the placeholder; a generated preview goes straight to it. Section heads: a section with people reads \" people groups · \" exactly as before; one with none reads its items alone. sectionItemCountKey lets a wall count one section in its own words ({ key, n }); unnamed sections keep the default. The gate client-compiles PosterWall and any .vue it imports (a child section component); arm L proves a child .vue loads and renders (fixtures in internal/fixtures/poster-wall-host-seams/). Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 19 mutants each FAIL.", "check:shell-header": "a header-off shell has ONE way to its tabs and ONE language menu, in the right place for the width: VerticalTabsLayout renders the phone bar (☰ + #phone-end) only when narrow AND header-off, #rail-end only when wide AND header-off, and no corner circle; ShellLanguageMenu's pick dispatches exactly one cancelable, composed, bubbling doxa-language-request (detail.language), does not switch when a host cancels, and writes ?lang= only on a tree-owned page; every switcher language has a catalogue and the current language is always listed. Headless (Vite SSR). Run the `check:shell-header:self-test` script for the NEGATIVE test (11 planted defects)", "check:side-lanes": "the large side-lane sheets (60x96, 66x66, 36x60) through the real poster-render.js, on a SEEDED worst case (855 groups, stacked duplicates, tight clusters, real and placeholder photos): every name drawn exactly once, leader crossings 0 (re-measured from the SVG, and equal to the renderer's own count), no overflow rows, photos only where a real one exists and the format allows them.", "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.",