diff --git a/plugin-designs/my-upg-100-list/data/visual-gate.mjs b/plugin-designs/my-upg-100-list/data/visual-gate.mjs index 77fd08a9c..722bbaf99 100644 --- a/plugin-designs/my-upg-100-list/data/visual-gate.mjs +++ b/plugin-designs/my-upg-100-list/data/visual-gate.mjs @@ -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