A profile/instance whose config says provider:'maplibre' now generates a page that loads the MapLibre peer (jsdelivr, same versions as useMapInstance) and mounts IMMEDIATELY — the token ladder is skipped entirely. Mapbox pages are unchanged. demo-map (the example vibe coders copy) is now provider-aware via resolveMapProvider, and ships a 'maplibre' instance as the living token-free demo. embed-smoke covers instance pages; verified 2/2 with live canvases — including the maplibre one with NO token anywhere.
104 lines
5.3 KiB
JavaScript
104 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* embed-smoke.mjs — prove EVERY map embeds: load each generated profile page
|
|
* headless and assert its custom element actually mounts.
|
|
*
|
|
* WHY: "embeddability works on all the maps" is the product promise. The build
|
|
* guards prove the files exist; THIS proves each page boots — element defined,
|
|
* shadow DOM populated, no ProfileLoader error card, and (for map profiles) a
|
|
* WebGL canvas present.
|
|
*
|
|
* HOW: reads public/doxa-maps/manifest.json (the same registry partners use),
|
|
* serves public/ itself if nothing is on --port, and drives Playwright
|
|
* Chromium with SwiftShader software WebGL — CI boxes rarely have a GPU. (The
|
|
* posters pipeline stays Firefox-only; this is a headless VERIFICATION harness,
|
|
* not a rendering pipeline.)
|
|
*
|
|
* Usage: node internal/embed-smoke.mjs [--port 4177] [--grep <substr>]
|
|
* Exit: 0 all pass · 1 any fail (list printed)
|
|
*/
|
|
import { createServer } from 'node:http'
|
|
import { readFileSync, existsSync, statSync, createReadStream } from 'node:fs'
|
|
import { resolve, join, extname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { chromium } from 'playwright'
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
const PUB = resolve(__dirname, '..', 'public')
|
|
const args = process.argv.slice(2)
|
|
const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d }
|
|
const PORT = Number(argOf('--port', 4177))
|
|
const GREP = argOf('--grep', '')
|
|
|
|
// ── tiny static server (no deps; mime enough for the pages) ─────────────────
|
|
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
'.css': 'text/css', '.json': 'application/json', '.geojson': 'application/geo+json',
|
|
'.png': 'image/png', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.csv': 'text/csv' }
|
|
const server = createServer((req, res) => {
|
|
try {
|
|
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname)
|
|
if (p.endsWith('/')) p += 'index.html'
|
|
const f = join(PUB, p)
|
|
if (!f.startsWith(PUB) || !existsSync(f) || !statSync(f).isFile()) { res.writeHead(404); return res.end() }
|
|
res.writeHead(200, { 'content-type': MIME[extname(f)] || 'application/octet-stream' })
|
|
createReadStream(f).pipe(res)
|
|
} catch { res.writeHead(500); res.end() }
|
|
})
|
|
|
|
// ── the page list: every profile page in the manifest ───────────────────────
|
|
const manifest = JSON.parse(readFileSync(join(PUB, 'doxa-maps', 'manifest.json'), 'utf8'))
|
|
const targets = []
|
|
for (const b of manifest.bundles) for (const p of b.profiles) {
|
|
// iframeUrl is ./-relative to the tree root
|
|
const url = p.iframeUrl.replace(/^\.\//, '/doxa-maps/')
|
|
targets.push({ bundle: b.bundle, profile: p.profile, element: p.element, url })
|
|
// Preconfigured instances are embeddable pages too — verify them the same way.
|
|
for (const inst of (p.instances || [])) {
|
|
targets.push({ bundle: b.bundle, profile: `${p.profile}/${inst.id}`, element: inst.element || p.element,
|
|
url: inst.iframeUrl.replace(/^\.\//, '/doxa-maps/') })
|
|
}
|
|
}
|
|
const list = GREP ? targets.filter(t => (t.bundle + '/' + t.profile).includes(GREP)) : targets
|
|
|
|
await new Promise(r => server.listen(PORT, '127.0.0.1', r))
|
|
console.log(`[embed-smoke] serving ${PUB} on :${PORT} — ${list.length} profile page(s)`)
|
|
|
|
const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'] })
|
|
const results = []
|
|
for (const t of list) {
|
|
const ctx = await browser.newContext({ viewport: { width: 1100, height: 750 } })
|
|
const page = await ctx.newPage()
|
|
const errors = []
|
|
page.on('pageerror', e => errors.push(String(e).slice(0, 160)))
|
|
const r = { ...t, ok: false, why: '' }
|
|
try {
|
|
await page.goto(`http://127.0.0.1:${PORT}${t.url}`, { waitUntil: 'load', timeout: 30000 })
|
|
// give the element time to define + mount + (maybe) paint a canvas
|
|
await page.waitForFunction((tag) => {
|
|
const el = document.querySelector(tag)
|
|
return el && el.shadowRoot && el.shadowRoot.childElementCount > 0
|
|
}, t.element, { timeout: 25000 })
|
|
await page.waitForTimeout(4000)
|
|
const verdict = await page.evaluate((tag) => {
|
|
const el = document.querySelector(tag)
|
|
const sr = el.shadowRoot
|
|
return {
|
|
errorCard: !!sr.querySelector('.profile-error'),
|
|
canvas: !!sr.querySelector('canvas'),
|
|
nodes: sr.querySelectorAll('*').length,
|
|
}
|
|
}, t.element)
|
|
if (verdict.errorCard) r.why = 'ProfileLoader error card shown'
|
|
else if (verdict.nodes < 5) r.why = `shadow DOM nearly empty (${verdict.nodes} nodes)`
|
|
else { r.ok = true; r.why = `${verdict.nodes} nodes${verdict.canvas ? ' + canvas' : ' (no canvas — DOM profile)'}` }
|
|
} catch (e) { r.why = 'mount timeout / ' + String(e.message || e).slice(0, 100) }
|
|
if (errors.length && r.ok) r.why += ` · ${errors.length} pageerror(s): ${errors[0]}`
|
|
results.push(r)
|
|
console.log(` ${r.ok ? '✓' : '✗'} ${t.bundle}/${t.profile} <${t.element}> — ${r.why}`)
|
|
await ctx.close()
|
|
}
|
|
await browser.close(); server.close()
|
|
|
|
const failed = results.filter(r => !r.ok)
|
|
console.log(`\n[embed-smoke] ${results.length - failed.length}/${results.length} pages mount cleanly`)
|
|
if (failed.length) { console.log('FAILED:'); failed.forEach(f => console.log(` ✗ ${f.bundle}/${f.profile} — ${f.why}`)); process.exit(1) }
|