doxa-maps/internal/check-map-props-unit.mjs
Doxa Maps b16004e072
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source f1f93d78
2026-09-26 04:20:35 +00:00

82 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* check-map-props-unit — the engine and token props, decided in the library, measured WITHOUT a browser.
*
* resolveMapProvider ('auto': Mapbox when a token is available, MapLibre otherwise; an explicit provider
* wins), and the page-wide token (useSharedMapToken): publish, CLEAR, and a token-service URL. Runs the
* real library modules in Node against a stand-in window/location, so it is fast and needs no browser.
*
* Each case names the defect it guards (U3 = Clear left the old token live, U1 = tokenUrl read by nothing,
* U7 = mixed-case provider dropped).
*
* USAGE node internal/check-map-props-unit.mjs EXIT 0 pass · 1 fail
*/
const listeners = {}
globalThis.window = globalThis
globalThis.CustomEvent = class { constructor(type, o) { this.type = type; this.detail = o && o.detail } }
window.addEventListener = (t, f) => { (listeners[t] = listeners[t] || []).push(f) }
window.removeEventListener = (t, f) => { listeners[t] = (listeners[t] || []).filter((x) => x !== f) }
window.dispatchEvent = (e) => { for (const f of listeners[e.type] || []) f(e); return true }
let search = ''
Object.defineProperty(globalThis, 'location', { configurable: true, get: () => ({ search }) })
const { resolveMapProvider, hasMapToken, explicitMapProvider, DEFAULT_PROVIDER } = await import('../library/composables/useMapProvider.js')
const shared = await import('../library/composables/useSharedMapToken.js')
let bad = 0
const eq = (name, got, want) => { const ok = got === want; if (!ok) bad++; console.log(` ${ok ? 'ok ' : 'FAIL'} ${name.padEnd(64)} ${JSON.stringify(got)}${ok ? '' : ` (want ${JSON.stringify(want)})`}`) }
const reset = () => { search = ''; delete window.MAP_PROVIDER; shared.clearMapToken(); window.__doxaMapTokenPending = null }
const TK = ['pk', 'eyJ1IjoidGVzdCJ9', 'c2ln'].join('.')
console.log('MAP-PROPS: the library, in Node (no browser)')
eq('default is auto', DEFAULT_PROVIDER, 'auto')
reset(); eq('auto, no token anywhere → maplibre', resolveMapProvider({}), 'maplibre')
reset(); eq('auto, tk in profile-config → mapbox', resolveMapProvider({ tk: TK }), 'mapbox')
reset(); eq('auto, placeholder tk YOUR_MAPBOX_TOKEN → maplibre', resolveMapProvider({ tk: 'YOUR_MAPBOX_TOKEN' }), 'maplibre')
reset(); window.MAP_TOKEN = TK; eq('auto, host window.MAP_TOKEN → mapbox', resolveMapProvider({}), 'mapbox')
reset(); eq('auto, a token the caller holds (useMapInstance) → mapbox', resolveMapProvider({}, { token: TK }), 'mapbox')
reset(); eq('auto, tokenUrl in profile-config → mapbox (U1)', resolveMapProvider({ tokenUrl: 'https://example.org/token' }), 'mapbox')
reset(); eq('explicit maplibre with a token → maplibre', resolveMapProvider({ provider: 'maplibre', tk: TK }), 'maplibre')
reset(); eq('explicit mapbox without a token → mapbox', resolveMapProvider({ provider: 'mapbox' }), 'mapbox')
reset(); eq('mixed case "MapLibre" is honoured (U7)', resolveMapProvider({ provider: 'MapLibre', tk: TK }), 'maplibre')
reset(); search = '?provider=maplibre'; window.MAP_TOKEN = TK; eq('?provider=maplibre wins over a host token', resolveMapProvider({}), 'maplibre')
reset(); window.MAP_PROVIDER = 'mapbox'; eq('window.MAP_PROVIDER is explicit', explicitMapProvider({}), 'mapbox')
reset(); eq('"auto" is not explicit', explicitMapProvider({ provider: 'auto' }), '')
// ── the page-wide token ──
reset()
const tok = shared.useSharedMapToken()
shared.publishMapToken(TK)
eq('publish reaches window.MAP_TOKEN', window.MAP_TOKEN, TK)
eq('publish reaches a live element (ref)', tok.value, TK)
shared.clearMapToken()
eq('CLEAR empties window.MAP_TOKEN (U3)', window.MAP_TOKEN || '', '')
eq('CLEAR reaches a live element (U3)', tok.value, '')
eq('after CLEAR, auto → maplibre (U3)', resolveMapProvider({}), 'maplibre')
window.MAP_TOKEN = TK
eq('after CLEAR, a host assignment is adopted again (bridge intact, U3)', tok.value, TK)
eq('hasMapToken sees it', hasMapToken({}), true)
// The OLD Clear path: `delete window.MAP_TOKEN` + publishMapToken('') leaves the old token live (publishing
// '' is a no-op by design, which is why Clear needs clearMapToken). It also used to remove the bridge for
// good; the bridge now re-installs itself, so a later host token is adopted again.
reset(); shared.publishMapToken(TK)
delete window.MAP_TOKEN; shared.publishMapToken('')
eq('reproduction: the old Clear left the OLD token live on elements', tok.value, TK)
window.MAP_TOKEN = 'pk.other'
eq('self-heal: after that delete, a later host token reaches elements again', tok.value, 'pk.other')
window.__doxaMapToken = ''; window.MAP_TOKEN = ''
// ── token-service URL (U1) ──
reset()
globalThis.fetch = async (url) => ({ ok: true, headers: { get: () => 'application/json' }, json: async () => ({ token: url.includes('good') ? TK : 'sk.secret' }) })
const got = await shared.fetchMapToken('https://example.org/good')
eq('tokenUrl answer is published page-wide (U1)', window.MAP_TOKEN, TK)
eq('fetchMapToken resolves with it', got, TK)
reset()
const secret = await shared.fetchMapToken('https://example.org/bad')
eq('a non-public (sk.) answer is refused', secret, '')
eq('…and nothing is published', window.MAP_TOKEN || '', '')
if (bad) { console.log(`MAP-PROPS: FAIL — ${bad} case(s).`); process.exit(1) }
console.log('MAP-PROPS: PASS — auto picks the engine from the token ladder, explicit wins, Clear clears, tokenUrl is honoured.')