UPG-100 gate: find controls by class, assert the localized title separately
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

The (G-rail) probe queried `.upg-rail-batch[title="Batch Select"]`. Once the
title becomes a bound `:title="BATCH_SELECT_TITLE"` (studio L53) that selector
matches under `en` and nowhere else, and the gate reports "button not in the
toolbar" — a PRODUCT failure for what is really a broken query. The class is
already proven unique by the railBtns === 1 assertion in the same probe, so it
is the identity; the title is still checked, as the property that actually
matters for a11y: NON-EMPTY, which holds in every locale. That also catches a
regression the equality missed — a :title binding resolving to '' or undefined.

The (4) map-controls check had the same coupling, three times. Those titles are
ALREADY bound (ZoomInButton.vue: :title="t('buttons.zoomIn')"), but the page is
pinned to en-US at newPage({ locale }) — L4/LESSON-03 — so they are not failing
today. The expected strings now come from the same i18n source the component
reads (library/i18n/locales/en/buttons.json) rather than being hardcoded, so the
gate survives being run in another locale. Verified the three keys resolve to
the literals they replace, so this asserts exactly what it asserted before.

The theme button cannot use that shim: its title is DYNAMIC ("Switch to dark" /
"Switch to light"), so there is no single key to resolve, and it stays a prefix
match on English prose. Its assertion moves from `> 0` to `=== 1` (studio's
call): that does not remove the coupling, but it stops 0 and 2 both collapsing
into "not > 0", and the failure line already prints the count.

Those four .mcb buttons share one class with no stable per-control hook, and
aria-label mirrors the localized title, so the title is the only identity
available to a query. The durable fix is data-control="zoom-in|zoom-out|
fullscreen|theme|location" on library/components/map-controls/MapControlButton
— studio's under 5B. They are riding it on the next landing that rebuilds the
17 consumers anyway rather than spending a rebuild on it alone; this shim comes
out when that lands.
This commit is contained in:
Doxa Maps 2026-09-28 10:48:23 +00:00
parent fc6dd4ac19
commit ad822c0f31

View file

