Everything the flights landed since the first snapshot: all 11 interface languages complete, the gospel-resources campaign (audio Scripture pins for 2,121 languages, the Deaf-gap completion, 3,603 resource links verified), the shared poster wall with print-in-place, prayer-walk sharing, and the 34-gate build pipeline. Tree rebuilt from source; SRI consistent; hygiene clean.
218 lines
14 KiB
JavaScript
218 lines
14 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 agent 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 agent 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'
|
|
// ★ ASK FOR index.html EXPLICITLY. These were bare-directory paths, which the OLD local server
|
|
// (python3 -m http.server) resolved to index.html while the LIVE static layer returns 404 — so this
|
|
// gate passed for a whole flight on URLs that do not work in production. `bun run serve` is now
|
|
// internal/serve-strict.mjs, which 404s a directory exactly as production does, and it caught these
|
|
// two the first time it ran. The manifest already ships 28/28 iframeUrl values in this form.
|
|
const SHELL = '/doxa-maps/plugin-studio/plugin-studio/index.html'
|
|
const LEAF = '/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html'
|
|
|
|
// 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) }
|
|
|
|
// The gesture's shape, named ONCE. The assertion below derives its threshold from these, so the
|
|
// check and the gesture can never drift apart — the failure mode that let `movedByMap < 200` sit
|
|
// beside a measured-broken value of 300 and pass.
|
|
const WHEEL_TICKS = 10
|
|
const WHEEL_DY = 300
|
|
const VIEWPORT_FOLD = 900 // the viewport height this gate opens the page at
|
|
|
|
async function wheel(page, x, y, dy, times = WHEEL_TICKS) {
|
|
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 agent 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, WHEEL_DY)
|
|
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, WHEEL_DY)
|
|
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 < VIEWPORT_FOLD
|
|
|
|
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)
|
|
|
|
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.')
|
|
|
|
// ★ ASSERT ON THE NUMBER WE ALREADY MEASURED. This gate computed `movedByMap` at step 2 — the
|
|
// gesture the agent actually makes — and then graded only `movedTotal`, which sums in the
|
|
// step-3 gesture BELOW the map. So it reported PASS (2611px total) while wheeling OVER the map
|
|
// moved just 300px and the agent still could not reach the grid: Mapbox scrollZoom was eating
|
|
// the wheel because syncScrollZoomToHost sampled the layout on three fixed timeouts and froze a
|
|
// stale answer. Passing on the total rewarded an escape hatch the user will never find — his
|
|
// cursor is over the map, which fills most of the band.
|
|
//
|
|
// The failure mode to remember: a gate that MEASURES the right thing and then ASSERTS on a
|
|
// different variable. Distinct from a gate that cannot fail; it fails honestly, just never on
|
|
// the condition it was built for. Audit every gate for values it computes but never checks.
|
|
// ★ THE THRESHOLD IS DERIVED, NOT GUESSED. This was `movedByMap < 200` — a number picked as a
|
|
// vague "did it move meaningfully" floor WITHOUT comparing it to the value already measured as
|
|
// broken, which was 300. So the assertion passed on the exact measurement that demonstrates the
|
|
// bug, and the gate reported PASS across three ticks while the agent still could not scroll.
|
|
//
|
|
// The gesture requests WHEEL_TICKS * WHEEL_DY px. A healthy scrollport delivers most of that
|
|
// (capped only by the scroll range actually available). Delivering one tick's worth means the
|
|
// other nine were swallowed by a nested scrollport under the cursor. Comparing DELIVERED against
|
|
// REQUESTED is self-calibrating: change the tick count or delta and the assertion still holds,
|
|
// where a hardcoded constant silently rots.
|
|
// ★ THE BASELINE IS MEASURED, NOT COMPUTED. A first attempt derived the expected travel as
|
|
// `before.top - VIEWPORT_FOLD`, which is 911-900 = 11px — a plausible-looking quantity that is
|
|
// simply the wrong one (it asks how far until the grid reaches the fold, not how much scroll the
|
|
// PANE has). With expected=11 the assertion passed on the broken 300px, repeating the exact
|
|
// mistake it was written to fix.
|
|
//
|
|
// The honest baseline is the SAME gesture performed somewhere it is known to work: step 3 wheels
|
|
// identically lower down the page and delivers `movedTotal - movedByMap`. Two identical gestures
|
|
// on one surface should move the page comparably; if the one over the dashboard moves far less,
|
|
// something under that cursor is eating it. No constant to rot, and it self-adjusts to whatever
|
|
// scroll range the layout actually has.
|
|
const requested = WHEEL_TICKS * WHEEL_DY
|
|
const movedByLower = movedTotal - movedByMap
|
|
if (movedByLower > 100 && movedByMap < movedByLower * 0.5) {
|
|
fail('a nested scrollport ATE THE WHEEL — the SAME ' + requested + 'px gesture moved the grid ' +
|
|
movedByLower + 'px lower down the page but only ' + movedByMap + 'px over the dashboard' +
|
|
'. The grid is technically reachable but NOT by ' +
|
|
'the gesture the agent makes.\n' +
|
|
' ★ Do NOT assume the map. MEASURED 2026-09-22: the culprit was .rd-grid — a virtualized table ' +
|
|
'with scrollHeight 54834 in a ~207px window — consuming the wheel by normal scroll-chaining, ' +
|
|
'while the Mapbox canvas sat ABOVE the cursor and was never involved. Fix was ' +
|
|
'overscroll-behavior:contain on .rd-grid. Identify the element UNDER THE CURSOR before blaming ' +
|
|
'the one nearby: compare each scrollport\'s scrollTop before and after the gesture.')
|
|
}
|
|
|
|
console.log('SHELL-SCROLL: PASS — the grid is reachable by wheel in the cap-5 shell.')
|
|
|
|
// ── 5. THE OTHER SIDE OF THE FIX: a MAP tab must NOT gain a scrollbar. ─────────────────────
|
|
// Making the pane scrollable is only correct if it stays a NO-OP for the ~19 fixed-viewport map
|
|
// tabs. It did not, at first: a hosted map ships min-height:100dvh (resolved against the 900px
|
|
// VIEWPORT) beside height:100% (resolved against the 769px PANE), so the floor made it 131px
|
|
// taller than its container and the new overflow:auto faithfully grew a scrollbar on it. That
|
|
// regression was invisible to every assertion above — the grid test passed while a map tab broke —
|
|
// so it is asserted here, in the same gate, rather than left to be re-found by hand.
|
|
const page2 = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
|
await page2.goto(BASE + SHELL, { waitUntil: 'load' })
|
|
await page2.waitForTimeout(6000)
|
|
const mapTab = await page2.evaluate(`(() => { ${WALK}
|
|
for (const t of __walk('.vtl-tab')) {
|
|
if ((t.textContent || '').toLowerCase().includes('doxa-research-map')) { t.click(); return t.textContent.trim() }
|
|
}
|
|
return null
|
|
})()`)
|
|
if (mapTab) {
|
|
await page2.waitForTimeout(9000)
|
|
const panes = await page2.evaluate(`(() => { ${WALK}
|
|
return __walk('.ps-pane, .vtl-host').filter(e => e.getBoundingClientRect().height > 0)
|
|
.map(e => ({ sh: e.scrollHeight, ch: e.clientHeight, over: e.scrollHeight - e.clientHeight }))
|
|
})()`)
|
|
const leaking = panes.filter(p => p.over > 4)
|
|
console.log(' map tab "' + mapTab + '" pane overflow: ' + JSON.stringify(panes))
|
|
await browser.close()
|
|
if (leaking.length) fail('a MAP tab grew a scrollbar (' + leaking[0].over + 'px overflow: ' + leaking[0].sh + '/' + leaking[0].ch + '). The pane scroll must be a NO-OP for fixed-viewport elements — check the dvh floor neutraliser.')
|
|
console.log('MAP-TAB-NO-SCROLLBAR: PASS — fixed-viewport map tabs gained no scrollbar.')
|
|
} else {
|
|
await browser.close()
|
|
console.log('MAP-TAB-NO-SCROLLBAR: SKIPPED — no doxa-research-map tab in this shell.')
|
|
}
|
|
}
|
|
run().catch(e => { console.error('SHELL-SCROLL: FAIL — ' + e.message); process.exit(1) })
|