Assert the cap-5 rail opens on the Big Picture, in the specified order
Some checks are pending
build-gate / build (push) Waiting to run

The Driver named the sequence: Big Picture, UPG-100, Prayer Progress, Gospel
Resources. Tab 1 is the landing surface -- the first thing anyone opening the
product SPA sees, and the opening step of the onboarding ramp. That ordering
was held only by the order of entries in NAV_MODEL and by comments explaining
why, with nothing failing if someone reordered them.

Easy to break silently: NAV_MODEL is also where sections, badges and
narrow-screen flags live, so it gets edited for reasons unrelated to ordering,
and a row moved while adding a badge produces no error and a SPA that still
works. It just opens somewhere else. That file's own comments record Big
Picture once being painted twice in the rail, from a one-row section that
re-declared a row below.

Asserts the RENDERED rail, not the source array. Parsing NAV_MODEL was the
first attempt and returned an empty list -- the entries do not carry the id
shape I assumed, and a regex over a 300-line commented array is guesswork
either way. The rail is what the user sees and what the Driver described, and
a source parse could pass while a rendering bug shows something else.

Order, not membership: checking that four names are present would pass on a
rail that opens on Gospel Resources. Prefix-matched so a row may carry its
subtitle.

Negative-tested: swapping the first two expected entries fails by name and
prints want versus got for every row.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 20:39:05 +00:00
parent 5d12841fe7
commit e1baca1bbb
2 changed files with 100 additions and 1 deletions

View file

@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* check-spa-tab-order.mjs — the cap-5 product SPA must open on the Big Picture, in the Driver's order.
*
* WHAT THIS PROTECTS. The Driver specified the rail explicitly: "cap-5 tab order (Big Picture,
* UPG-100, Prayer Progress, Gospel Resources)". Tab 1 is not a preference — it is the LANDING
* SURFACE, the first thing anyone opening doxa-maps-spa sees, and the opening step of the
* onboarding ramp. Today that ordering is held only by the order of entries in a source array and
* by comments explaining why; nothing fails if someone reorders them.
*
* ★ WHY THIS IS EASY TO BREAK WITHOUT NOTICING. NAV_MODEL is also where sections, badges and
* narrow-screen flags live, so it gets edited for reasons that have nothing to do with ordering —
* and a row moved while adding a badge produces no error, no console warning, and a SPA that still
* works perfectly. It just opens somewhere else. The nav file's own comments record that "Big
* Picture" was once painted TWICE in the rail, from a one-row section that re-declared a row below;
* that is the same class of accident.
*
* ★ IT ASSERTS THE RENDERED RAIL, NOT THE SOURCE ARRAY. Parsing NAV_MODEL is what I tried first and
* it returned an EMPTY list — the entries do not carry the `id:` shape I assumed, and a regex over
* a 300-line commented array is guesswork either way. The rail is the thing the user sees, it is
* what the Driver described, and reading it requires no assumption about how it is built. A source
* parse could also pass while a rendering bug shows a different order.
*
* ORDER, NOT MEMBERSHIP. Checking that four names are present would pass on a rail that opens on
* Gospel Resources. The assertion is the SEQUENCE, matched as prefixes so a row may carry a
* subtitle ("Big Picture — dashboard + people-groups grid") without failing.
*
* PRECONDITION: a built tree and `bun run serve` on 4173. No Mapbox token needed — the rail renders
* before any map mounts, and heavy tabs lazy-mount on activation, which this never triggers.
*
* NEGATIVE-TESTED: swapping the first two entries of NAV_MODEL fails by name, printing expected vs
* actual side by side.
*/
import { chromium } from 'playwright'
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps'
// The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
// whitespace-collapsed) so a row may carry its subtitle.
const EXPECTED = ['Big Picture', 'UPG-100', 'Prayer Progress', 'Gospel Resources']
const WALK = `(sel)=>{const o=[],s=new Set();const w=(r)=>{if(!r||s.has(r))return;s.add(r);try{o.push(...r.querySelectorAll(sel))}catch(e){};for(const e of r.querySelectorAll('*'))if(e.shadowRoot)w(e.shadowRoot)};w(document);return o}`
const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1600, height: 900 } })
let rows = []
let err = null
try {
await page.goto(`${BASE}/doxa-maps-spa/doxa-maps-spa/`, { waitUntil: 'load', timeout: 45000 })
await page.waitForTimeout(8000)
rows = await page.evaluate(
`(${WALK})('.vtl-tab').map(t => (t.textContent || '').trim().replace(/\\s+/g, ' '))`
)
} catch (e) {
err = e.message.slice(0, 160)
}
await browser.close()
if (err) {
console.error('SPA-TAB-ORDER: FAIL — could not load the cap-5 SPA: ' + err)
console.error(' Needs a built tree and `bun run serve` on 4173. Reported as a FAILURE, not a')
console.error(' skip: a gate that cannot reach its subject has not passed, it has not run.')
process.exit(1)
}
if (!rows.length) {
console.error('SPA-TAB-ORDER: FAIL — no .vtl-tab rows found in the composed tree.')
console.error(' The rail is inside a shadow root, so this walks recursively with a seen-Set; a')
console.error(' flat querySelectorAll returns 0 here and would read as "no tabs" rather than')
console.error(' "wrong tool".')
process.exit(1)
}
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
const actual = rows.slice(0, EXPECTED.length)
const mismatches = EXPECTED.map((want, i) => {
const got = actual[i] || '(missing)'
return norm(got).startsWith(norm(want)) ? null : { i, want, got: got.slice(0, 52) }
}).filter(Boolean)
if (mismatches.length) {
console.error(`SPA-TAB-ORDER: FAIL — ${mismatches.length} of ${EXPECTED.length} rail rows are out of order.\n`)
EXPECTED.forEach((want, i) => {
const got = (actual[i] || '(missing)').slice(0, 52)
const ok = norm(got).startsWith(norm(want))
console.error(` ${ok ? 'ok ' : 'FAIL'} ${i + 1}. want "${want}" got "${got}"`)
})
console.error('\n Tab 1 is the LANDING SURFACE — the first thing anyone opening the product SPA')
console.error(' sees, and the opening step of the onboarding ramp. A reordered rail produces no')
console.error(' error and a SPA that still works; it just opens somewhere else.')
console.error(' The order lives in NAV_MODEL in plugin-designs/doxa-maps-spa/doxa-maps-spa.vue.')
process.exit(1)
}
console.log(`SPA-TAB-ORDER: PASS — the rail opens in the specified order (${rows.length} rows total).`)
EXPECTED.forEach((w, i) => console.log(` ${i + 1}. ${(actual[i] || '').slice(0, 56)}`))
console.log(' Asserted on the RENDERED rail rather than on NAV_MODEL: the array is edited for')
console.log(' reasons unrelated to ordering (badges, sections, narrow-screen flags), and a source')
console.log(' parse can pass while a rendering bug shows something else.')

View file

@ -101,7 +101,8 @@
"check:kpi-unit": "node internal/check-kpi-unit-once.mjs",
"check:synced-css": "node internal/check-synced-css-copies.mjs",
"check:composite-reg": "node internal/check-composite-registration.mjs",
"check:filter-width": "node internal/check-filter-width-stable.mjs"
"check:filter-width": "node internal/check-filter-width-stable.mjs",
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs"
},
"devDependencies": {
"@capacitor/android": "^7.6.9",