doxa-maps/internal/verify-shell-scroll.mjs
doxa-maps-88655c70 b2fc0e4dda
Some checks are pending
build-gate / build (push) Waiting to run
Make the shell-scroll gate runnable, and catch orphan gates
verify-shell-scroll.mjs was written and negative-tested, then left with no
package.json entry and no importer, so nothing could ever run it again. It
also defaulted to localhost:8899 -- an ad-hoc port from an earlier session --
so it failed with ERR_CONNECTION_REFUSED for anyone else and read as a broken
gate rather than a missing server. Now check:shell-scroll, defaulting to
BASE_URL or 127.0.0.1:4173 like every other browser gate, and listed in
verify-browser-all's GATES array.

check:no-orphan-gates fails any gate-shaped file in internal/ that nothing can
invoke. The repo carded this once already; it recurs because writing a gate
and registering one are separate acts and only the first feels like work.

Not every unreferenced file is a lost gate, so the rule is not "everything
must be wired": theme-flash-probe is a dated diagnostic for two fixed bugs,
worth keeping as a reproduction recipe. It now carries an @one-shot tag saying
so, which makes the author state which kind of thing they wrote.

Two self-referential defects in the first version of that gate, both from
reading evidence out of documentation:

- it cited theme-flash-probe.mjs in its own header as an example, then grepped
  for importers, found itself, and declared the orphan reachable. The gate
  written to catch orphans made an orphan pass by mentioning it. A gate must
  never be its own witness, so it now excludes its own file.
- it matched @one-shot anywhere in prose, including its own paragraph
  explaining the tag, and reported "1 tagged" when nothing had been tagged.
  The tag must now be a marker at line start.

Negative-tested: removing check:shell-scroll from package.json fails by name.

The scroll fix itself is NOT done. contain:layout and .vtl-host overflow-y are
confirmed in the built bundle, and a real wheel gesture still moves the grid
0px with 2586px mounted and unreachable. The gate is what caught that; the
diff looked correct.
2026-09-22 17:55:43 +00:00

