PosterWall headerAction: a countless item's sticker reads the label alone

A template card (Poster Design) carries no people-group count, and the new
branch printed a bare ' UPG ·' before its label. The count and separator now
render only when item.count is set, in the headerAction branch only; the
default toggle is unchanged. check:poster-wall-header-action gains arm C for
it, which fails with the guard removed.
This commit is contained in:
Doxa Maps 2026-09-30 21:11:33 +00:00
parent 58242987c8
commit b3d6db3c59
3 changed files with 23 additions and 9 deletions

View file

@ -16,8 +16,9 @@
* 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.
* B (headerAction) one .tape-h, NO aria-pressed, label = "7 UPG ·" + t(labelKey), aria-label = t(ariaKey);
* a click emits 'header-action' with the item, dispatches NO window event, sets no is-last.
* C (headerAction, an item with no count) the sticker reads t(labelKey) alone: no "UPG", no "·".
* A vacuous run (no .tape-h found, or no items rendered) FAILS.
*/
import { readFileSync } from 'node:fs'
@ -122,10 +123,10 @@ try {
const PosterWall = (await server.ssrLoadModule('virtual:poster-wall-client')).default
const item = { id: 'gate-item', label: 'Gate Item', count: 7 }
async function mountWall(extra) {
async function mountWall(extra, items = [item]) {
const emitted = []
const root = node('root')
const app = createApp(PosterWall, { items: [item], eventName: 'gate:show', instanceId: 'gate', ...extra,
const app = createApp(PosterWall, { items, 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)
@ -174,6 +175,7 @@ try {
// 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 (!/7\s*UPG/.test(label)) fail(`B: an item WITH a count keeps its "7 UPG ·" prefix, got ${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 {
@ -185,6 +187,15 @@ try {
}
w.app.unmount()
}
// ── C: a template card has no count, so its sticker is the label alone ─────────────────────
{
const w = await mountWall({ headerAction: { labelKey: 'gate.fullDetails' } }, [{ id: 'gate-template', label: 'Gate Template' }])
const label = w.tapes[0] ? textOf(w.tapes[0]) : ''
if (w.tapes.length !== 1) fail(`C: expected 1 header sticker, found ${w.tapes.length}`)
else if (/UPG|·|undefined/.test(label) || !/full\s*details/i.test(label)) fail(`C: a countless item should read the label alone, got ${JSON.stringify(label)}`)
w.app.unmount()
}
} catch (e) {
fail(`threw: ${e?.stack || e}`)
} finally {
@ -196,5 +207,5 @@ 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)`)
console.log(`POSTER-WALL-HEADER-ACTION: PASS (3 arms, ${buttonsSeen} stickers rendered; default toggle + window event unchanged, headerAction emits and dispatches nothing)`)
}

View file

@ -189,7 +189,8 @@ const props = defineProps({
// 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 }.
// with the item, dispatching no window event. ariaKey gets { label: item.label }. An item with no
// count (a template, not a list of people) shows the label alone, without the "N UPG ·" prefix.
headerAction: { type: Object, default: null },
})
const emit = defineEmits(['header-action'])
@ -1271,8 +1272,10 @@ useShadowStyles(
<!-- 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>
<template v-if="item.count != null">
<span class="hcount">{{ item.count }} {{ unit }}</span>
<span class="hsep">·</span>
</template>
<span class="hlabel">{{ t(headerAction.labelKey) }}</span>
</button>

View file

@ -570,7 +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: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. A third arm: an item with no count reads the label alone (no \"N UPG ·\"). Mutation-tested: no seam, a dispatching new branch, an emitting default, aria-pressed on the new branch, an unguarded count prefix 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.",