@ -638,13 +638,38 @@ async function main() {
await clearFilter(page);
// ── (4) controls present in DOM (zoom, theme, fullscreen) ──────────────────
const zoomIn = await page.locator(`${EL} button.mcb[title="Zoom in"]`).count();
const zoomOut = await page.locator(`${EL} button.mcb[title="Zoom out"]`).count();
const fullscreen = await page.locator(`${EL} button.mcb[title="Toggle fullscreen"]`).count();
// ★ THESE TITLES ARE LOCALIZED (ZoomInButton.vue binds :title="t('buttons.zoomIn')",
// and MapControlButton mirrors title into aria-label). The page is pinned to en-US
// at newPage({ locale }) above (L4/LESSON-03), so matching the English string is not
// failing TODAY — but it is the same coupling that broke the (G-rail) probe when
// Batch Select became a bound title, so the expected strings are resolved from the
// SAME i18n source the component reads instead of being hardcoded here. If this gate
// is ever run in another locale, it keeps working.
//
// These four buttons all share class .mcb with NO stable per-control hook (no
// data-control, and aria-label is localized too), so — unlike .upg-rail-batch — the
// class alone cannot distinguish them and the title is the only identity available.
// The durable fix is a data-* attribute in library/components/map-controls/, which is
// studio's under 5B; sent to them. Until then this is the locale-correct version.
// __dirname is plugin-designs/my-upg-100-list/data → three up is the repo root.
const B = JSON.parse(readFileSync(
join(__dirname, '..', '..', '..', 'library/i18n/locales/en/buttons.json'), 'utf8'));
const titleSel = (s) => `${EL} button.mcb[title="${String(s).replace(/"/g, '\\"')}"]`;
const zoomIn = await page.locator(titleSel(B.zoomIn)).count();
const zoomOut = await page.locator(titleSel(B.zoomOut)).count();
const fullscreen = await page.locator(titleSel(B.toggleFullscreen)).count();
// The theme button is the WEAKEST of the four and cannot use the shim above: its
// title is DYNAMIC ("Switch to dark" / "Switch to light"), so there is no single key
// to resolve — hence the prefix match. A prefix match on English prose degrades to 0
// under localization, and `> 0` collapses that into the same false as "the button is
// missing". Asserting == 1 (studio's suggestion) does not remove the coupling, but it
// makes the COUNT mean something: 0 and 2 become distinguishable failures instead of
// both reading as "not > 0". The real fix is data-control="theme" (studio, riding the
// next landing that rebuilds consumers anyway).
const theme = await page.locator(`${EL} button.mcb[title^="Switch to"]`).count();
const mcbCount = await page.locator(`${EL} button.mcb`).count();
check('(4) map controls present in DOM (zoom + theme + fullscreen)',
zoomIn > 0 && zoomOut > 0 && fullscreen > 0 && theme > 0,
zoomIn > 0 && zoomOut > 0 && fullscreen > 0 && theme === 1,
`zoomIn=${zoomIn} zoomOut=${zoomOut} fullscreen=${fullscreen} theme=${theme} (.mcb total=${mcbCount})`);
// ── (6) slice 13: pin click → PeopleGroupDetail side panel opens ───────────
@ -1157,14 +1182,26 @@ async function main() {
await page.waitForTimeout(300);
// ── (G-rail) the Batch toggle renders INSIDE the MapToolbar control group ──
// Query the rail (.map-toolbar) for the profile batch button (title="Batch
// Select"); assert it is a CHILD of the toolbar (not the .upg-hud), and that
// a real DOM click on it flips __UPG_SELECT__.batchMode. NO-IMAGES: DOM + handle.
// Query the rail (.map-toolbar) for the profile batch button; assert it is a
// CHILD of the toolbar (not the .upg-hud), and that a real DOM click on it
// flips __UPG_SELECT__.batchMode. NO-IMAGES: DOM + handle.
//
// ★ FIND BY CLASS, ASSERT THE TITLE SEPARATELY — the title is LOCALIZED.
// This used to be `.upg-rail-batch[title="Batch Select"]`, which pinned the gate
// to one English string: once the title became `:title="BATCH_SELECT_TITLE"`
// (studio L53) the selector matched in `en` and NOWHERE ELSE, and the gate would
// report "button not in the toolbar" — a product failure — for what is really a
// broken query. `.upg-rail-batch` is already proven unique by railBtns === 1
// below, so the class alone is the identity. The title is still checked, as the
// property that actually matters for a11y: it must be NON-EMPTY, which holds in
// every locale. That also catches a regression the old equality missed — a
// :title binding resolving to '' or undefined.
const railProbe = await page.evaluate((elName) => {
const host = document.querySelector(elName);
const root = host && host.shadowRoot ? host.shadowRoot : document;
const toolbar = root.querySelector('.map-toolbar');
const btn = toolbar ? toolbar.querySelector('.upg-rail-batch[title="Batch Select"]') : null;
const btn = toolbar ? toolbar.querySelector('.upg-rail-batch') : null;
const title = btn ? (btn.getAttribute('title') || '') : null;
const inHud = !!(root.querySelector('.upg-hud .upg-rail-batch')); // must be FALSE
// R1.4 (L4): the rail button is the ONLY batch control — the old HUD
// .upg-batch-btn must not exist anywhere, and exactly ONE rail button.
@ -1172,7 +1209,7 @@ async function main() {
const railBtns = root.querySelectorAll('.upg-rail-batch').length; // must be 1
const before = window.__UPG_SELECT__ ? !!window.__UPG_SELECT__.batchMode : null;
if (btn) btn.click(); // engage batch mode via the RAIL button
return { hasToolbar: !!toolbar, inToolbar: !!btn, inHud, hudBtns, railBtns, before };
return { hasToolbar: !!toolbar, inToolbar: !!btn, title, inHud, hudBtns, railBtns, before };
}, EL);
await page.waitForTimeout(250);
// R1.4 de-glitch of the GATE itself: aria-pressed is a Vue-rendered binding —
@ -1189,8 +1226,12 @@ async function main() {
check('(G-rail) toolA R1.3/R1.4: the SINGLE Batch toggle lives INSIDE the map-controls rail (no HUD button anywhere) AND drives batchMode',
railProbe.hasToolbar && railProbe.inToolbar && railProbe.inHud === false
&& railProbe.hudBtns === 0 && railProbe.railBtns === 1
// Titled, in WHATEVER locale is under test — asserted as non-empty, never
// against an English literal. A returned value nobody asserts on is decoration.
&& typeof railProbe.title === 'string' && railProbe.title.trim() !== ''
&& railProbe.before === false && railOn === true && railAria === 'true',
`inToolbar=${railProbe.inToolbar} inHud(mustFalse)=${railProbe.inHud} hudBtns(must0)=${railProbe.hudBtns} ` +
`inToolbar=${railProbe.inToolbar} title(mustBeNonEmpty)=${JSON.stringify(railProbe.title)} ` +
`inHud(mustFalse)=${railProbe.inHud} hudBtns(must0)=${railProbe.hudBtns} ` +
`railBtns(must1)=${railProbe.railBtns} before=${railProbe.before} afterClick=${railOn} aria-pressed=${railAria}`);
// ── (G-2row) batch ON → select 2 legend rows → both selected, union highlight