About thirty browser gates could pass while describing another worktree's build.
This machine runs several checkouts and a default port belongs to whichever
server started first — measured 2026-09-24, :4173 was another worktree's, and an
hour of probing reported confidently on code nobody here had touched. check:sri
cannot see it: a stale tree and its stale leaf agree perfectly.
Twenty gates now call requireThisTree() right after they resolve their target and
exit 75 on a mismatch — EX_TEMPFAIL, the code this repo already uses for "not
measured", which the wave runner reports as ⊘ rather than ✗. Exiting 1 would turn
a mis-pointed probe into a red product gate, which is the failure this whole
family of checks exists to prevent.
★ NARROWER THAN THE PROBLEM STATEMENT, ON PURPOSE. Three classes do NOT get it,
because there it would be wrong rather than missing:
self-serving gates start their own server on an OS-assigned port
file:// gates read this checkout directly — no server to be wrong about
remote targets verify-map-paint defaults to the LIVE site, and "does
production match what we built" is a deploy question asked
deliberately. MEASURED mid-rollout: it reported NOT
MEASURED against the live host, which was the guard being
wrong, not the gate. It now enforces on localhost only.
TWO THINGS THE ROLLOUT TAUGHT, both caught by running rather than reading:
· node --check passes a file that will die on ReferenceError. Adding a ROOT
constant to sixteen gates that do not import resolve/dirname/fileURLToPath
parsed perfectly and crashed on first run — the same shape that broke
check-locale-reachable twice. The imports are added under aliased names so
they cannot collide with a gate's own.
· Five gates take their URL from argv with no default and are left for a
per-file pass rather than patched blind.
CONTROLS: against another worktree's server a guarded gate prints NOT MEASURED
naming the mismatch and exits 75; against this checkout's server it runs and
reports normally; against the live site it proceeds.
102 lines
5.7 KiB
JavaScript
102 lines
5.7 KiB
JavaScript
// check-mfe-two-tab — a two-tab MFE must actually MOUNT BOTH PANES and switch.
|
|
//
|
|
// The failure this catches is BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT: every layer
|
|
// inside these composites is position:absolute/height:100%, so the whole chain
|
|
// resolves to ZERO unless :host is sized. The header still renders and the tab
|
|
// strip still paints — the element looks alive and every pane is 0px tall. That is
|
|
// the "elements are not embedding" report, and a DOM-presence check passes it.
|
|
// So this asserts MEASURED pane height, not mere existence.
|
|
//
|
|
// It also asserts ONE share affordance (two on one surface shipped once as
|
|
// BUG-UPG100-DOUBLE-SHARE) and that clicking tab 2 actually changes the pressed
|
|
// tab — a strip that paints but does not switch is the other silent half.
|
|
//
|
|
// Usage: node internal/check-mfe-two-tab.mjs <url> [expectedTabCount]
|
|
import { chromium } from 'playwright'
|
|
import { requireThisTree } from './_serves-this-tree.mjs'
|
|
import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path'
|
|
import { fileURLToPath as _fileURLToPath } from 'node:url'
|
|
|
|
const URL = process.argv[2]
|
|
const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..')
|
|
|
|
// ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port
|
|
// belongs to whichever server started first — measured 2026-09-24 on :4173, where browser gates
|
|
// reported confidently about a build nobody here had touched. 75 (EX_TEMPFAIL), not 1: the wrong
|
|
// tree is a precondition, not a product defect, and the wave runner reports 75 as ⊘ rather than ✗.
|
|
// A file:// target passes straight through — there is no server there to be wrong about.
|
|
await requireThisTree(URL, ROOT, 'MFE-TWO-TAB')
|
|
if (!URL) { console.error('usage: check-mfe-two-tab.mjs <url> [tabs]'); process.exit(2) }
|
|
const WANT_TABS = Number(process.argv[3] || 2)
|
|
|
|
const b = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
|
const pg = await b.newPage({ viewport: { width: 1280, height: 900 } })
|
|
const errs = []
|
|
pg.on('pageerror', (e) => errs.push(String(e).slice(0, 140)))
|
|
await pg.goto(URL, { waitUntil: 'load', timeout: 45000 })
|
|
await pg.waitForTimeout(9000)
|
|
|
|
// Everything lives in a shadow root, so reach through it explicitly.
|
|
const r = await pg.evaluate(() => {
|
|
// Find the host by SHADOW ROOT, not by rendered content: an element that
|
|
// collapsed to 0px still HAS its shadow root, and reporting "not found" would
|
|
// misdiagnose the zero-height collapse as a missing element. Measured — removing
|
|
// :host produced exactly that misleading message.
|
|
const host = [...document.querySelectorAll('*')].find((e) => e.shadowRoot
|
|
&& (e.shadowRoot.querySelector('.viewsel, .pane-col, [role="tab"]')
|
|
|| /mfe/i.test(e.tagName)))
|
|
if (!host) return { error: 'no custom element with a shadow root found on this page' }
|
|
const sr = host.shadowRoot
|
|
// ⚠ Select the TABS, not their container. '.viewsel .seg' matches the tablist
|
|
// WRAPPER as well as its buttons, which counted 3 tabs for a 2-tab strip and
|
|
// concatenated their text ("Country PostersMap"). role=tab is the semantic
|
|
// anchor ViewSelect already sets, so it cannot drift with the class names.
|
|
const tabs = [...sr.querySelectorAll('[role="tab"]')]
|
|
.map((t) => (t.textContent || '').trim()).filter(Boolean)
|
|
const panes = [...sr.querySelectorAll('.pane')].map((p) => {
|
|
const box = p.getBoundingClientRect()
|
|
const mounted = !!p.querySelector('.pane-mount > *')
|
|
return { h: Math.round(box.height), w: Math.round(box.width), mounted }
|
|
})
|
|
const shares = sr.querySelectorAll('[aria-label*="hare"], .share-btn, .sharebtn').length
|
|
const hostBox = host.getBoundingClientRect()
|
|
return { tabs, panes, shares, hostH: Math.round(hostBox.height) }
|
|
})
|
|
await b.close()
|
|
|
|
if (r.error) { console.error(`MFE-TWO-TAB: FAIL — ${r.error}`); process.exit(1) }
|
|
|
|
const fails = []
|
|
if (r.tabs.length !== WANT_TABS) fails.push(`${r.tabs.length} tab(s) in the strip, expected ${WANT_TABS}`)
|
|
// ★ THE HOST HEIGHT IS THE LOAD-BEARING ASSERTION, not the pane heights.
|
|
// MEASURED: squashing the host to 0px leaves its panes still reporting 843px,
|
|
// because they are position:absolute and escape the collapsed box. So a
|
|
// pane-height check alone does NOT catch BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT —
|
|
// the panes look fine while the element occupies no space and the reader sees
|
|
// nothing. Only the host measurement reveals it.
|
|
if (r.hostH < 100) {
|
|
fails.push(`the host element measured ${r.hostH}px tall — :host is not sized, so the `
|
|
+ 'element occupies no space even though its panes still measure normally')
|
|
}
|
|
if (r.panes.length !== WANT_TABS) fails.push(`${r.panes.length} pane(s) mounted, expected ${WANT_TABS}`)
|
|
for (const [i, p] of r.panes.entries()) {
|
|
if (!p.mounted) fails.push(`pane ${i} has no child element mounted`)
|
|
// Kept as a secondary signal only — see the note on hostH above for why this
|
|
// one cannot be trusted to catch the collapse on its own.
|
|
if (p.h < 100) fails.push(`pane ${i} measured ${p.h}px tall`)
|
|
}
|
|
if (r.shares > 1) fails.push(`${r.shares} share affordances on one surface — exactly one belongs here`)
|
|
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
|
|
|
|
console.log(` host height : ${r.hostH}px`)
|
|
console.log(` tabs : ${r.tabs.join(' | ') || '(none)'}`)
|
|
console.log(` panes : ${r.panes.map((p) => `${p.w}x${p.h}${p.mounted ? '' : ' UNMOUNTED'}`).join(', ')}`)
|
|
console.log(` share buttons : ${r.shares}`)
|
|
console.log(` page errors : ${errs.length}`)
|
|
|
|
if (fails.length) {
|
|
console.error(`\nMFE-TWO-TAB: FAIL — ${fails.length} problem(s)`)
|
|
for (const f of fails) console.error(' - ' + f)
|
|
process.exit(1)
|
|
}
|
|
console.log(`\nMFE-TWO-TAB: PASS — ${WANT_TABS} tabs, both panes mounted and sized, one share affordance.`)
|