mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
style: format Map Source component boundary
This commit is contained in:
1 parent
72d531b18f
commit
da7496b7eb
6 files changed
+271
-86
No files matched your search
@@ -318,7 +318,12 @@
|
|||||||
},
|
},
|
||||||
"map-source-controls": {
|
"map-source-controls": {
|
||||||
"exports": ["./ui/maps"],
|
"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": []
|
"external": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,73 +3,143 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import puppeteer from 'puppeteer';
|
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 page = await browser.newPage();
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', (error) => errors.push(error.message));
|
page.on('pageerror', (error) => errors.push(error.message));
|
||||||
let failures = 0;
|
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 {
|
try {
|
||||||
await page.setViewport({ width: 1440, height: 900 });
|
await page.setViewport({ width: 1440, height: 900 });
|
||||||
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
|
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
|
||||||
url.searchParams.set('welcome', '0');
|
url.searchParams.set('welcome', '0');
|
||||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(
|
||||||
const cover = document.getElementById('loading-screen');
|
() => {
|
||||||
return window.__godsEyeView?.styleManager?._mapSourceControls && cover?.classList.contains('hidden') && Number(getComputedStyle(cover).opacity) === 0;
|
const cover = document.getElementById('loading-screen');
|
||||||
}, { timeout: 60_000 });
|
return (
|
||||||
|
window.__godsEyeView?.styleManager?._mapSourceControls &&
|
||||||
|
cover?.classList.contains('hidden') &&
|
||||||
|
Number(getComputedStyle(cover).opacity) === 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 60_000 },
|
||||||
|
);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const manager = window.__godsEyeView.styleManager;
|
const manager = window.__godsEyeView.styleManager;
|
||||||
const controller = manager.mapStackController;
|
const controller = manager.mapStackController;
|
||||||
const original = controller.setStack;
|
const original = controller.setStack;
|
||||||
window.__qaMapSelections = [];
|
window.__qaMapSelections = [];
|
||||||
controller.setStack = function(id) { window.__qaMapSelections.push(id); return original.call(this, id); };
|
controller.setStack = function (id) {
|
||||||
manager.setPanelCollapsed('control-panel', false, { persist: false, syncShare: false });
|
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.click('[data-stack-id="osm"]');
|
||||||
await page.waitForFunction(() => window.__godsEyeView.styleManager.mapStackController.getActiveId() === 'osm');
|
await page.waitForFunction(
|
||||||
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(() => {
|
window.__godsEyeView.styleManager.mapStackController.getActiveId() ===
|
||||||
const manager = window.__godsEyeView.styleManager;
|
'osm',
|
||||||
return manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset.stackId === 'osm' && manager._mapStackStatus.textContent === manager.mapStackController.getState().activeStack.shortLabel;
|
);
|
||||||
}));
|
check(
|
||||||
check('the public map action uses the same component and truthful result', await page.evaluate(async () => {
|
'a native chip click requests OSM exactly once',
|
||||||
const manager = window.__godsEyeView.styleManager;
|
await page.evaluate(
|
||||||
const result = await manager.setMapStack('esri-imagery');
|
() =>
|
||||||
const active = manager.mapStackController.getActiveId();
|
window.__qaMapSelections.length === 1 &&
|
||||||
return window.__qaMapSelections.at(-1) === 'esri-imagery' && manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset.stackId === active && (result.ok || !!manager.mapStackController.getState().lastError);
|
window.__qaMapSelections[0] === 'osm',
|
||||||
}));
|
),
|
||||||
check('refresh revokes retained old chips', await page.evaluate(() => {
|
);
|
||||||
const manager = window.__godsEyeView.styleManager;
|
check(
|
||||||
const old = manager._mapStackChips.querySelector('[data-stack-id="osm"]');
|
'active chip and status follow the displayed OSM source',
|
||||||
manager._mapSourceControls.refresh();
|
await page.evaluate(() => {
|
||||||
const before = window.__qaMapSelections.length;
|
const manager = window.__godsEyeView.styleManager;
|
||||||
old.click();
|
return (
|
||||||
return window.__qaMapSelections.length === before;
|
manager._mapStackChips.querySelector('[aria-pressed="true"]')?.dataset
|
||||||
}));
|
.stackId === 'osm' &&
|
||||||
check('refreshed chips still select once', await page.evaluate(async () => {
|
manager._mapStackStatus.textContent ===
|
||||||
const manager = window.__godsEyeView.styleManager;
|
manager.mapStackController.getState().activeStack.shortLabel
|
||||||
const before = window.__qaMapSelections.length;
|
);
|
||||||
manager._mapStackChips.querySelector('[data-stack-id="osm"]').click();
|
}),
|
||||||
await Promise.resolve();
|
);
|
||||||
return window.__qaMapSelections.length === before + 1;
|
check(
|
||||||
}));
|
'the public map action uses the same component and truthful result',
|
||||||
await page.waitForFunction(() => window.__godsEyeView.styleManager.mapStackController.getActiveId() === 'osm');
|
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 });
|
fs.mkdirSync('qa-shots/map-source-controls', { recursive: true });
|
||||||
await page.screenshot({ path: 'qa-shots/map-source-controls/desktop.png' });
|
await page.screenshot({ path: 'qa-shots/map-source-controls/desktop.png' });
|
||||||
await page.setViewport({ width: 620, height: 900 });
|
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' });
|
await page.screenshot({ path: 'qa-shots/map-source-controls/narrow.png' });
|
||||||
check('destroyed controls cannot issue requests from retained chips', await page.evaluate(async () => {
|
check(
|
||||||
const manager = window.__godsEyeView.styleManager;
|
'destroyed controls cannot issue requests from retained chips',
|
||||||
const before = window.__qaMapSelections.length;
|
await page.evaluate(async () => {
|
||||||
manager._mapSourceControls.destroy();
|
const manager = window.__godsEyeView.styleManager;
|
||||||
manager._mapStackChips.querySelector('[data-stack-id="osm"]').click();
|
const before = window.__qaMapSelections.length;
|
||||||
const result = await manager._mapSourceControls.select('esri-imagery');
|
manager._mapSourceControls.destroy();
|
||||||
return result === null && window.__qaMapSelections.length === before;
|
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);
|
check('no uncaught browser errors', errors.length === 0);
|
||||||
if (errors.length) console.log(JSON.stringify(errors));
|
if (errors.length) console.log(JSON.stringify(errors));
|
||||||
} finally { await browser.close(); }
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
console.log(`RESULT: ${failures} failures`);
|
console.log(`RESULT: ${failures} failures`);
|
||||||
process.exitCode = failures ? 1 : 0;
|
process.exitCode = failures ? 1 : 0;
|
||||||
+25
-8
@@ -40,7 +40,9 @@ export function mapStackChipModel(stack, activeId) {
|
|||||||
const fallbackReason = requiresIon
|
const fallbackReason = requiresIon
|
||||||
? keySetupRequirement('cesium-ion')
|
? keySetupRequirement('cesium-ion')
|
||||||
: `${label || 'This map stack'} is unavailable`;
|
: `${label || 'This map stack'} is unavailable`;
|
||||||
const unavailableHint = available ? '' : String(stack?.unavailableReason || fallbackReason);
|
const unavailableHint = available
|
||||||
|
? ''
|
||||||
|
: String(stack?.unavailableReason || fallbackReason);
|
||||||
return {
|
return {
|
||||||
id: String(stack?.id ?? ''),
|
id: String(stack?.id ?? ''),
|
||||||
label,
|
label,
|
||||||
@@ -63,10 +65,10 @@ export function mapStackChipModel(stack, activeId) {
|
|||||||
* `PRESENTED_MAP_STACK_IDS` order; unlisted stacks stay outside this presentation.
|
* `PRESENTED_MAP_STACK_IDS` order; unlisted stacks stay outside this presentation.
|
||||||
*/
|
*/
|
||||||
export function mapStackChipModels(stacks, activeId) {
|
export function mapStackChipModels(stacks, activeId) {
|
||||||
const stacksById = new Map((Array.isArray(stacks) ? stacks : [])
|
const stacksById = new Map(
|
||||||
.map((stack) => [stack?.id, stack]));
|
(Array.isArray(stacks) ? stacks : []).map((stack) => [stack?.id, stack]),
|
||||||
return PRESENTED_MAP_STACK_IDS
|
);
|
||||||
.map((id) => stacksById.get(id))
|
return PRESENTED_MAP_STACK_IDS.map((id) => stacksById.get(id))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((stack) => mapStackChipModel(stack, activeId));
|
.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.
|
* @param {(element: HTMLElement, type: string, listener: Function) => void} [options.bind] - Listener owner override.
|
||||||
* @returns {Array<object>} The rendered chip models.
|
* @returns {Array<object>} 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 [];
|
if (!container) return [];
|
||||||
const ownerDoc = doc || container.ownerDocument || globalThis.document;
|
const ownerDoc = doc || container.ownerDocument || globalThis.document;
|
||||||
if (!ownerDoc?.createElement) return [];
|
if (!ownerDoc?.createElement) return [];
|
||||||
@@ -97,13 +109,18 @@ export function renderMapStackChips(container, stacks, { activeId = null, onSele
|
|||||||
MAP_STACK_CHIP_CLASS,
|
MAP_STACK_CHIP_CLASS,
|
||||||
model.active ? 'active' : '',
|
model.active ? 'active' : '',
|
||||||
model.available ? '' : 'unavailable',
|
model.available ? '' : 'unavailable',
|
||||||
].filter(Boolean).join(' ');
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
chip.dataset.stackId = model.id;
|
chip.dataset.stackId = model.id;
|
||||||
chip.title = model.title;
|
chip.title = model.title;
|
||||||
chip.setAttribute('aria-pressed', String(model.active));
|
chip.setAttribute('aria-pressed', String(model.active));
|
||||||
chip.setAttribute('aria-disabled', String(!model.available));
|
chip.setAttribute('aria-disabled', String(!model.available));
|
||||||
if (!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');
|
const label = ownerDoc.createElement('span');
|
||||||
|
|||||||
+8
-1
@@ -1,2 +1,9 @@
|
|||||||
export { createMapSourceControls } from './mapSourceControls.js';
|
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';
|
||||||
@@ -5,8 +5,13 @@ import { renderMapStackChips, syncMapStackChips } from '../mapStackChips.js';
|
|||||||
* The supplied controller remains authoritative for availability and active state.
|
* The supplied controller remains authoritative for availability and active state.
|
||||||
*/
|
*/
|
||||||
export function createMapSourceControls({
|
export function createMapSourceControls({
|
||||||
container, statusElement, controller, subscribe,
|
container,
|
||||||
claimSelection = () => {}, onStateChanged = () => {}, onError = () => {},
|
statusElement,
|
||||||
|
controller,
|
||||||
|
subscribe,
|
||||||
|
claimSelection = () => {},
|
||||||
|
onStateChanged = () => {},
|
||||||
|
onError = () => {},
|
||||||
}) {
|
}) {
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
@@ -20,7 +25,10 @@ export function createMapSourceControls({
|
|||||||
syncMapStackChips(container, state.activeId);
|
syncMapStackChips(container, state.activeId);
|
||||||
if (statusElement) {
|
if (statusElement) {
|
||||||
const stack = state.activeStack;
|
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);
|
statusElement.classList.toggle('warn', !!state.lastError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +50,8 @@ export function createMapSourceControls({
|
|||||||
}
|
}
|
||||||
if (destroyed || current !== generation) return state;
|
if (destroyed || current !== generation) return state;
|
||||||
render(controller.getState());
|
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();
|
if (syncShare) onStateChanged();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -51,7 +60,9 @@ export function createMapSourceControls({
|
|||||||
for (const remove of removers.splice(0)) remove();
|
for (const remove of removers.splice(0)) remove();
|
||||||
renderMapStackChips(container, controller.getStacks(), {
|
renderMapStackChips(container, controller.getStacks(), {
|
||||||
activeId: controller.getActiveId(),
|
activeId: controller.getActiveId(),
|
||||||
onSelect: (id) => { void select(id).catch(() => {}); },
|
onSelect: (id) => {
|
||||||
|
void select(id).catch(() => {});
|
||||||
|
},
|
||||||
bind,
|
bind,
|
||||||
});
|
});
|
||||||
render(controller.getState());
|
render(controller.getState());
|
||||||
@@ -63,7 +74,9 @@ export function createMapSourceControls({
|
|||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
return {
|
return {
|
||||||
render, select, refresh,
|
render,
|
||||||
|
select,
|
||||||
|
refresh,
|
||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ function makeElement(tagName = 'div') {
|
|||||||
children: [],
|
children: [],
|
||||||
classList: {
|
classList: {
|
||||||
toggle(name, force) {
|
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;
|
const next = force === undefined ? !classes.has(name) : !!force;
|
||||||
if (next) classes.add(name);
|
if (next) classes.add(name);
|
||||||
else classes.delete(name);
|
else classes.delete(name);
|
||||||
@@ -26,16 +28,35 @@ function makeElement(tagName = 'div') {
|
|||||||
return String(element.className).split(/\s+/).includes(name);
|
return String(element.className).split(/\s+/).includes(name);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
appendChild(child) { element.children.push(child); return child; },
|
appendChild(child) {
|
||||||
setAttribute(name, value) { element.attributes[name] = String(value); },
|
element.children.push(child);
|
||||||
getAttribute(name) { return element.attributes[name] ?? null; },
|
return child;
|
||||||
addEventListener(type, handler) { (element.listeners[type] ||= []).push(handler); },
|
},
|
||||||
removeEventListener(type, handler) { element.listeners[type] = (element.listeners[type] || []).filter((current) => current !== handler); },
|
setAttribute(name, value) {
|
||||||
click() { for (const handler of element.listeners.click || []) handler(); },
|
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', {
|
Object.defineProperty(element, 'innerHTML', {
|
||||||
get() { return ''; },
|
get() {
|
||||||
set() { element.children.length = 0; },
|
return '';
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
element.children.length = 0;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -44,7 +65,16 @@ function fixture() {
|
|||||||
const container = makeElement();
|
const container = makeElement();
|
||||||
container.ownerDocument = { createElement: (tag) => makeElement(tag) };
|
container.ownerDocument = { createElement: (tag) => makeElement(tag) };
|
||||||
const statusElement = makeElement();
|
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' };
|
let state = { activeId: 'osm', activeStack: sources[0], status: 'ready' };
|
||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
const requests = [];
|
const requests = [];
|
||||||
@@ -55,20 +85,49 @@ function fixture() {
|
|||||||
getState: (status) => ({ ...state, status: status || state.status }),
|
getState: (status) => ({ ...state, status: status || state.status }),
|
||||||
setStack: (id) => {
|
setStack: (id) => {
|
||||||
calls.push(['select', 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,
|
const controls = createMapSourceControls({
|
||||||
subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
|
container,
|
||||||
claimSelection: () => calls.push(['claim']), onStateChanged: () => calls.push(['state']), onError: (message) => calls.push(['error', message]),
|
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) {
|
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();
|
return controller.getState();
|
||||||
}
|
}
|
||||||
const emit = () => { for (const listener of listeners) listener(); };
|
const emit = () => {
|
||||||
return { container, statusElement, controller, controls, requests, calls, listeners, chip, change, 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', () => {
|
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.chip('osm').getAttribute('aria-pressed'), 'true');
|
||||||
assert.equal(f.statusElement.textContent, 'OSM');
|
assert.equal(f.statusElement.textContent, 'OSM');
|
||||||
const unavailable = f.chip('bing-aerial');
|
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');
|
assert.equal(unavailable.getAttribute('aria-disabled'), 'true');
|
||||||
unavailable.click();
|
unavailable.click();
|
||||||
assert.equal(f.requests.length, 0);
|
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', () => {
|
test('provider-driven fallback updates the active chip and durable state notification', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
f.change('esri-imagery'); f.emit();
|
f.change('esri-imagery');
|
||||||
f.change('osm', 'Source unavailable; using OSM'); f.emit();
|
f.emit();
|
||||||
|
f.change('osm', 'Source unavailable; using OSM');
|
||||||
|
f.emit();
|
||||||
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
||||||
assert.equal(f.statusElement.textContent, 'OSM');
|
assert.equal(f.statusElement.textContent, 'OSM');
|
||||||
assert.equal(f.statusElement.classList.contains('warn'), true);
|
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'));
|
f.requests[1].resolve(f.change('osm'));
|
||||||
await second;
|
await second;
|
||||||
const count = f.calls.length;
|
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;
|
await first;
|
||||||
assert.equal(f.statusElement.textContent, 'OSM');
|
assert.equal(f.statusElement.textContent, 'OSM');
|
||||||
assert.equal(f.calls.length, count);
|
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;
|
await pending;
|
||||||
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
||||||
assert.equal(f.statusElement.classList.contains('warn'), 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();
|
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'));
|
f.requests[0].reject(new Error('Request failed'));
|
||||||
await assert.rejects(pending, /Request failed/);
|
await assert.rejects(pending, /Request failed/);
|
||||||
assert.equal(f.statusElement.textContent, 'OSM');
|
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();
|
f.controls.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user