From da7496b7ebb7c49a2f9c44a9489b2f819f07a06b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 12 Sep 2026 22:25:05 -0700 Subject: [PATCH] style: format Map Source component boundary --- scripts/package-boundaries.json | 7 +- scripts/qa-map-source-controls.mjs | 162 +++++++++++++++++++++-------- src/mapStackChips.js | 33 ++++-- src/ui/mapSource.js | 9 +- src/ui/mapSourceControls.js | 25 +++-- src/ui/mapSourceControls.test.mjs | 121 ++++++++++++++++----- 6 files changed, 271 insertions(+), 86 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 30366f8b9..97c6601c8 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -318,7 +318,12 @@ }, "map-source-controls": { "exports": ["./ui/maps"], - "modules": ["src/ui/mapSource.js", "src/ui/mapSourceControls.js", "src/mapStackChips.js", "src/keySetupCore.mjs"], + "modules": [ + "src/ui/mapSource.js", + "src/ui/mapSourceControls.js", + "src/mapStackChips.js", + "src/keySetupCore.mjs" + ], "external": [] } } diff --git a/scripts/qa-map-source-controls.mjs b/scripts/qa-map-source-controls.mjs index 501475566..3ddcc64bf 100644 --- a/scripts/qa-map-source-controls.mjs +++ b/scripts/qa-map-source-controls.mjs @@ -3,73 +3,143 @@ import fs from 'node:fs'; import puppeteer from 'puppeteer'; -const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', ...(process.platform === 'darwin' ? ['--use-angle=metal', '--enable-gpu'] : ['--use-gl=angle', '--use-angle=swiftshader'])] }); +const browser = await puppeteer.launch({ + headless: true, + args: [ + '--no-sandbox', + ...(process.platform === 'darwin' + ? ['--use-angle=metal', '--enable-gpu'] + : ['--use-gl=angle', '--use-angle=swiftshader']), + ], +}); const page = await browser.newPage(); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); let failures = 0; -const check = (name, ok) => { console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`); if (!ok) failures++; }; +const check = (name, ok) => { + console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`); + if (!ok) failures++; +}; try { await page.setViewport({ width: 1440, height: 900 }); const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173'); url.searchParams.set('welcome', '0'); await page.goto(url.href, { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => { - const cover = document.getElementById('loading-screen'); - return window.__godsEyeView?.styleManager?._mapSourceControls && cover?.classList.contains('hidden') && Number(getComputedStyle(cover).opacity) === 0; - }, { timeout: 60_000 }); + await page.waitForFunction( + () => { + const cover = document.getElementById('loading-screen'); + return ( + window.__godsEyeView?.styleManager?._mapSourceControls && + cover?.classList.contains('hidden') && + Number(getComputedStyle(cover).opacity) === 0 + ); + }, + { timeout: 60_000 }, + ); await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; const controller = manager.mapStackController; const original = controller.setStack; window.__qaMapSelections = []; - controller.setStack = function(id) { window.__qaMapSelections.push(id); return original.call(this, id); }; - manager.setPanelCollapsed('control-panel', false, { persist: false, syncShare: false }); + controller.setStack = function (id) { + window.__qaMapSelections.push(id); + return original.call(this, id); + }; + manager.setPanelCollapsed('control-panel', false, { + persist: false, + syncShare: false, + }); }); await page.click('[data-stack-id="osm"]'); - await page.waitForFunction(() => window.__godsEyeView.styleManager.mapStackController.getActiveId() === 'osm'); - check('a native chip click requests OSM exactly once', await page.evaluate(() => window.__qaMapSelections.length === 1 && window.__qaMapSelections[0] === 'osm')); - check('active chip and status follow the displayed OSM source', await page.evaluate(() => { - const manager = window.__godsEyeView.styleManager; - return manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset.stackId === 'osm' && manager._mapStackStatus.textContent === manager.mapStackController.getState().activeStack.shortLabel; - })); - check('the public map action uses the same component and truthful result', await page.evaluate(async () => { - const manager = window.__godsEyeView.styleManager; - const result = await manager.setMapStack('esri-imagery'); - const active = manager.mapStackController.getActiveId(); - return window.__qaMapSelections.at(-1) === 'esri-imagery' && manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset.stackId === active && (result.ok || !!manager.mapStackController.getState().lastError); - })); - check('refresh revokes retained old chips', await page.evaluate(() => { - const manager = window.__godsEyeView.styleManager; - const old = manager._mapStackChips.querySelector('[data-stack-id="osm"]'); - manager._mapSourceControls.refresh(); - const before = window.__qaMapSelections.length; - old.click(); - return window.__qaMapSelections.length === before; - })); - check('refreshed chips still select once', await page.evaluate(async () => { - const manager = window.__godsEyeView.styleManager; - const before = window.__qaMapSelections.length; - manager._mapStackChips.querySelector('[data-stack-id="osm"]').click(); - await Promise.resolve(); - return window.__qaMapSelections.length === before + 1; - })); - await page.waitForFunction(() => window.__godsEyeView.styleManager.mapStackController.getActiveId() === 'osm'); + await page.waitForFunction( + () => + window.__godsEyeView.styleManager.mapStackController.getActiveId() === + 'osm', + ); + check( + 'a native chip click requests OSM exactly once', + await page.evaluate( + () => + window.__qaMapSelections.length === 1 && + window.__qaMapSelections[0] === 'osm', + ), + ); + check( + 'active chip and status follow the displayed OSM source', + await page.evaluate(() => { + const manager = window.__godsEyeView.styleManager; + return ( + manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset + .stackId === 'osm' && + manager._mapStackStatus.textContent === + manager.mapStackController.getState().activeStack.shortLabel + ); + }), + ); + check( + 'the public map action uses the same component and truthful result', + await page.evaluate(async () => { + const manager = window.__godsEyeView.styleManager; + const result = await manager.setMapStack('esri-imagery'); + const active = manager.mapStackController.getActiveId(); + return ( + window.__qaMapSelections.at(-1) === 'esri-imagery' && + manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset + .stackId === active && + (result.ok || !!manager.mapStackController.getState().lastError) + ); + }), + ); + check( + 'refresh revokes retained old chips', + await page.evaluate(() => { + const manager = window.__godsEyeView.styleManager; + const old = manager._mapStackChips.querySelector('[data-stack-id="osm"]'); + manager._mapSourceControls.refresh(); + const before = window.__qaMapSelections.length; + old.click(); + return window.__qaMapSelections.length === before; + }), + ); + check( + 'refreshed chips still select once', + await page.evaluate(async () => { + const manager = window.__godsEyeView.styleManager; + const before = window.__qaMapSelections.length; + manager._mapStackChips.querySelector('[data-stack-id="osm"]').click(); + await Promise.resolve(); + return window.__qaMapSelections.length === before + 1; + }), + ); + await page.waitForFunction( + () => + window.__godsEyeView.styleManager.mapStackController.getActiveId() === + 'osm', + ); fs.mkdirSync('qa-shots/map-source-controls', { recursive: true }); await page.screenshot({ path: 'qa-shots/map-source-controls/desktop.png' }); await page.setViewport({ width: 620, height: 900 }); - await page.waitForFunction(() => document.getElementById('left-panel-stack')?.dataset.layoutMode === 'mobile'); + await page.waitForFunction( + () => + document.getElementById('left-panel-stack')?.dataset.layoutMode === + 'mobile', + ); await page.screenshot({ path: 'qa-shots/map-source-controls/narrow.png' }); - check('destroyed controls cannot issue requests from retained chips', await page.evaluate(async () => { - const manager = window.__godsEyeView.styleManager; - const before = window.__qaMapSelections.length; - manager._mapSourceControls.destroy(); - manager._mapStackChips.querySelector('[data-stack-id="osm"]').click(); - const result = await manager._mapSourceControls.select('esri-imagery'); - return result === null && window.__qaMapSelections.length === before; - })); + check( + 'destroyed controls cannot issue requests from retained chips', + await page.evaluate(async () => { + const manager = window.__godsEyeView.styleManager; + const before = window.__qaMapSelections.length; + manager._mapSourceControls.destroy(); + manager._mapStackChips.querySelector('[data-stack-id="osm"]').click(); + const result = await manager._mapSourceControls.select('esri-imagery'); + return result === null && window.__qaMapSelections.length === before; + }), + ); check('no uncaught browser errors', errors.length === 0); if (errors.length) console.log(JSON.stringify(errors)); -} finally { await browser.close(); } +} finally { + await browser.close(); +} console.log(`RESULT: ${failures} failures`); process.exitCode = failures ? 1 : 0; diff --git a/src/mapStackChips.js b/src/mapStackChips.js index 0717904e0..ac523700d 100644 --- a/src/mapStackChips.js +++ b/src/mapStackChips.js @@ -40,7 +40,9 @@ export function mapStackChipModel(stack, activeId) { const fallbackReason = requiresIon ? keySetupRequirement('cesium-ion') : `${label || 'This map stack'} is unavailable`; - const unavailableHint = available ? '' : String(stack?.unavailableReason || fallbackReason); + const unavailableHint = available + ? '' + : String(stack?.unavailableReason || fallbackReason); return { id: String(stack?.id ?? ''), label, @@ -63,10 +65,10 @@ export function mapStackChipModel(stack, activeId) { * `PRESENTED_MAP_STACK_IDS` order; unlisted stacks stay outside this presentation. */ export function mapStackChipModels(stacks, activeId) { - const stacksById = new Map((Array.isArray(stacks) ? stacks : []) - .map((stack) => [stack?.id, stack])); - return PRESENTED_MAP_STACK_IDS - .map((id) => stacksById.get(id)) + const stacksById = new Map( + (Array.isArray(stacks) ? stacks : []).map((stack) => [stack?.id, stack]), + ); + return PRESENTED_MAP_STACK_IDS.map((id) => stacksById.get(id)) .filter(Boolean) .map((stack) => mapStackChipModel(stack, activeId)); } @@ -82,7 +84,17 @@ export function mapStackChipModels(stacks, activeId) { * @param {(element: HTMLElement, type: string, listener: Function) => void} [options.bind] - Listener owner override. * @returns {Array} The rendered chip models. */ -export function renderMapStackChips(container, stacks, { activeId = null, onSelect = null, doc, bind = (element, type, listener) => element.addEventListener(type, listener) } = {}) { +export function renderMapStackChips( + container, + stacks, + { + activeId = null, + onSelect = null, + doc, + bind = (element, type, listener) => + element.addEventListener(type, listener), + } = {}, +) { if (!container) return []; const ownerDoc = doc || container.ownerDocument || globalThis.document; if (!ownerDoc?.createElement) return []; @@ -97,13 +109,18 @@ export function renderMapStackChips(container, stacks, { activeId = null, onSele MAP_STACK_CHIP_CLASS, model.active ? 'active' : '', model.available ? '' : 'unavailable', - ].filter(Boolean).join(' '); + ] + .filter(Boolean) + .join(' '); chip.dataset.stackId = model.id; chip.title = model.title; chip.setAttribute('aria-pressed', String(model.active)); chip.setAttribute('aria-disabled', String(!model.available)); if (!model.available) { - chip.setAttribute('aria-label', `${model.label} unavailable: ${model.unavailableHint}`); + chip.setAttribute( + 'aria-label', + `${model.label} unavailable: ${model.unavailableHint}`, + ); } const label = ownerDoc.createElement('span'); diff --git a/src/ui/mapSource.js b/src/ui/mapSource.js index 5635c72ee..e6b1925af 100644 --- a/src/ui/mapSource.js +++ b/src/ui/mapSource.js @@ -1,2 +1,9 @@ export { createMapSourceControls } from './mapSourceControls.js'; -export { MAP_STACK_CHIP_CLASS, PRESENTED_MAP_STACK_IDS, mapStackChipModel, mapStackChipModels, renderMapStackChips, syncMapStackChips } from '../mapStackChips.js'; +export { + MAP_STACK_CHIP_CLASS, + PRESENTED_MAP_STACK_IDS, + mapStackChipModel, + mapStackChipModels, + renderMapStackChips, + syncMapStackChips, +} from '../mapStackChips.js'; diff --git a/src/ui/mapSourceControls.js b/src/ui/mapSourceControls.js index b2bf12af7..383f0d15f 100644 --- a/src/ui/mapSourceControls.js +++ b/src/ui/mapSourceControls.js @@ -5,8 +5,13 @@ import { renderMapStackChips, syncMapStackChips } from '../mapStackChips.js'; * The supplied controller remains authoritative for availability and active state. */ export function createMapSourceControls({ - container, statusElement, controller, subscribe, - claimSelection = () => {}, onStateChanged = () => {}, onError = () => {}, + container, + statusElement, + controller, + subscribe, + claimSelection = () => {}, + onStateChanged = () => {}, + onError = () => {}, }) { let destroyed = false; let generation = 0; @@ -20,7 +25,10 @@ export function createMapSourceControls({ syncMapStackChips(container, state.activeId); if (statusElement) { const stack = state.activeStack; - statusElement.textContent = state.status === 'switching' ? '...' : (stack?.shortLabel || stack?.label || 'MAP'); + statusElement.textContent = + state.status === 'switching' + ? '...' + : stack?.shortLabel || stack?.label || 'MAP'; statusElement.classList.toggle('warn', !!state.lastError); } } @@ -42,7 +50,8 @@ export function createMapSourceControls({ } if (destroyed || current !== generation) return state; render(controller.getState()); - if (state?.activeId === before && stackId !== before && state?.lastError) onError(state.lastError); + if (state?.activeId === before && stackId !== before && state?.lastError) + onError(state.lastError); if (syncShare) onStateChanged(); return state; } @@ -51,7 +60,9 @@ export function createMapSourceControls({ for (const remove of removers.splice(0)) remove(); renderMapStackChips(container, controller.getStacks(), { activeId: controller.getActiveId(), - onSelect: (id) => { void select(id).catch(() => {}); }, + onSelect: (id) => { + void select(id).catch(() => {}); + }, bind, }); render(controller.getState()); @@ -63,7 +74,9 @@ export function createMapSourceControls({ }); refresh(); return { - render, select, refresh, + render, + select, + refresh, destroy() { if (destroyed) return; destroyed = true; diff --git a/src/ui/mapSourceControls.test.mjs b/src/ui/mapSourceControls.test.mjs index 821ba6a1c..b2bceee8b 100644 --- a/src/ui/mapSourceControls.test.mjs +++ b/src/ui/mapSourceControls.test.mjs @@ -16,7 +16,9 @@ function makeElement(tagName = 'div') { children: [], classList: { toggle(name, force) { - const classes = new Set(String(element.className).split(/\s+/).filter(Boolean)); + const classes = new Set( + String(element.className).split(/\s+/).filter(Boolean), + ); const next = force === undefined ? !classes.has(name) : !!force; if (next) classes.add(name); else classes.delete(name); @@ -26,16 +28,35 @@ function makeElement(tagName = 'div') { return String(element.className).split(/\s+/).includes(name); }, }, - appendChild(child) { element.children.push(child); return child; }, - setAttribute(name, value) { element.attributes[name] = String(value); }, - getAttribute(name) { return element.attributes[name] ?? null; }, - addEventListener(type, handler) { (element.listeners[type] ||= []).push(handler); }, - removeEventListener(type, handler) { element.listeners[type] = (element.listeners[type] || []).filter((current) => current !== handler); }, - click() { for (const handler of element.listeners.click || []) handler(); }, + appendChild(child) { + element.children.push(child); + return child; + }, + setAttribute(name, value) { + element.attributes[name] = String(value); + }, + getAttribute(name) { + return element.attributes[name] ?? null; + }, + addEventListener(type, handler) { + (element.listeners[type] ||= []).push(handler); + }, + removeEventListener(type, handler) { + element.listeners[type] = (element.listeners[type] || []).filter( + (current) => current !== handler, + ); + }, + click() { + for (const handler of element.listeners.click || []) handler(); + }, }; Object.defineProperty(element, 'innerHTML', { - get() { return ''; }, - set() { element.children.length = 0; }, + get() { + return ''; + }, + set() { + element.children.length = 0; + }, }); return element; } @@ -44,7 +65,16 @@ function fixture() { const container = makeElement(); container.ownerDocument = { createElement: (tag) => makeElement(tag) }; const statusElement = makeElement(); - const sources = [{ id: 'osm', label: 'OSM' }, { id: 'esri-imagery', label: 'Esri' }, { id: 'bing-aerial', label: 'Bing', available: false, unavailableReason: 'Unavailable for this test' }]; + const sources = [ + { id: 'osm', label: 'OSM' }, + { id: 'esri-imagery', label: 'Esri' }, + { + id: 'bing-aerial', + label: 'Bing', + available: false, + unavailableReason: 'Unavailable for this test', + }, + ]; let state = { activeId: 'osm', activeStack: sources[0], status: 'ready' }; const listeners = new Set(); const requests = []; @@ -55,20 +85,49 @@ function fixture() { getState: (status) => ({ ...state, status: status || state.status }), setStack: (id) => { calls.push(['select', id]); - return new Promise((resolve, reject) => requests.push({ id, resolve, reject })); + return new Promise((resolve, reject) => + requests.push({ id, resolve, reject }), + ); }, }; - const controls = createMapSourceControls({ container, statusElement, controller, - subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, - claimSelection: () => calls.push(['claim']), onStateChanged: () => calls.push(['state']), onError: (message) => calls.push(['error', message]), + const controls = createMapSourceControls({ + container, + statusElement, + controller, + subscribe: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + claimSelection: () => calls.push(['claim']), + onStateChanged: () => calls.push(['state']), + onError: (message) => calls.push(['error', message]), }); - const chip = (id) => container.children.find((el) => el.dataset.stackId === id); + const chip = (id) => + container.children.find((el) => el.dataset.stackId === id); function change(id, lastError = null) { - state = { activeId: id, activeStack: sources.find((source) => source.id === id), status: 'ready', lastError }; + state = { + activeId: id, + activeStack: sources.find((source) => source.id === id), + status: 'ready', + lastError, + }; return controller.getState(); } - const emit = () => { for (const listener of listeners) listener(); }; - return { container, statusElement, controller, controls, requests, calls, listeners, chip, change, emit }; + const emit = () => { + for (const listener of listeners) listener(); + }; + return { + container, + statusElement, + controller, + controls, + requests, + calls, + listeners, + chip, + change, + emit, + }; } test('initial presentation uses actual state and retains unavailable chip semantics', () => { @@ -76,7 +135,11 @@ test('initial presentation uses actual state and retains unavailable chip semant assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true'); assert.equal(f.statusElement.textContent, 'OSM'); const unavailable = f.chip('bing-aerial'); - assert.equal(unavailable.disabled, false, 'unavailable chips remain focusable'); + assert.equal( + unavailable.disabled, + false, + 'unavailable chips remain focusable', + ); assert.equal(unavailable.getAttribute('aria-disabled'), 'true'); unavailable.click(); assert.equal(f.requests.length, 0); @@ -98,8 +161,10 @@ test('selection claims authority before requesting and never lights a pending so test('provider-driven fallback updates the active chip and durable state notification', () => { const f = fixture(); - f.change('esri-imagery'); f.emit(); - f.change('osm', 'Source unavailable; using OSM'); f.emit(); + f.change('esri-imagery'); + f.emit(); + f.change('osm', 'Source unavailable; using OSM'); + f.emit(); assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true'); assert.equal(f.statusElement.textContent, 'OSM'); assert.equal(f.statusElement.classList.contains('warn'), true); @@ -114,7 +179,11 @@ test('a late superseded response cannot overwrite the latest displayed selection f.requests[1].resolve(f.change('osm')); await second; const count = f.calls.length; - f.requests[0].resolve({ activeId: 'esri-imagery', activeStack: { label: 'old' }, lastError: 'obsolete failure' }); + f.requests[0].resolve({ + activeId: 'esri-imagery', + activeStack: { label: 'old' }, + lastError: 'obsolete failure', + }); await first; assert.equal(f.statusElement.textContent, 'OSM'); assert.equal(f.calls.length, count); @@ -128,7 +197,9 @@ test('failed selection keeps the real source lit and reports its error', async ( await pending; assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true'); assert.equal(f.statusElement.classList.contains('warn'), true); - assert.ok(f.calls.some((call) => call[0] === 'error' && call[1] === 'Unavailable')); + assert.ok( + f.calls.some((call) => call[0] === 'error' && call[1] === 'Unavailable'), + ); f.controls.destroy(); }); @@ -138,7 +209,9 @@ test('rejected selection leaves switching presentation and propagates a useful e f.requests[0].reject(new Error('Request failed')); await assert.rejects(pending, /Request failed/); assert.equal(f.statusElement.textContent, 'OSM'); - assert.ok(f.calls.some((call) => call[0] === 'error' && call[1] === 'Request failed')); + assert.ok( + f.calls.some((call) => call[0] === 'error' && call[1] === 'Request failed'), + ); f.controls.destroy(); });