PosterWall: a headerAction seam for the card's header sticker
The sticker on every poster card was hard-wired to "Show on map". A wall with
no map beside it (a gallery of poster templates) needs the same card with a
different action, and a fork of the card would drift from the wall.
headerAction null (the default) renders the existing toggle unchanged: the
same markup, aria-pressed, the is-last highlight and the window event, so
every current wall is untouched. { labelKey, ariaKey } renders a plain button
labelled from those keys that emits 'header-action' with the item and
dispatches no window event.
check:poster-wall-header-action (gates:pre) mounts the real PosterWall
headlessly on a recording renderer and calls the sticker's onClick in both
modes. Four mutants fail it: no seam, a new branch that also dispatches, a
default that emits, and aria-pressed on the new branch.
This commit is contained in:
parent
8d968ed0d0
commit
58242987c8
3 changed files with 218 additions and 2 deletions
200
internal/check-poster-wall-header-action.mjs
Normal file
200
internal/check-poster-wall-header-action.mjs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-poster-wall-header-action.mjs: PosterWall's header sticker does what its headerAction prop says.
|
||||
*
|
||||
* WHY. PosterWall's card header was hard-wired to "Show on map". A wall with no map beside it (the
|
||||
* Poster Design gallery) needs the same card with a different action, so the header became a seam:
|
||||
* headerAction null (the default) keeps the "Show on map" TOGGLE; { labelKey, ariaKey } makes it a
|
||||
* plain button that emits 'header-action'. Every existing wall passes nothing, so the default path is
|
||||
* the one that must not move, and the new path must not leak the old one's side effects (a window
|
||||
* event a map would act on, aria-pressed, the is-last highlight).
|
||||
*
|
||||
* HOW. No browser (fleet policy). PosterWall has no <style> block, so it is compiled here as a CLIENT
|
||||
* component (compiler-sfc, inline template) and loaded through Vite with the repo's @map alias. It is
|
||||
* mounted on a recording renderer: every element keeps its tag, props and children, so the gate reads
|
||||
* the rendered header buttons and CALLS their real onClick. Two arms, one card each:
|
||||
* A (default) one .tape-h, aria-pressed="false"; a click dispatches ONE window event named by
|
||||
* eventName with listId = the item id, sets aria-pressed="true" and is-last, and
|
||||
* emits nothing; a second click dispatches listId null (the toggle).
|
||||
* B (headerAction) one .tape-h, NO aria-pressed, label = t(labelKey), aria-label = t(ariaKey); a click
|
||||
* emits 'header-action' with the item, dispatches NO window event, sets no is-last.
|
||||
* 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 { 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 failures = []
|
||||
const fail = (m) => failures.push(m)
|
||||
|
||||
// ── the thinnest page PosterWall's setup touches ───────────────────────────────────────────────
|
||||
const dispatched = []
|
||||
{
|
||||
const noop = () => {}
|
||||
const el = () => ({ style: {}, dataset: {}, setAttribute: noop, getAttribute: () => null, removeAttribute: noop,
|
||||
appendChild: (c) => c, removeChild: noop, addEventListener: noop, removeEventListener: noop,
|
||||
querySelector: () => null, querySelectorAll: () => [], getRootNode() { return this },
|
||||
classList: { add: noop, remove: noop, toggle: noop, contains: () => false } })
|
||||
const html = el(); html.lang = 'en'
|
||||
Object.assign(globalThis, {
|
||||
window: globalThis, self: globalThis,
|
||||
document: Object.assign(el(), { documentElement: html, head: el(), body: el(), createElement: el, createTextNode: el,
|
||||
currentScript: null, readyState: 'complete', cookie: '', getElementById: () => null }),
|
||||
location: new URL(`https://gate.invalid/${TREE_NAME}/x/x/index.html?lang=en`),
|
||||
localStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
sessionStorage: { getItem: () => null, setItem: noop, removeItem: noop },
|
||||
matchMedia: () => ({ matches: false, addEventListener: noop, removeEventListener: noop, addListener: noop, removeListener: noop }),
|
||||
addEventListener: noop, removeEventListener: noop,
|
||||
dispatchEvent: (e) => { dispatched.push(e); return true },
|
||||
requestAnimationFrame: (f) => setTimeout(f, 0), cancelAnimationFrame: clearTimeout,
|
||||
ResizeObserver: class { observe() {} unobserve() {} disconnect() {} },
|
||||
IntersectionObserver: class { observe() {} unobserve() {} disconnect() {} },
|
||||
MutationObserver: class { observe() {} disconnect() {} },
|
||||
HTMLElement: class {}, ShadowRoot: globalThis.ShadowRoot || class ShadowRoot {},
|
||||
CustomEvent: class { constructor(type, o) { this.type = type; this.detail = o?.detail } },
|
||||
})
|
||||
if (!globalThis.navigator?.language) globalThis.navigator = { language: 'en', languages: ['en'], userAgent: 'gate' }
|
||||
}
|
||||
|
||||
// ── PosterWall as a CLIENT component, loaded through Vite for the alias ────────────────────────
|
||||
const CLIENT_ID = '\0poster-wall-client.js'
|
||||
const clientCompile = {
|
||||
name: 'gate-poster-wall-client',
|
||||
resolveId: (id) => (id === 'virtual:poster-wall-client' ? CLIENT_ID : null),
|
||||
async load(id) {
|
||||
if (id !== CLIENT_ID) return null
|
||||
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 { createServer } = await import('vite')
|
||||
const server = await createServer({
|
||||
root: ROOT, configFile: false, logLevel: 'error', appType: 'custom',
|
||||
server: { middlewareMode: true, hmr: false, ws: false, watch: null },
|
||||
plugins: [clientCompile],
|
||||
resolve: { alias: { '@map': join(ROOT, 'library') } },
|
||||
define: { __DOXA_TREE_NAME__: JSON.stringify(TREE_NAME) },
|
||||
})
|
||||
|
||||
// ── a renderer that records what it draws ──────────────────────────────────────────────────────
|
||||
// Recorded elements also answer the few DOM calls PosterWall's hooks make (v-show writes style.display,
|
||||
// the row fitter queries the wall, useShadowStyles finds its root node).
|
||||
const noopFn = () => {}
|
||||
function node(tag) {
|
||||
return { tag, props: {}, children: [], parent: null, text: '', style: { setProperty: noopFn, getPropertyValue: () => '' },
|
||||
dataset: {}, classList: { add: noopFn, remove: noopFn, toggle: noopFn, contains: () => false },
|
||||
querySelector: () => null, querySelectorAll: () => [], addEventListener: noopFn, removeEventListener: noopFn,
|
||||
setAttribute: noopFn, getAttribute: () => null, removeAttribute: noopFn, appendChild: (c) => c,
|
||||
getBoundingClientRect: () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 }),
|
||||
getRootNode() { return { host: null, adoptedStyleSheets: [], appendChild: (c) => c, querySelector: () => null } } }
|
||||
}
|
||||
function insert(child, parent, anchor) {
|
||||
if (child.parent) child.parent.children.splice(child.parent.children.indexOf(child), 1)
|
||||
child.parent = parent
|
||||
const i = anchor ? parent.children.indexOf(anchor) : -1
|
||||
if (i >= 0) parent.children.splice(i, 0, child); else parent.children.push(child)
|
||||
}
|
||||
const walk = (n, f) => { f(n); for (const c of n.children) walk(c, f) }
|
||||
const textOf = (n) => { let s = ''; walk(n, (x) => { if (x.tag === '#text') s += x.text }); return s }
|
||||
const hasClass = (n, c) => String(n.props.class || '').split(/\s+/).includes(c)
|
||||
const classStr = (v) => (typeof v === 'string' ? v : Array.isArray(v) ? v.map(classStr).join(' ')
|
||||
: v && typeof v === 'object' ? Object.keys(v).filter((k) => v[k]).join(' ') : '')
|
||||
|
||||
let buttonsSeen = 0
|
||||
try {
|
||||
const rc = await import('@vue/runtime-core')
|
||||
const { createApp } = rc.createRenderer({
|
||||
insert, remove: (c) => { if (c.parent) c.parent.children.splice(c.parent.children.indexOf(c), 1); c.parent = null },
|
||||
createElement: (tag) => node(tag), createText: (t) => Object.assign(node('#text'), { text: t }),
|
||||
createComment: () => node('#comment'), setText: (n, t) => { n.text = t },
|
||||
setElementText: (n, t) => { n.children = [Object.assign(node('#text'), { text: t, parent: n })] },
|
||||
parentNode: (n) => n.parent, nextSibling: (n) => { const p = n.parent; return p ? p.children[p.children.indexOf(n) + 1] || null : null },
|
||||
patchProp: (el, key, _prev, next) => { el.props[key] = key === 'class' ? classStr(next) : next },
|
||||
})
|
||||
const PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default
|
||||
const item = { id: 'gate-item', label: 'Gate Item', count: 7 }
|
||||
|
||||
async function mountWall(extra) {
|
||||
const emitted = []
|
||||
const root = node('root')
|
||||
const app = createApp(PosterWall, { items: [item], eventName: 'gate:show', instanceId: 'gate', ...extra,
|
||||
'onHeader-action': (i) => emitted.push(i) })
|
||||
app.config.errorHandler = (e) => fail(`mount error: ${e?.message || e}`)
|
||||
app.mount(root)
|
||||
await rc.nextTick()
|
||||
const tapes = []; walk(root, (n) => { if (n.tag === 'button' && hasClass(n, 'tape-h')) tapes.push(n) })
|
||||
const cards = []; walk(root, (n) => { if (n.tag === 'div' && hasClass(n, 'card')) cards.push(n) })
|
||||
buttonsSeen += tapes.length
|
||||
return { root, tapes, cards, emitted, app }
|
||||
}
|
||||
|
||||
// ── A: the default is the toggle it always was ─────────────────────────────────────────────
|
||||
{
|
||||
dispatched.length = 0
|
||||
const w = await mountWall({})
|
||||
if (w.cards.length !== 1) fail(`A: expected 1 card, rendered ${w.cards.length}`)
|
||||
if (w.tapes.length !== 1) fail(`A: expected 1 header sticker, found ${w.tapes.length}`)
|
||||
const b = w.tapes[0]
|
||||
if (b) {
|
||||
if (b.props['aria-pressed'] !== 'false') fail(`A: aria-pressed should start "false", got ${JSON.stringify(b.props['aria-pressed'])}`)
|
||||
if (typeof b.props.onClick !== 'function') fail('A: the sticker has no click handler')
|
||||
else {
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
const ev = dispatched.filter((e) => e.type === 'gate:show')
|
||||
if (ev.length !== 1 || ev[0].detail?.listId !== item.id) fail(`A: first click should dispatch gate:show with listId ${item.id}, got ${JSON.stringify(ev.map((e) => e.detail))}`)
|
||||
if (b.props['aria-pressed'] !== 'true') fail('A: aria-pressed should be "true" after the first click')
|
||||
if (!hasClass(w.cards[0], 'is-last')) fail('A: the card should carry is-last after the first click')
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
const ev2 = dispatched.filter((e) => e.type === 'gate:show')
|
||||
if (ev2.length !== 2 || ev2[1].detail?.listId !== null) fail('A: second click should dispatch listId null (the toggle)')
|
||||
}
|
||||
if (w.emitted.length) fail(`A: the default must emit nothing, emitted ${w.emitted.length}`)
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
|
||||
// ── B: headerAction swaps the action and drops the toggle's side effects ────────────────────
|
||||
{
|
||||
dispatched.length = 0
|
||||
const w = await mountWall({ headerAction: { labelKey: 'gate.fullDetails', ariaKey: 'gate.fullDetailsAria' } })
|
||||
if (w.tapes.length !== 1) fail(`B: expected 1 header sticker, found ${w.tapes.length}`)
|
||||
const b = w.tapes[0]
|
||||
if (b) {
|
||||
if ('aria-pressed' in b.props && b.props['aria-pressed'] != null) fail(`B: a plain button must not carry aria-pressed, got ${JSON.stringify(b.props['aria-pressed'])}`)
|
||||
const label = textOf(b)
|
||||
// No i18n is installed, so t() falls back to humanizing the key: the label must come from
|
||||
// labelKey, and must not be the map wording.
|
||||
if (!/full\s*details/i.test(label)) fail(`B: label should come from labelKey, got ${JSON.stringify(label)}`)
|
||||
if (/on\s*map/i.test(label)) fail(`B: label still says the map wording: ${JSON.stringify(label)}`)
|
||||
if (!/aria/i.test(String(b.props['aria-label']))) fail(`B: aria-label should come from ariaKey, got ${JSON.stringify(b.props['aria-label'])}`)
|
||||
if (typeof b.props.onClick !== 'function') fail('B: the sticker has no click handler')
|
||||
else {
|
||||
b.props.onClick(); await rc.nextTick()
|
||||
if (w.emitted.length !== 1 || w.emitted[0]?.id !== item.id) fail(`B: a click should emit header-action with the item once, emitted ${JSON.stringify(w.emitted)}`)
|
||||
if (dispatched.some((e) => e.type === 'gate:show')) fail('B: a click must dispatch NO show-on-map window event')
|
||||
if (hasClass(w.cards[0], 'is-last')) fail('B: the card must not take the is-last highlight')
|
||||
}
|
||||
}
|
||||
w.app.unmount()
|
||||
}
|
||||
} catch (e) {
|
||||
fail(`threw: ${e?.stack || e}`)
|
||||
} finally {
|
||||
await server.close()
|
||||
}
|
||||
|
||||
if (!buttonsSeen) fail('vacuous: no header sticker rendered in either arm')
|
||||
if (failures.length) {
|
||||
console.error(`POSTER-WALL-HEADER-ACTION: FAIL (${failures.length})\n - ${failures.join('\n - ')}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`POSTER-WALL-HEADER-ACTION: PASS (2 arms, ${buttonsSeen} stickers rendered; default toggle + window event unchanged, headerAction emits and dispatches nothing)`)
|
||||
}
|
||||
|
|
@ -185,7 +185,14 @@ const props = defineProps({
|
|||
// cards are thumbnails can set it lower so a phone keeps two per row; see the clamp below for why
|
||||
// that is safe now.
|
||||
minCardPx: { type: Number, default: 0 },
|
||||
// ★ WHAT THE CARD'S HEADER STICKER DOES. null (the default) is today's "Show on map" toggle,
|
||||
// unchanged for every wall that does not set it. A wall with no map beside it (a gallery of
|
||||
// templates) passes { labelKey, ariaKey } instead: the sticker then reads t(labelKey), is a plain
|
||||
// button rather than a toggle (no aria-pressed, no is-last highlight), and emits 'header-action'
|
||||
// with the item, dispatching no window event. ariaKey gets { label: item.label }.
|
||||
headerAction: { type: Object, default: null },
|
||||
})
|
||||
const emit = defineEmits(['header-action'])
|
||||
|
||||
// The live count, seeded from the prop. A ref rather than the prop directly, because the control
|
||||
// changes it and a prop is not ours to write.
|
||||
|
|
@ -1255,12 +1262,19 @@ useShadowStyles(
|
|||
|
||||
<!-- A TOGGLE (showOnMap clears the selection on a second press), so it says whether it is on:
|
||||
without aria-pressed a second press sounded identical to the first to a screen reader. -->
|
||||
<button type="button" class="tape tape-h" :aria-pressed="lastShown === item.id ? 'true' : 'false'"
|
||||
<button v-if="!headerAction" type="button" class="tape tape-h" :aria-pressed="lastShown === item.id ? 'true' : 'false'"
|
||||
:aria-label="t('buttons.showOnMapAria', { label: item.label })" @click="showOnMap(item)">
|
||||
<span class="hcount">{{ item.count }} {{ unit }}</span>
|
||||
<span class="hsep">·</span>
|
||||
<span class="hlabel">{{ t('buttons.showOnMap') }}</span>
|
||||
</button>
|
||||
<!-- headerAction set: a plain button that hands the item to the host (see the prop). -->
|
||||
<button v-else type="button" class="tape tape-h"
|
||||
:aria-label="t(headerAction.ariaKey || headerAction.labelKey, { label: item.label })" @click="emit('header-action', item)">
|
||||
<span class="hcount">{{ item.count }} {{ unit }}</span>
|
||||
<span class="hsep">·</span>
|
||||
<span class="hlabel">{{ t(headerAction.labelKey) }}</span>
|
||||
</button>
|
||||
|
||||
<div class="main">
|
||||
<!-- Unsupported comes FIRST: there is no poster to try to load. -->
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@
|
|||
"check:posters-exportable": "node internal/check-posters-exportable.mjs",
|
||||
"check:poster-prints": "node internal/check-poster-prints.mjs",
|
||||
"check:poster-raster": "node internal/check-poster-raster.mjs",
|
||||
"check:poster-wall-header-action": "node internal/check-poster-wall-header-action.mjs",
|
||||
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs",
|
||||
"check:prayer-walk-planner": "node internal/check-prayer-walk-planner.mjs",
|
||||
"check:preset-config-merge": "node internal/check-preset-config-merge.mjs",
|
||||
|
|
@ -342,7 +343,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:poster-wall-header-action check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -569,6 +570,7 @@
|
|||
"check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",
|
||||
"check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.",
|
||||
"check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
|
||||
"check:poster-wall-header-action": "PosterWall's card header sticker does what headerAction says. Default (null) is the Show-on-map TOGGLE every existing wall uses: aria-pressed, one window event per click (listId, then null), the is-last highlight, no emit. { labelKey, ariaKey } makes it a plain button: label and aria-label from those keys, one header-action emit with the item, NO window event, no aria-pressed, no is-last. Headless: PosterWall compiled as a client component and mounted on a recording renderer; the real onClick is called. Mutation-tested: no seam, a dispatching new branch, an emitting default, aria-pressed on the new branch each FAIL.",
|
||||
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
|
||||
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
|
||||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (go-int's upg-neighbor presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue