test(mfe): a two-tab element must mount both panes AND occupy space
Some checks are pending
build-gate / build (push) Waiting to run

Gates the composite MFEs at the rendered layer. The failure it exists for is
BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT: every layer inside these elements is
position:absolute/height:100%, so the chain resolves to ZERO unless :host is
sized. The header renders, the tab strip paints, the panes mount — the element
looks alive and occupies no space. A DOM-presence check passes it.

★ THE HOST HEIGHT IS THE LOAD-BEARING ASSERTION, and finding that out cost a
wrong first attempt. I wrote the gate to check PANE heights, which is the
obvious reading of "zero-height collapse" — then measured it: squashing the host
to 0px leaves its panes still reporting 843px, because they are absolutely
positioned and escape the collapsed box. A pane-height check alone does NOT
catch the bug. Only measuring the host does. The pane check stays as a secondary
signal with a note saying why it cannot be trusted alone.

Also asserts exactly one share affordance (two on one surface shipped once as
BUG-UPG100-DOUBLE-SHARE) and that the strip has the expected number of tabs.

Tabs are selected by role=tab, not by class. '.viewsel .seg' matched the tablist
WRAPPER as well as its buttons, reporting 3 tabs for a 2-tab strip with
concatenated text ("Country PostersMap") — the gate finding a bug in itself.

Passing on all three composites today: upg-100-mfe, regions-mfe and the new
country-posters-mfe. Confirmed to FAIL before being accepted.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 04:31:15 +00:00
parent 7edcda368b
commit c9ed3b4e9a
2 changed files with 92 additions and 0 deletions

View file

@ -0,0 +1,91 @@
// 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'
const URL = process.argv[2]
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.`)

View file

@ -57,6 +57,7 @@
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
"check:card-deck": "node internal/check-card-deck.mjs",
"check:poster-raster": "node internal/check-poster-raster.mjs",
"check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",
"check:share-policy": "node internal/check-share-policy.mjs",
"check:zone-census": "node internal/check-zone-census.mjs",
"check:print-geometry": "node internal/check-print-geometry.mjs",