Add 5-capability verification gate: proves every capability level mounts, renders and is error-free

This commit is contained in:
doxa-maps-88655c70 2026-09-20 04:44:07 +00:00
parent e4be3ee4ee
commit 79d46126de
2 changed files with 84 additions and 1 deletions

View file

@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* verify-5-capabilities.mjs — prove ALL FIVE capability levels actually render.
*
* ★ WHY: the card BUILD-DXMAPS-ALL-5-CAPS-WORKING-LOW-BUG was marked done with NO end-to-end check.
* "Done is a passing test, not a field value" — this is that test. First run (2026-09-20) came back
* 3/5: caps 1-3 clean, caps 4-5 failing on ONE shared bug (vue-i18n code 27), which is exactly the
* kind of thing a field value hides and a sweep surfaces.
*
* ★ THE MODEL, grounded not remembered: capability = 5 - PROFILE_TIER, where PROFILE_TIER lives in
* internal/plugin-gallery/discover.mjs:
* nested-mfe-spa 0 · nested-elements-mfe 1 · monolithic 2 · parameterized 3 · single 4
* One representative bundle per level. If a bundle's declared type changes, update REPS here.
*
* ★ TWO MEASUREMENT TRAPS THIS ENCODES (both cost real debugging time):
* - SHADOW DOM DOES NOT PIERCE. A cap-4/cap-5 composite mounts CHILD elements with their OWN shadow
* roots, so a single-level querySelector reports a working canvas as absent. Hence the recursive
* deep() walk, and hence "a canvas that HOLDS a GL context" rather than "a canvas exists".
* - shadowRoot.textContent INCLUDES the injected <style>, so regexing it matches CSS. Clone the
* children, drop STYLE nodes, then read visible copy.
*
* WHAT COUNTS AS PASS: the element mounts, has real height (>=200px), renders real text (>40 chars),
* and produces ZERO console errors and ZERO HTTP>=400. Rendering without errors is the bar, because a
* console error is what masks the NEXT real failure.
*
* USAGE
* bun run verify:5-caps # against the live catalog
* BASE=http://127.0.0.1:8080/doxa-maps/ node internal/verify-5-capabilities.mjs # a local serve
* Exits non-zero if any level fails, so it can gate a release.
*/
import { chromium } from 'playwright'
const GL=['--use-gl=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl','--no-sandbox']
const B=(process.env.BASE || 'https://nuxtbase.app/plugin-assets/doxa-maps/').replace(/\/?$/,'/')
// capability = 5 - PROFILE_TIER (discover.mjs). One representative per level.
const REPS=[
['cap-1 single', 'doxa-vision-card/doxa-vision-card/index.html', 'single'],
['cap-2 parameterized', 'people-groups-grid/people-groups-grid/index.html', 'preset-element'],
['cap-3 monolithic', 'doxa-research-map/research-map/index.html', 'monolithic'],
['cap-4 nested-elements', 'dashboard-plus-grid/dashboard-plus-grid/index.html','nested-elements-mfe'],
['cap-5 nested-mfe-spa', 'doxa-maps-spa/doxa-maps-spa/index.html', 'nested-mfe-spa'],
]
const b=await chromium.launch({headless:true,args:GL})
let bad=0
for(const [name,path,type] of REPS){
const page=await b.newPage({viewport:{width:1440,height:900}})
const errs=[]; page.on('console',m=>{if(m.type()==='error')errs.push(m.text().slice(0,60))})
const http=[]; page.on('response',r=>{if(r.status()>=400)http.push(r.status()+' '+r.url().slice(-44))})
try{ await page.goto(B+path+'?cb=5c',{waitUntil:'networkidle',timeout:90000}) }
catch(e){ console.log(` FAIL ${name} — nav`); bad++; await page.close(); continue }
await page.waitForTimeout(15000)
const r=await page.evaluate(()=>{
// shadow DOM does NOT pierce — walk nested roots
const deep=(root,sel,out=[])=>{out.push(...root.querySelectorAll(sel))
for(const el of root.querySelectorAll('*')) if(el.shadowRoot) deep(el.shadowRoot,sel,out); return out}
const host=[...document.querySelectorAll('*')].find(e=>e.tagName.includes('-')&&e.shadowRoot)
if(!host) return {mounted:false}
const sr=host.shadowRoot, bb=host.getBoundingClientRect()
// visible copy only (textContent includes injected <style>)
const clone=document.createElement('div')
;[...sr.children].forEach(c=>{if(c.tagName!=='STYLE')clone.appendChild(c.cloneNode(true))})
clone.querySelectorAll('style').forEach(s=>s.remove())
const txt=clone.textContent.replace(/\s+/g,' ').trim()
const cv=deep(sr,'canvas').find(c=>c.getContext('webgl2')||c.getContext('webgl'))
return { mounted:true, tag:host.tagName.toLowerCase(),
size:Math.round(bb.width)+'x'+Math.round(bb.height),
h:Math.round(bb.height),
nested: [...sr.querySelectorAll('*')].filter(e=>e.tagName.includes('-')).length,
glCanvas: !!cv, textLen: txt.length, head: txt.slice(0,58) }
})
const ok = r.mounted && r.h>=200 && r.textLen>40 && errs.length===0 && http.length===0
console.log(` ${ok?'PASS':'FAIL'} ${name} [${type}]`)
console.log(` ${r.mounted?`${r.tag} ${r.size} nested=${r.nested} gl=${r.glCanvas} text=${r.textLen}`:'DID NOT MOUNT'}`)
if(r.head) console.log(` "${r.head}"`)
if(errs.length) console.log(` console errors: ${errs.length} -> ${errs[0]}`)
if(http.length) console.log(` HTTP>=400: ${http.length} -> ${http[0]}`)
if(!ok) bad++
await page.close()
}
console.log(`\n5-CAPS: ${bad?`FAIL — ${bad} of ${REPS.length}`:`PASS — all ${REPS.length} capability levels mount, render and are error-free`}`)
await b.close()
if (bad) process.exit(1)

View file

@ -46,7 +46,8 @@
"check:template-literal": "node internal/template-literal-check.mjs",
"verify:map-paint": "node internal/verify-map-paint.mjs",
"check:sri": "node internal/sri-consistency-check.mjs",
"check:settled": "node internal/settled-tree-gate.mjs"
"check:settled": "node internal/settled-tree-gate.mjs",
"verify:5-caps": "node internal/verify-5-capabilities.mjs"
},
"devDependencies": {
"@capacitor/android": "7",