130 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
/**
* verify-shell-scroll.mjs — THE GATE FOR BUG-STILL-CANNOT-SCROLL-DOWN-TO-THE-GRID.
*
* ★ WHAT IT ASSERTS, and why each assertion exists rather than the obvious one.
*
* The obvious assertion — `scrollHeight > innerHeight` — is a FALSE PASS and was used twice to
* close this bug while the Driver could still not scroll. In the cap-5 shell the document measures
* scrollHeight 900 === clientHeight 900 while the composite element measures 3380/769: the
* overflow is real, but it belongs to an element with `overflow:visible` that no scroller owns.
* Geometry said "tall"; the user's wheel moved nothing.
*
* So this gate asserts the ONLY thing the Driver actually cares about: that a REAL WHEEL GESTURE
* over the map area moves the grid up the screen and brings it into view. It measures the grid's
* position before and after, in the SHELL, which is the surface he uses — not the standalone leaf,
* which passed all along and is why the two previous fixes looked green.
*
* ★ NEGATIVE-TESTED. Reverting the fix (restoring `.ps-pane`/`.vtl-host` to `position:absolute;
* inset:0` with no overflow, and `.vtl-content` to `contain:layout paint`) makes this exit 1 with
* "grid did not move". A gate nobody has broken is a gate nobody knows works.
*
* Usage: node internal/verify-shell-scroll.mjs [baseUrl]
* (defaults to http://localhost:8899; serve ./public)
*/
import { chromium } from 'playwright'
// Default matches every other browser gate in this repo: `bun run serve` on loopback 4173.
// It was `localhost:8899` — one of the ad-hoc ports a previous session happened to have running,
// which meant the gate failed with ERR_CONNECTION_REFUSED for anyone else and looked like a broken
// gate rather than a missing server. Prefer BASE_URL (env) over a positional argument, because
// `bun run <script> <arg>` cannot forward arguments past an inline `#` comment — see
// check:script-flags. The positional form is kept as a fallback for direct `node` invocation.
const BASE = process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173'
const SHELL = '/doxa-maps/plugin-studio/plugin-studio/'
const LEAF = '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/'
// Shadow DOM DOES NOT PIERCE. A single-level querySelector lies about every composite, so every
// lookup here is a recursive walk with a seen-Set, re-injected after each interaction (a page
// navigation or re-render drops anything we defined earlier).
const WALK = `
function __walk(sel) {
const seen = new Set(); const out = [];
const visit = (root) => {
if (!root || seen.has(root)) return; seen.add(root);
let els = []; try { els = Array.from(root.querySelectorAll('*')) } catch (e) { return }
for (const el of els) {
try { if (el.matches && el.matches(sel)) out.push(el) } catch (e) {}
if (el.shadowRoot) visit(el.shadowRoot);
}
};
visit(document); return out;
}
// ★ ANCHOR ON .pgg-root, NOT the <people-groups-grid> TAG. In the cap-4 composite the grid is
// IMPORTED as a Vue child component and compiled into one bundle — so there is no custom-element
// tag to find, and a tag-based lookup reports "not mounted" for a grid that is right there. That
// wrong answer cost a run of this very gate. .pgg-root is the grid's own root div and exists in
// both shapes (standalone element and baked-in child).
function __gridTop() {
const g = __walk('.pgg-root')[0];
if (!g) return null;
const r = g.getBoundingClientRect();
return { top: Math.round(r.top), h: Math.round(r.height) };
}
`
const fail = (m) => { console.error('SHELL-SCROLL: FAIL — ' + m); process.exit(1) }
async function wheel(page, x, y, dy, times = 10) {
await page.mouse.move(x, y)
for (let i = 0; i < times; i++) { await page.mouse.wheel(0, dy); await page.waitForTimeout(80) }
await page.waitForTimeout(500)
}
const run = async () => {
const browser = await chromium.launch({
args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--use-angle=swiftshader']
})
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
await page.goto(BASE + SHELL, { waitUntil: 'load' })
await page.waitForTimeout(6000)
// HEAVY TABS LAZY-MOUNT ON ACTIVATION. Before this click the composite does not exist and every
// query returns 0 — which reads as "no bug" if you stop here. This is the step both previous
// investigations skipped.
const tab = await page.evaluate(`(() => { ${WALK}
for (const t of __walk('.vtl-tab')) {
if ((t.textContent || '').toLowerCase().includes('dashboard-plus-grid')) { t.click(); return t.textContent.trim() }
}
return null
})()`)
if (!tab) { await browser.close(); fail('could not find the dashboard-plus-grid tab in the shell rail') }
await page.waitForTimeout(9000)
// 1. The grid must be MOUNTED. If it is not rendered at all, "cannot scroll to it" has a
// completely different cause and this gate must say so rather than blaming the scroll.
const before = await page.evaluate(`(() => { ${WALK} return __gridTop() })()`)
if (!before) { await browser.close(); fail('.pgg-root is NOT MOUNTED in the shell — different bug, not a scroll bug') }
if (before.h < 400) { await browser.close(); fail('.pgg-root mounted but collapsed (h=' + before.h + ') — not a scroll bug') }
// 2. A REAL WHEEL GESTURE over the MAP AREA must move the grid up the screen. This is the
// gesture the Driver makes, over the surface he makes it on. Mapbox scrollZoom eating the
// wheel would show up here as no movement.
await wheel(page, 850, 420, 300)
const afterMap = await page.evaluate(`(() => { ${WALK} return __gridTop() })()`)
const movedByMap = before.top - afterMap.top
// 3. ...and a gesture BELOW the map must too (in case the map swallows its own area).
await wheel(page, 850, 800, 300)
const afterLow = await page.evaluate(`(() => { ${WALK} return __gridTop() })()`)
const movedTotal = before.top - afterLow.top
// 4. THE REAL QUESTION: is the grid actually ON SCREEN now? A grid that moved 50px but still
// sits below the fold is still unreachable. Require its top inside the viewport.
const onScreen = afterLow.top < 900
console.log('SHELL-SCROLL measurements (cap-5 shell, tab="' + tab + '"):')
console.log(' grid mounted : h=' + before.h + 'px, top=' + before.top)
console.log(' wheel over MAP area : grid moved ' + movedByMap + 'px (top ' + before.top + ' -> ' + afterMap.top + ')')
console.log(' wheel BELOW map : grid moved ' + movedTotal + 'px total (top -> ' + afterLow.top + ')')
console.log(' grid on screen : ' + onScreen)
await browser.close()
if (movedTotal < 200) fail('grid did not move — a real wheel gesture in the SHELL scrolled ' + movedTotal + 'px. The grid is mounted at h=' + before.h + 'px and is UNREACHABLE.')
if (!onScreen) fail('grid moved ' + movedTotal + 'px but its top is still at ' + afterLow.top + ' (below the 900px fold) — still not reachable.')
console.log('SHELL-SCROLL: PASS — the grid is reachable by wheel in the cap-5 shell.')
}
run().catch(e => { console.error('SHELL-SCROLL: FAIL — ' + e.message); process.exit(1) })