Merge main into bundler-2/work
This commit is contained in:
commit
2590fe5eb1
6 changed files with 79 additions and 1657 deletions
|
|
@ -18,6 +18,12 @@
|
|||
* not one baked into the bundle). A MapLibre-only plugin takes no token: reported n/a
|
||||
* D its tab inside the Research Gallery
|
||||
* B and C test exactly what a partner pastes, so a broken snippet fails here too.
|
||||
* Every surface with a map also gets the PAINT arm (after gospel's check-gospel-map-paints):
|
||||
* distinct colours read off the map's own canvas (floor 40) and the map's height over the plugin
|
||||
* element's (floor 70%). Feature counts cannot see a white rectangle; these two can. Each has its
|
||||
* own self-test poison and must be the arm that catches it: 'blank' (every layer's opacity 0,
|
||||
* every feature still "drawn") and 'collapse' (the container squeezed to 300px).
|
||||
* Drawn counts are read SETTLED: equal on two consecutive polls, never the first non-zero read.
|
||||
* A surface passes when:
|
||||
* - the element mounted with content (its shadow tree has real nodes),
|
||||
* - 0 page errors and 0 failed requests (4xx/5xx; Mapbox telemetry and favicons excluded),
|
||||
|
|
@ -59,6 +65,10 @@ const ONLY = arg('--only', '') ? arg('--only', '').split(',') : null
|
|||
const SURFACES = arg('--surfaces', 'A,B,C,D').split(',')
|
||||
const SELF_TEST = !argv.includes('--no-self-test')
|
||||
const WAIT_MS = Number(process.env.SHELF_WAIT_MS || 75000)
|
||||
// the PAINT arm (after gospel's check-gospel-map-paints, 0574fe7f): every assertion above counts
|
||||
// FEATURES, and queryRenderedFeatures answers happily over a white rectangle. These two look.
|
||||
const MIN_COLOURS = Number(process.env.MIN_MAP_COLOURS || 40)
|
||||
const MIN_FILL = Number(process.env.MIN_MAP_FILL || 0.7)
|
||||
|
||||
const { chromium } = await import(resolve(ROOT, 'node_modules/playwright/index.mjs'))
|
||||
const manifest = JSON.parse(readFileSync(resolve(ROOT, `public/${TREE_NAME}/manifest.json`), 'utf8'))
|
||||
|
|
@ -174,6 +184,48 @@ const PROBE = (tag) => `(() => {
|
|||
return { own, sized, host: !!host, busy, maps }
|
||||
})()`.replace('__TAG__', tag)
|
||||
|
||||
// ── PAINT: does each map put pixels on the screen, and does it fill its plugin? ───────────────
|
||||
// COLOURS are read off the map's OWN WebGL canvas inside its 'render' event (the drawing buffer is
|
||||
// still intact there), so a legend or panel floating over the map cannot inflate the count, and
|
||||
// no per-plugin clip rectangle is needed. A basemap with land, water, borders and data has hundreds
|
||||
// of colours; a blank map has one. FILL is the map's height over the PLUGIN ELEMENT's height, not
|
||||
// canvas over container: in gospel's incident a stylesheet collapsed the container and the canvas
|
||||
// followed it, so canvas/container stayed 1.0 over a 300px strip in a 900px box.
|
||||
// POISON (self-test only): 'blank' zeroes every layer's opacity (the map is white and every
|
||||
// feature still counts as drawn); 'collapse' squeezes the container to 300px (the real incident).
|
||||
const PAINT = (tag, poison) => `(async () => {
|
||||
const acc = []; const w = (r) => { for (const el of r.querySelectorAll('*')) { acc.push(el); if (el.shadowRoot) w(el.shadowRoot) } }; w(document)
|
||||
const host = acc.find(e => e.tagName === ${JSON.stringify('__TAG__')}.toUpperCase())
|
||||
const hb = host ? host.getBoundingClientRect() : null
|
||||
const maps = (window.__shelfMaps || []).filter(m => { try { return m.getContainer() && m.getContainer().isConnected } catch (_) { return false } })
|
||||
const poison = ${JSON.stringify('__POISON__')}
|
||||
const OPACITY = { background: ['background-opacity'], fill: ['fill-opacity'], line: ['line-opacity'], circle: ['circle-opacity', 'circle-stroke-opacity'], symbol: ['text-opacity', 'icon-opacity'], raster: ['raster-opacity'], 'fill-extrusion': ['fill-extrusion-opacity'], heatmap: ['heatmap-opacity'] }
|
||||
for (const m of maps) {
|
||||
if (poison === 'blank') for (const l of (m.getStyle().layers || [])) for (const k of (OPACITY[l.type] || [])) { try { m.setPaintProperty(l.id, k, 0) } catch (_) {} }
|
||||
if (poison === 'collapse') { const c = m.getContainer(); c.style.position = 'relative'; c.style.inset = 'auto'; c.style.height = '300px'; c.style.flex = 'none'; m.resize() }
|
||||
}
|
||||
if (poison) await new Promise(r => setTimeout(r, 1200))
|
||||
const out = []
|
||||
for (const m of maps) {
|
||||
const cb = m.getContainer().getBoundingClientRect()
|
||||
const colours = await new Promise(res => {
|
||||
const t = setTimeout(() => res(-1), 8000)
|
||||
m.once('render', () => { clearTimeout(t); try {
|
||||
const cv = m.getCanvas(), W = Math.min(480, cv.width), H = Math.max(1, Math.round(cv.height * W / cv.width))
|
||||
const c = document.createElement('canvas'); c.width = W; c.height = H; const x = c.getContext('2d'); x.drawImage(cv, 0, 0, W, H)
|
||||
const d = x.getImageData(0, 0, W, H).data, seen = new Set()
|
||||
for (let i = 0; i < d.length; i += 4) seen.add(((d[i] >> 3) << 11) | ((d[i + 1] >> 3) << 6) | ((d[i + 2] >> 3) << 1) | (d[i + 3] > 127 ? 1 : 0))
|
||||
res(seen.size)
|
||||
} catch (_) { res(-2) } })
|
||||
m.triggerRepaint()
|
||||
})
|
||||
let drawn = 0
|
||||
try { const st = m.getStyle(), srcs = st.sources || {}, geo = Object.keys(srcs).filter(k => srcs[k].type === 'geojson'); const layers = (st.layers || []).filter(l => geo.includes(l.source)).map(l => l.id); if (layers.length) drawn = m.queryRenderedFeatures({ layers }).length } catch (_) {}
|
||||
out.push({ colours, fill: hb && hb.height ? cb.height / hb.height : 0, drawn })
|
||||
}
|
||||
return out
|
||||
})()`.replace('__TAG__', tag).replace('__POISON__', poison || '')
|
||||
|
||||
const b = await chromium.launch({ headless: true, args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] })
|
||||
|
||||
async function measure(url, s, poisonBundle, opts = {}) {
|
||||
|
|
@ -193,16 +245,21 @@ async function measure(url, s, poisonBundle, opts = {}) {
|
|||
const host0 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
|
||||
// B: the plugin lives in the iframe whose document comes from the tree
|
||||
const ctx = () => (opts.frame ? p.frames().find(f => f.url().startsWith(BASE)) : p.mainFrame()) || p.mainFrame()
|
||||
let st = null
|
||||
let st = null, prevDrawn = ''
|
||||
const t0 = Date.now()
|
||||
// provenance needs one tile request, not a drawn map: a fake token cannot draw anything
|
||||
while (Date.now() - t0 < (opts.provenanceOnly ? Math.min(WAIT_MS, 25000) : WAIT_MS)) {
|
||||
await p.waitForTimeout(2500)
|
||||
try { st = await ctx().evaluate(PROBE(s.tag)) } catch (_) { st = null; continue }
|
||||
const mapsReady = st.maps.every(m => m.loaded && m.drawn > 0)
|
||||
// SETTLED, not first-non-zero: the drawn counts must hold across two polls, or a count read
|
||||
// mid-fit (1752 of 2107 while tiles were still arriving) passes as the answer
|
||||
const drawnNow = st.maps.map(m => m.drawn).join(','), steady = drawnNow === prevDrawn; prevDrawn = drawnNow
|
||||
const mapsReady = st.maps.every(m => m.loaded && m.drawn > 0) && steady
|
||||
if (opts.provenanceOnly ? tokens.has(FAKE_TK) : (st.own > 8 && st.sized && st.busy === 0 && mapsReady && Date.now() - t0 > 6000)) break
|
||||
}
|
||||
const host1 = opts.frame ? await p.evaluate(() => ({ title: document.title, href: location.href, y: scrollY })) : null
|
||||
let paint = []
|
||||
if (!opts.provenanceOnly && st && st.maps.length) { try { paint = await ctx().evaluate(PAINT(s.tag, opts.poisonPaint)) } catch (e) { paint = [{ colours: -3, fill: 0, drawn: 0 }] } }
|
||||
await p.close()
|
||||
if (opts.provenanceOnly) {
|
||||
// C provenance: with a FAKE host token the tiles cannot load, so only one thing is asserted:
|
||||
|
|
@ -215,11 +272,16 @@ async function measure(url, s, poisonBundle, opts = {}) {
|
|||
else if (st.own <= 8 || !st.sized) why.push(`<${s.tag}> mounted empty (${st.own} node(s)${st.sized ? '' : ', not sized'})`)
|
||||
if (st && st.busy) why.push(`${st.busy} loading label(s) left`)
|
||||
if (st) st.maps.forEach((m, i) => { if (!m.loaded) why.push(`map ${i + 1}: 'load' never fired`); else if (!m.drawn) why.push(`map ${i + 1}: no own data drawn (${m.geo} geojson source(s))`) })
|
||||
paint.forEach((q, i) => {
|
||||
if (q.colours < 0) why.push(`map ${i + 1}: could not read its pixels (${q.colours})`)
|
||||
else if (q.colours < MIN_COLOURS) why.push(`map ${i + 1}: only ${q.colours} colour(s) on its canvas, a blank map (floor ${MIN_COLOURS})${q.drawn ? `, while ${q.drawn} features still count as drawn` : ''}`)
|
||||
if (q.fill < MIN_FILL) why.push(`map ${i + 1}: fills ${Math.round(q.fill * 100)}% of <${s.tag}>'s height, collapsed (floor ${Math.round(MIN_FILL * 100)}%)`)
|
||||
})
|
||||
if (host0 && host1 && (host0.title !== host1.title || host0.href !== host1.href || host0.y !== host1.y)) why.push(`the host page changed (title/URL/scroll): ${JSON.stringify(host0)} -> ${JSON.stringify(host1)}`)
|
||||
if (opts.noMapbox && mapboxReqs.length) why.push(`${mapboxReqs.length} api.mapbox.com request(s) under maplibre, e.g. ${mapboxReqs[0]}`)
|
||||
if (errs.length) why.push(`page error: ${errs[0]}`)
|
||||
if (fails.length) why.push(`failed request: ${fails[0]}`)
|
||||
return { pass: !why.length, why, maps: st ? st.maps.length : 0, mapboxMaps: st ? st.maps.filter(m => m.mapbox).length : 0 }
|
||||
return { pass: !why.length, why, maps: st ? st.maps.length : 0, paint, drawn: st ? st.maps.map(m => m.drawn) : [], mapboxMaps: st ? st.maps.filter(m => m.mapbox).length : 0 }
|
||||
}
|
||||
|
||||
const leaf = (s, eng) => `${BASE}/${s.bundle}/${s.profile}/index.html?provider=${eng}` + (eng === 'mapbox' ? `&tk=${TK}` : '')
|
||||
|
|
@ -233,6 +295,14 @@ try {
|
|||
const c = await measure(`${HOST}/C/${s.bundle}/maplibre`, s, s.bundle, { noMapbox: true })
|
||||
if (a.pass || d.pass || c.pass) { console.error(`SELF-TEST FAILED: a deliberately broken ${s.bundle} still passed (${a.pass ? 'leaf ' : ''}${d.pass ? 'gallery ' : ''}${c.pass ? 'script-embed' : ''}). This gate cannot see a broken bundle.`); exit = 3 }
|
||||
else console.log(`self-test: a deliberately broken ${s.bundle} fails as it must (leaf: ${a.why[0]}; script-embed: ${c.why[0]}; gallery: ${d.why[0]})`)
|
||||
// each PAINT arm gets its own poison and must be the arm that catches it: a collapse caught
|
||||
// only by the colour floor (or a blank map caught only by fill) proves nothing about the other
|
||||
const sm = shelf.find(x => x.isMap)
|
||||
if (sm) for (const [poison, arm] of [['blank', /colour\(s\) on its canvas/], ['collapse', /of <.*>'s height, collapsed/]]) {
|
||||
const r = await measure(leaf(sm, 'maplibre'), sm, null, { poisonPaint: poison })
|
||||
if (!r.why.some(w => arm.test(w))) { console.error(`SELF-TEST FAILED: a ${poison} ${sm.bundle} map was not caught by its own arm (${r.pass ? 'it passed' : r.why.join('; ')}). This gate cannot see a ${poison} map.`); exit = 3 }
|
||||
else console.log(`self-test: a ${poison} ${sm.bundle} map fails as it must (${r.why.find(w => arm.test(w))})`)
|
||||
}
|
||||
}
|
||||
if (exit !== 3) {
|
||||
for (const [k, v] of Object.entries(SEED)) if (shelf.some(s => s.bundle === k)) console.log(`SEEDED ${k}: ${v.why}`)
|
||||
|
|
@ -242,7 +312,7 @@ try {
|
|||
// That is a fleet-wide leaf property, reported separately; C is where a partner chooses.
|
||||
const row = (r, s, eng, surf, t0) => {
|
||||
if (!r.pass) exit = 1
|
||||
console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${(r.maps ? r.maps + ' map(s)' : 'no map').padEnd(9)} ${String(Math.round((Date.now() - t0) / 1000)).padStart(3)}s${r.pass ? '' : ' — ' + r.why.join('; ')}`)
|
||||
console.log(`${r.pass ? 'PASS' : 'FAIL'} ${s.bundle.padEnd(24)} ${eng.padEnd(9)} ${surf.padEnd(10)} ${(r.maps ? r.maps + ' map(s)' : 'no map').padEnd(9)} ${(r.paint && r.paint.length ? r.paint.map(q => `${q.colours}c ${Math.round(q.fill * 100)}%`).join(' ') : '').padEnd(12)} ${String(Math.round((Date.now() - t0) / 1000)).padStart(3)}s${r.pass ? '' : ' — ' + r.why.join('; ')}`)
|
||||
}
|
||||
for (const s of shelf) for (const eng of ENGINES) for (const [surf, url, opts] of [
|
||||
['A leaf', leaf(s, eng), {}],
|
||||
|
|
@ -261,7 +331,7 @@ try {
|
|||
row(await measure(`${HOST}/C/${s.bundle}/mapbox/fake`, s, null, { provenanceOnly: true }), s, eng, 'C host-tk', t0)
|
||||
}
|
||||
}
|
||||
console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin works on its own page, iframed and script-embedded on another site, and in the Gallery, under ${ENGINES.join(' and ')}`)
|
||||
console.log(exit ? 'EXPERIMENTAL-SHELF: FAIL' : `EXPERIMENTAL-SHELF: PASS — every experimental plugin works ${SURFACES.map(x => ({ A: 'on its own page', B: 'iframed on another site', C: 'script-embedded on another site', D: 'in the Gallery' })[x]).filter(Boolean).join(', ')}, and every map paints, under ${ENGINES.join(' and ')}`)
|
||||
}
|
||||
} finally { await b.close(); reap() }
|
||||
process.exit(exit)
|
||||
|
|
|
|||
|
|
@ -199,7 +199,10 @@ const rootEntries = [...new Set(files.map(f => f.rel.split('/')[0]))].sort()
|
|||
for (const e of rootEntries) {
|
||||
if (CONTRACT.has(e)) continue
|
||||
const label = files.some(f => f.rel.startsWith(`${e}/`)) ? `${e}/` : e
|
||||
hit(STRICT ? findings : warnings, 'contract', label, `outside the drop-in contract (${[...CONTRACT].join(', ')} only)`)
|
||||
// ALWAYS a finding, not a strict-mode warning: public/ holding ONLY the output folder + README.md is a
|
||||
// Driver decision (BUILD-OUTPUT-IS-ONE-FOLDER), not a style preference. MEASURED 2026-09-25: a rebuild
|
||||
// under the OLD tree name re-added public/doxa-maps/ beside public/dx-maps/, and this gate only warned.
|
||||
hit(findings, 'contract', label, `outside the drop-in contract (${[...CONTRACT].join(', ')} only)`)
|
||||
}
|
||||
|
||||
// inventory
|
||||
|
|
|
|||
38
public/doxa-maps/big-picture/big-picture.js
generated
38
public/doxa-maps/big-picture/big-picture.js
generated
File diff suppressed because one or more lines are too long
1491
public/doxa-maps/doxa-research-map/doxa-research-map.js
generated
1491
public/doxa-maps/doxa-research-map/doxa-research-map.js
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue