mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(ui): render the map source chips and status badge from the store
createMapSourceControls() (./ui/maps) no longer builds the chip row or writes the status badge: it presents both and hands them to its publish option, so the entry stays Vue-free. The shell publishes them to the gev-display store and connects the controls, and the dock renders the chips and calls select() for an available one. renderMapStackChips and syncMapStackChips are deprecated. The map source QA harnesses wait for the dock to render and no longer read the removed element refs.
This commit is contained in:
1 parent
decac29fbd
commit
4966877859
13 files changed
+233
-155
No files matched your search
@@ -2,69 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createMapSourceControls } from './mapSourceControls.js';
|
||||
|
||||
function makeElement(tagName = 'div') {
|
||||
const element = {
|
||||
tagName,
|
||||
type: '',
|
||||
className: '',
|
||||
title: '',
|
||||
disabled: false,
|
||||
textContent: '',
|
||||
dataset: {},
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
children: [],
|
||||
classList: {
|
||||
toggle(name, force) {
|
||||
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);
|
||||
element.className = [...classes].join(' ');
|
||||
},
|
||||
contains(name) {
|
||||
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();
|
||||
},
|
||||
};
|
||||
Object.defineProperty(element, 'innerHTML', {
|
||||
get() {
|
||||
return '';
|
||||
},
|
||||
set() {
|
||||
element.children.length = 0;
|
||||
},
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
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' },
|
||||
@@ -79,8 +17,9 @@ function fixture() {
|
||||
const listeners = new Set();
|
||||
const requests = [];
|
||||
const calls = [];
|
||||
const views = [];
|
||||
const controller = {
|
||||
getStacks: () => sources,
|
||||
getStacks: () => sources.map((source) => ({ ...source })),
|
||||
getActiveId: () => state.activeId,
|
||||
getState: (status) => ({ ...state, status: status || state.status }),
|
||||
setStack: (id) => {
|
||||
@@ -91,9 +30,8 @@ function fixture() {
|
||||
},
|
||||
};
|
||||
const controls = createMapSourceControls({
|
||||
container,
|
||||
statusElement,
|
||||
controller,
|
||||
publish: (view) => views.push(view),
|
||||
subscribe: (listener) => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
@@ -102,8 +40,12 @@ function fixture() {
|
||||
onStateChanged: () => calls.push(['state']),
|
||||
onError: (message) => calls.push(['error', message]),
|
||||
});
|
||||
const chip = (id) =>
|
||||
container.children.find((el) => el.dataset.stackId === id);
|
||||
const view = () => views.at(-1);
|
||||
const chip = (id) => view().chips.find((model) => model.id === id);
|
||||
const active = () =>
|
||||
view()
|
||||
.chips.filter((model) => model.active)
|
||||
.map((model) => model.id);
|
||||
function change(id, lastError = null) {
|
||||
state = {
|
||||
activeId: id,
|
||||
@@ -117,14 +59,15 @@ function fixture() {
|
||||
for (const listener of listeners) listener();
|
||||
};
|
||||
return {
|
||||
container,
|
||||
statusElement,
|
||||
controller,
|
||||
sources,
|
||||
controls,
|
||||
requests,
|
||||
calls,
|
||||
listeners,
|
||||
views,
|
||||
view,
|
||||
chip,
|
||||
active,
|
||||
change,
|
||||
emit,
|
||||
};
|
||||
@@ -132,17 +75,20 @@ function fixture() {
|
||||
|
||||
test('initial presentation uses actual state and retains unavailable chip semantics', () => {
|
||||
const f = fixture();
|
||||
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.deepEqual(f.active(), ['osm']);
|
||||
assert.deepEqual(f.view().status, { text: 'OSM', warn: false });
|
||||
assert.deepEqual(
|
||||
f.view().chips.map((model) => model.id),
|
||||
['bing-aerial', 'esri-imagery', 'osm'],
|
||||
);
|
||||
assert.equal(unavailable.getAttribute('aria-disabled'), 'true');
|
||||
unavailable.click();
|
||||
assert.equal(f.requests.length, 0);
|
||||
const unavailable = f.chip('bing-aerial');
|
||||
assert.equal(unavailable.available, false);
|
||||
assert.equal(unavailable.title, 'Unavailable for this test');
|
||||
assert.equal(
|
||||
unavailable.ariaLabel,
|
||||
'Bing unavailable: Unavailable for this test',
|
||||
);
|
||||
assert.equal(f.chip('osm').ariaLabel, null);
|
||||
f.controls.destroy();
|
||||
});
|
||||
|
||||
@@ -150,12 +96,12 @@ test('selection claims authority before requesting and never lights a pending so
|
||||
const f = fixture();
|
||||
const pending = f.controls.select('esri-imagery');
|
||||
assert.deepEqual(f.calls, [['claim'], ['select', 'esri-imagery']]);
|
||||
assert.equal(f.statusElement.textContent, '...');
|
||||
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
||||
assert.equal(f.view().status.text, '...');
|
||||
assert.deepEqual(f.active(), ['osm']);
|
||||
f.requests[0].resolve(f.change('esri-imagery'));
|
||||
await pending;
|
||||
assert.equal(f.chip('esri-imagery').getAttribute('aria-pressed'), 'true');
|
||||
assert.equal(f.statusElement.textContent, 'Esri');
|
||||
assert.deepEqual(f.active(), ['esri-imagery']);
|
||||
assert.equal(f.view().status.text, 'Esri');
|
||||
f.controls.destroy();
|
||||
});
|
||||
|
||||
@@ -165,9 +111,8 @@ test('provider-driven fallback updates the active chip and durable state notific
|
||||
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);
|
||||
assert.deepEqual(f.active(), ['osm']);
|
||||
assert.deepEqual(f.view().status, { text: 'OSM', warn: true });
|
||||
assert.deepEqual(f.calls, [['state'], ['state']]);
|
||||
f.controls.destroy();
|
||||
});
|
||||
@@ -185,7 +130,7 @@ test('a late superseded response cannot overwrite the latest displayed selection
|
||||
lastError: 'obsolete failure',
|
||||
});
|
||||
await first;
|
||||
assert.equal(f.statusElement.textContent, 'OSM');
|
||||
assert.equal(f.view().status.text, 'OSM');
|
||||
assert.equal(f.calls.length, count);
|
||||
f.controls.destroy();
|
||||
});
|
||||
@@ -195,8 +140,8 @@ test('failed selection keeps the real source lit and reports its error', async (
|
||||
const pending = f.controls.select('esri-imagery');
|
||||
f.requests[0].resolve(f.change('osm', 'Unavailable'));
|
||||
await pending;
|
||||
assert.equal(f.chip('osm').getAttribute('aria-pressed'), 'true');
|
||||
assert.equal(f.statusElement.classList.contains('warn'), true);
|
||||
assert.deepEqual(f.active(), ['osm']);
|
||||
assert.equal(f.view().status.warn, true);
|
||||
assert.ok(
|
||||
f.calls.some((call) => call[0] === 'error' && call[1] === 'Unavailable'),
|
||||
);
|
||||
@@ -208,7 +153,7 @@ test('rejected selection leaves switching presentation and propagates a useful e
|
||||
const pending = f.controls.select('esri-imagery');
|
||||
f.requests[0].reject(new Error('Request failed'));
|
||||
await assert.rejects(pending, /Request failed/);
|
||||
assert.equal(f.statusElement.textContent, 'OSM');
|
||||
assert.equal(f.view().status.text, 'OSM');
|
||||
assert.ok(
|
||||
f.calls.some((call) => call[0] === 'error' && call[1] === 'Request failed'),
|
||||
);
|
||||
@@ -224,17 +169,15 @@ test('restoration may select without claiming or publishing a user gesture', asy
|
||||
f.controls.destroy();
|
||||
});
|
||||
|
||||
test('refresh removes detached chip listeners and destruction removes all subscriptions', () => {
|
||||
test('availability is read on refresh, and destruction removes the subscription', () => {
|
||||
const f = fixture();
|
||||
const old = f.chip('esri-imagery');
|
||||
f.sources[2].available = true;
|
||||
f.emit();
|
||||
assert.equal(f.chip('bing-aerial').available, false);
|
||||
f.controls.refresh();
|
||||
old.click();
|
||||
assert.equal(f.requests.length, 0);
|
||||
const current = f.chip('esri-imagery');
|
||||
assert.equal(f.chip('bing-aerial').available, true);
|
||||
f.controls.destroy();
|
||||
f.controls.destroy();
|
||||
current.click();
|
||||
assert.equal(f.requests.length, 0);
|
||||
assert.equal(f.listeners.size, 0);
|
||||
});
|
||||
|
||||
@@ -243,10 +186,10 @@ test('completion after destruction cannot paint or notify', async () => {
|
||||
const pending = f.controls.select('esri-imagery');
|
||||
f.controls.destroy();
|
||||
const count = f.calls.length;
|
||||
const label = f.statusElement.textContent;
|
||||
const published = f.views.length;
|
||||
f.requests[0].resolve(f.change('esri-imagery'));
|
||||
await pending;
|
||||
assert.equal(f.calls.length, count);
|
||||
assert.equal(f.statusElement.textContent, label);
|
||||
assert.equal(f.views.length, published);
|
||||
assert.equal(await f.controls.select('osm'), null);
|
||||
});
|
||||
Reference in new issue
Block a user