mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor(maps): separate source factories and scene coordination
This commit is contained in:
1 parent
561a781099
commit
e59282dc9c
22 files changed
+1807
-798
No files matched your search
+312
-149
@@ -34,7 +34,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);
|
||||
@@ -44,15 +46,30 @@ 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); },
|
||||
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);
|
||||
},
|
||||
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;
|
||||
}
|
||||
@@ -60,37 +77,93 @@ function makeElement(tagName = 'div') {
|
||||
const doc = { createElement: (tagName) => makeElement(tagName) };
|
||||
|
||||
/** Text a chip renders, label + optional requirement badge. */
|
||||
const chipText = (chip) => chip.children.map((child) => child.textContent).join(' ');
|
||||
const chipText = (chip) =>
|
||||
chip.children.map((child) => child.textContent).join(' ');
|
||||
|
||||
// Shaped exactly like MapStackController.getStacks() output.
|
||||
const CONTROLLER_STACKS = [
|
||||
{ id: 'photoreal', label: 'Google 3D', requiresIon: false, available: true, unavailableReason: null },
|
||||
{ id: 'bing-aerial', label: 'Bing Aerial', requiresIon: true, available: true, unavailableReason: null },
|
||||
{ id: 'bing-labels', label: 'Bing Labels', requiresIon: true, available: true, unavailableReason: null },
|
||||
{ id: 'esri-imagery', label: 'Esri Satellite', requiresIon: false, available: true, unavailableReason: null },
|
||||
{ id: 'osm', label: 'OSM', requiresIon: false, available: true, unavailableReason: null },
|
||||
{
|
||||
id: 'photoreal',
|
||||
label: 'Google 3D',
|
||||
requiresIon: false,
|
||||
available: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
{
|
||||
id: 'bing-aerial',
|
||||
label: 'Bing Aerial',
|
||||
requiresIon: true,
|
||||
available: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
{
|
||||
id: 'bing-labels',
|
||||
label: 'Bing Labels',
|
||||
requiresIon: true,
|
||||
available: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
{
|
||||
id: 'esri-imagery',
|
||||
label: 'Esri Satellite',
|
||||
requiresIon: false,
|
||||
available: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
{
|
||||
id: 'osm',
|
||||
label: 'OSM',
|
||||
requiresIon: false,
|
||||
available: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
];
|
||||
|
||||
test('the row renders exactly the five owner-approved sources', () => {
|
||||
const container = makeElement();
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'photoreal', doc });
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, {
|
||||
activeId: 'photoreal',
|
||||
doc,
|
||||
});
|
||||
|
||||
assert.deepEqual(container.children.map((chip) => chip.dataset.stackId), [
|
||||
'photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
container.children.map((chip) => chip.dataset.stackId),
|
||||
['photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm'],
|
||||
);
|
||||
assert.deepEqual(container.children.map(chipText), [
|
||||
'Google 3D', 'Bing Aerial', 'Bing Labels', 'Esri Satellite', 'OSM',
|
||||
'Google 3D',
|
||||
'Bing Aerial',
|
||||
'Bing Labels',
|
||||
'Esri Satellite',
|
||||
'OSM',
|
||||
]);
|
||||
assert.deepEqual(PRESENTED_MAP_STACK_IDS, ['photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm']);
|
||||
assert.ok(container.children.every((chip) => chip.tagName === 'button' && chip.type === 'button'));
|
||||
assert.ok(container.children.every((chip) => chip.classList.contains(MAP_STACK_CHIP_CLASS)));
|
||||
assert.deepEqual(PRESENTED_MAP_STACK_IDS, [
|
||||
'photoreal',
|
||||
'bing-aerial',
|
||||
'bing-labels',
|
||||
'esri-imagery',
|
||||
'osm',
|
||||
]);
|
||||
assert.ok(
|
||||
container.children.every(
|
||||
(chip) => chip.tagName === 'button' && chip.type === 'button',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
container.children.every((chip) =>
|
||||
chip.classList.contains(MAP_STACK_CHIP_CLASS),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('internal and future stacks stay outside the approved presentation set', () => {
|
||||
const container = makeElement();
|
||||
// A future Hybrid stack may land in the controller, but it must not appear
|
||||
// until the owner-approved presentation allowlist explicitly includes it.
|
||||
const withHybrid = [...CONTROLLER_STACKS, { id: 'hybrid', label: 'Hybrid', available: true }];
|
||||
const withHybrid = [
|
||||
...CONTROLLER_STACKS,
|
||||
{ id: 'hybrid', label: 'Hybrid', available: true },
|
||||
];
|
||||
renderMapStackChips(container, withHybrid, { activeId: 'photoreal', doc });
|
||||
|
||||
assert.equal(container.children.length, 5);
|
||||
@@ -99,7 +172,10 @@ test('internal and future stacks stay outside the approved presentation set', ()
|
||||
|
||||
test('re-rendering replaces the previous chips instead of stacking a second row', () => {
|
||||
const container = makeElement();
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'photoreal', doc });
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, {
|
||||
activeId: 'photoreal',
|
||||
doc,
|
||||
});
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'osm', doc });
|
||||
|
||||
assert.equal(container.children.length, PRESENTED_MAP_STACK_IDS.length);
|
||||
@@ -122,19 +198,32 @@ test('clicking a chip dispatches that stack id — the same selection the dropdo
|
||||
|
||||
test('the active chip is the pressed chip, and exactly one is pressed', () => {
|
||||
const container = makeElement();
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'bing-labels', doc });
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, {
|
||||
activeId: 'bing-labels',
|
||||
doc,
|
||||
});
|
||||
|
||||
const pressed = container.children.filter((chip) => chip.getAttribute('aria-pressed') === 'true');
|
||||
assert.deepEqual(pressed.map((chip) => chip.dataset.stackId), ['bing-labels']);
|
||||
const pressed = container.children.filter(
|
||||
(chip) => chip.getAttribute('aria-pressed') === 'true',
|
||||
);
|
||||
assert.deepEqual(
|
||||
pressed.map((chip) => chip.dataset.stackId),
|
||||
['bing-labels'],
|
||||
);
|
||||
assert.ok(pressed[0].classList.contains('active'));
|
||||
assert.ok(container.children
|
||||
.filter((chip) => chip.dataset.stackId !== 'bing-labels')
|
||||
.every((chip) => !chip.classList.contains('active')));
|
||||
assert.ok(
|
||||
container.children
|
||||
.filter((chip) => chip.dataset.stackId !== 'bing-labels')
|
||||
.every((chip) => !chip.classList.contains('active')),
|
||||
);
|
||||
});
|
||||
|
||||
test('the lit chip tracks controller state, not the click', () => {
|
||||
const container = makeElement();
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'photoreal', doc });
|
||||
renderMapStackChips(container, CONTROLLER_STACKS, {
|
||||
activeId: 'photoreal',
|
||||
doc,
|
||||
});
|
||||
|
||||
// A rejected/superseded switch reports the stack that is genuinely active.
|
||||
syncMapStackChips(container, 'photoreal');
|
||||
@@ -151,11 +240,16 @@ test('the lit chip tracks controller state, not the click', () => {
|
||||
|
||||
test('keyless ion stacks stay focusable, aria-disabled, and say why', () => {
|
||||
const container = makeElement();
|
||||
const keyless = CONTROLLER_STACKS.map((stack) => (stack.requiresIon ? {
|
||||
...stack,
|
||||
available: false,
|
||||
unavailableReason: 'Needs CESIUM_ION_TOKEN — add it in Provider Settings',
|
||||
} : stack));
|
||||
const keyless = CONTROLLER_STACKS.map((stack) =>
|
||||
stack.requiresIon
|
||||
? {
|
||||
...stack,
|
||||
available: false,
|
||||
unavailableReason:
|
||||
'Needs CESIUM_ION_TOKEN — add it in Provider Settings',
|
||||
}
|
||||
: stack,
|
||||
);
|
||||
const selected = [];
|
||||
renderMapStackChips(container, keyless, {
|
||||
activeId: 'photoreal',
|
||||
@@ -164,59 +258,110 @@ test('keyless ion stacks stay focusable, aria-disabled, and say why', () => {
|
||||
});
|
||||
|
||||
const bingAerial = container.children[1];
|
||||
assert.equal(bingAerial.disabled, false, 'unavailable sources remain keyboard-reachable');
|
||||
assert.equal(
|
||||
bingAerial.disabled,
|
||||
false,
|
||||
'unavailable sources remain keyboard-reachable',
|
||||
);
|
||||
assert.equal(bingAerial.getAttribute('aria-disabled'), 'true');
|
||||
assert.equal(
|
||||
bingAerial.getAttribute('aria-label'),
|
||||
'Bing Aerial unavailable: Needs CESIUM_ION_TOKEN — add it in Provider Settings',
|
||||
);
|
||||
assert.ok(bingAerial.classList.contains('unavailable'));
|
||||
assert.equal(bingAerial.title, 'Needs CESIUM_ION_TOKEN — add it in Provider Settings');
|
||||
assert.equal(
|
||||
bingAerial.title,
|
||||
'Needs CESIUM_ION_TOKEN — add it in Provider Settings',
|
||||
);
|
||||
assert.equal(chipText(bingAerial), 'Bing Aerial ION');
|
||||
|
||||
bingAerial.click();
|
||||
assert.deepEqual(selected, [], 'an unavailable stack must not reach the switch path');
|
||||
assert.deepEqual(
|
||||
selected,
|
||||
[],
|
||||
'an unavailable stack must not reach the switch path',
|
||||
);
|
||||
|
||||
assert.equal(container.children[3].getAttribute('aria-disabled'), 'false', 'OSM stays selectable');
|
||||
assert.equal(
|
||||
container.children[3].getAttribute('aria-disabled'),
|
||||
'false',
|
||||
'OSM stays selectable',
|
||||
);
|
||||
});
|
||||
|
||||
test('a non-ion stack that fails never claims an ion token is required', () => {
|
||||
// The startup fallback-to-OSM case: Google 3D tiles failed to load, so
|
||||
// photoreal is unavailable for a reason that has nothing to do with ion.
|
||||
const container = makeElement();
|
||||
const tilesFailed = CONTROLLER_STACKS.map((stack) => (stack.id === 'photoreal' ? {
|
||||
...stack,
|
||||
available: false,
|
||||
unavailableReason: 'Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings',
|
||||
} : stack));
|
||||
const tilesFailed = CONTROLLER_STACKS.map((stack) =>
|
||||
stack.id === 'photoreal'
|
||||
? {
|
||||
...stack,
|
||||
available: false,
|
||||
unavailableReason:
|
||||
'Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings',
|
||||
}
|
||||
: stack,
|
||||
);
|
||||
renderMapStackChips(container, tilesFailed, { activeId: 'osm', doc });
|
||||
|
||||
const google = container.children[0];
|
||||
assert.equal(google.getAttribute('aria-disabled'), 'true');
|
||||
assert.equal(google.getAttribute('aria-label'), 'Google 3D unavailable: Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings');
|
||||
assert.equal(chipText(google), 'Google 3D', 'no ION badge on a stack that does not need ion');
|
||||
assert.equal(google.title, 'Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings');
|
||||
assert.equal(chipText(container.children[1]), 'Bing Aerial', 'available ion stacks stay unbadged');
|
||||
assert.equal(
|
||||
google.getAttribute('aria-label'),
|
||||
'Google 3D unavailable: Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings',
|
||||
);
|
||||
assert.equal(
|
||||
chipText(google),
|
||||
'Google 3D',
|
||||
'no ION badge on a stack that does not need ion',
|
||||
);
|
||||
assert.equal(
|
||||
google.title,
|
||||
'Needs GOOGLE_MAPS_API_KEY — add it in Provider Settings',
|
||||
);
|
||||
assert.equal(
|
||||
chipText(container.children[1]),
|
||||
'Bing Aerial',
|
||||
'available ion stacks stay unbadged',
|
||||
);
|
||||
});
|
||||
|
||||
test('models carry the stack\'s own reason and never invent an active chip', () => {
|
||||
assert.deepEqual(mapStackChipModels([{ id: 'osm', label: 'OSM' }], null), [{
|
||||
id: 'osm',
|
||||
label: 'OSM',
|
||||
available: true,
|
||||
active: false,
|
||||
requiresIon: false,
|
||||
requirement: '',
|
||||
unavailableHint: '',
|
||||
title: 'OSM',
|
||||
}]);
|
||||
test("models carry the stack's own reason and never invent an active chip", () => {
|
||||
assert.deepEqual(mapStackChipModels([{ id: 'osm', label: 'OSM' }], null), [
|
||||
{
|
||||
id: 'osm',
|
||||
label: 'OSM',
|
||||
available: true,
|
||||
active: false,
|
||||
requiresIon: false,
|
||||
requirement: '',
|
||||
unavailableHint: '',
|
||||
title: 'OSM',
|
||||
},
|
||||
]);
|
||||
|
||||
// A stack list without a controller-supplied reason still explains itself.
|
||||
assert.deepEqual(
|
||||
[
|
||||
mapStackChipModel({ id: 'bing-aerial', label: 'Bing Aerial', requiresIon: true, available: false }, null),
|
||||
mapStackChipModel({ id: 'hybrid', label: 'Hybrid', available: false }, null),
|
||||
].map(({ requirement, unavailableHint, title }) => ({ requirement, unavailableHint, title })),
|
||||
mapStackChipModel(
|
||||
{
|
||||
id: 'bing-aerial',
|
||||
label: 'Bing Aerial',
|
||||
requiresIon: true,
|
||||
available: false,
|
||||
},
|
||||
null,
|
||||
),
|
||||
mapStackChipModel(
|
||||
{ id: 'hybrid', label: 'Hybrid', available: false },
|
||||
null,
|
||||
),
|
||||
].map(({ requirement, unavailableHint, title }) => ({
|
||||
requirement,
|
||||
unavailableHint,
|
||||
title,
|
||||
})),
|
||||
[
|
||||
{
|
||||
requirement: 'ION',
|
||||
@@ -234,31 +379,12 @@ test('models carry the stack\'s own reason and never invent an active chip', ()
|
||||
assert.deepEqual(mapStackChipModels(undefined, 'osm'), []);
|
||||
});
|
||||
|
||||
test('the controller is the single source of the unavailability reason', () => {
|
||||
const controller = readFileSync(new URL('./mapStackController.js', import.meta.url), 'utf8');
|
||||
|
||||
assert.doesNotMatch(
|
||||
controller,
|
||||
/bing-road/,
|
||||
'Bing Road is retired: an old map=bing-road link must take the unknown-id photoreal fallback',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
controller,
|
||||
/getStacks\(\) \{[\s\S]*?unavailableReason: available \? null : this\._unavailableReason\(stack\)/,
|
||||
'getStacks() must expose why a stack is unavailable, not just that it is',
|
||||
);
|
||||
assert.match(
|
||||
controller,
|
||||
/if \(!this\.isStackAvailable\(stack\.id\)\) \{\s*const message = this\._unavailableReason\(stack\);/,
|
||||
'the toast and the chip tooltip must read the same reason',
|
||||
);
|
||||
assert.match(controller, /_unavailableReason\(stack\) \{[\s\S]*?stack\?\.requiresIon/);
|
||||
});
|
||||
|
||||
test('a missing row or document is inert rather than throwing during boot', () => {
|
||||
assert.deepEqual(renderMapStackChips(null, CONTROLLER_STACKS, { doc }), []);
|
||||
assert.deepEqual(renderMapStackChips(makeElement(), CONTROLLER_STACKS, { doc: {} }), []);
|
||||
assert.deepEqual(
|
||||
renderMapStackChips(makeElement(), CONTROLLER_STACKS, { doc: {} }),
|
||||
[],
|
||||
);
|
||||
assert.doesNotThrow(() => syncMapStackChips(null, 'osm'));
|
||||
});
|
||||
|
||||
@@ -268,8 +394,14 @@ test('the active cyan survives hover', () => {
|
||||
const active = css.indexOf('.map-stack-chip.active {');
|
||||
const unavailable = css.indexOf('.map-stack-chip.unavailable');
|
||||
|
||||
assert.ok(hover > 0 && active > hover, 'active must follow hover so it wins at equal specificity');
|
||||
assert.ok(unavailable > active, 'unavailable must follow both so a keyless chip never lights up');
|
||||
assert.ok(
|
||||
hover > 0 && active > hover,
|
||||
'active must follow hover so it wins at equal specificity',
|
||||
);
|
||||
assert.ok(
|
||||
unavailable > active,
|
||||
'unavailable must follow both so a keyless chip never lights up',
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
css.slice(hover, active),
|
||||
/:not\(/,
|
||||
@@ -285,18 +417,25 @@ test('the keyboard focus ring survives on the ACTIVE chip', () => {
|
||||
// chip-state rule sets.
|
||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||
const stripComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
const chipRules = [...css.matchAll(/([^{}]*\.map-stack-chip[^{}]*)\{([^{}]*)\}/g)]
|
||||
.map(([, selector, body], order) => ({
|
||||
selector: stripComments(selector).trim(),
|
||||
body: stripComments(body),
|
||||
order,
|
||||
}));
|
||||
const chipRules = [
|
||||
...css.matchAll(/([^{}]*\.map-stack-chip[^{}]*)\{([^{}]*)\}/g),
|
||||
].map(([, selector, body], order) => ({
|
||||
selector: stripComments(selector).trim(),
|
||||
body: stripComments(body),
|
||||
order,
|
||||
}));
|
||||
assert.ok(chipRules.length >= 5, 'expected the chip state rules to be found');
|
||||
|
||||
const ringIndex = chipRules.findIndex(({ selector, body }) => selector.includes(':focus-visible')
|
||||
&& /outline:\s*(?!none)\S/.test(body)
|
||||
&& /outline-offset:/.test(body));
|
||||
assert.ok(ringIndex >= 0, 'a :focus-visible rule must draw a real outline ring');
|
||||
const ringIndex = chipRules.findIndex(
|
||||
({ selector, body }) =>
|
||||
selector.includes(':focus-visible') &&
|
||||
/outline:\s*(?!none)\S/.test(body) &&
|
||||
/outline-offset:/.test(body),
|
||||
);
|
||||
assert.ok(
|
||||
ringIndex >= 0,
|
||||
'a :focus-visible rule must draw a real outline ring',
|
||||
);
|
||||
|
||||
// Nothing after it may touch outline again, so the ring cannot be erased by
|
||||
// .active, :disabled, or anything added later.
|
||||
@@ -307,7 +446,10 @@ test('the keyboard focus ring survives on the ACTIVE chip', () => {
|
||||
`"${rule.selector}" must not touch outline — it would erase the focus ring`,
|
||||
);
|
||||
}
|
||||
for (const selector of ['.map-stack-chip.active', '.map-stack-chip.unavailable']) {
|
||||
for (const selector of [
|
||||
'.map-stack-chip.active',
|
||||
'.map-stack-chip.unavailable',
|
||||
]) {
|
||||
assert.ok(
|
||||
chipRules.some((rule) => rule.selector.includes(selector)),
|
||||
`expected a ${selector} rule to exist for this check to mean anything`,
|
||||
@@ -317,83 +459,104 @@ test('the keyboard focus ring survives on the ACTIVE chip', () => {
|
||||
// `transition: all` animates outline-width off the `outline: none` base, and
|
||||
// Chrome parks that transition at 0px for chips on a wrapped line — the ring
|
||||
// never appeared on rows 2 and 3. The base rule must list its properties.
|
||||
const base = chipRules.find((rule) => rule.selector.endsWith('.map-stack-chip'));
|
||||
const base = chipRules.find((rule) =>
|
||||
rule.selector.endsWith('.map-stack-chip'),
|
||||
);
|
||||
assert.ok(base, 'expected the base .map-stack-chip rule');
|
||||
assert.doesNotMatch(
|
||||
base.body,
|
||||
/transition:\s*all\b/,
|
||||
'transition: all animates outline-width and kills the focus ring on wrapped rows',
|
||||
);
|
||||
assert.doesNotMatch(base.body, /transition:[^;]*outline/, 'the focus ring must not be animated');
|
||||
assert.match(base.body, /transition:\s*\n?\s*color/, 'the hover/active treatment still animates');
|
||||
assert.doesNotMatch(
|
||||
base.body,
|
||||
/transition:[^;]*outline/,
|
||||
'the focus ring must not be animated',
|
||||
);
|
||||
assert.match(
|
||||
base.body,
|
||||
/transition:\s*\n?\s*color/,
|
||||
'the hover/active treatment still animates',
|
||||
);
|
||||
});
|
||||
|
||||
test('the Visual Presets tray owns Map Source and the retired left panel is absent', () => {
|
||||
const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||
const ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8');
|
||||
const ui = readFileSync(
|
||||
new URL('./ui/applicationShell.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.doesNotMatch(html, /map-stack-select/, 'the SOURCE dropdown is replaced by the chip row');
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/map-stack-select/,
|
||||
'the SOURCE dropdown is replaced by the chip row',
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
/<section class="map-source-section"[\s\S]*?<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source"><\/div>/,
|
||||
);
|
||||
assert.doesNotMatch(html, /id="stack-panel"/, 'the duplicate left MAP STACK panel is retired');
|
||||
assert.match(html, /id="map-source-label">MAP SOURCE<[\s\S]*?id="map-stack-status"/);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/id="stack-panel"/,
|
||||
'the duplicate left MAP STACK panel is retired',
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
/id="map-source-label">MAP SOURCE<[\s\S]*?id="map-stack-status"/,
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
/<button id="control-panel-toggle"[\s\S]*?data-dock-toggle-target="control-panel"[\s\S]*?aria-controls="control-panel-popover"/,
|
||||
'the compact wing must expose a semantic keyboard disclosure',
|
||||
);
|
||||
const panels = readFileSync(new URL('./ui/panelDisclosure.js', import.meta.url), 'utf8');
|
||||
const panels = readFileSync(
|
||||
new URL('./ui/panelDisclosure.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(panels, /event\.key !== 'Escape'[\s\S]*?disclosure\?\.focus/);
|
||||
assert.match(ui, /querySelector\(\s*'\.map-stack-chip\.active',?\s*\)\s*\|\|\s*panel\.querySelector\('\.map-stack-chip'\)/);
|
||||
|
||||
const controls = readFileSync(new URL('./ui/mapSourceControls.js', import.meta.url), 'utf8');
|
||||
assert.match(ui, /return this\._mapSourceControls\.select\(stackId, \{ syncShare \}\)/,
|
||||
'the application action uses the component selection path');
|
||||
assert.match(controls, /onSelect: [\s\S]*?select\(id\)/,
|
||||
'chip clicks use the same component selection path');
|
||||
assert.match(controls, /await controller\.setStack\(stackId\)/,
|
||||
'selection still delegates source loading to the map controller');
|
||||
assert.match(controls, /render\(controller\.getState\(\)\)/,
|
||||
'the active chip is re-synced from actual controller state');
|
||||
assert.match(ui, /window\.addEventListener\('gev:map-stack-changed', onChange\)/,
|
||||
'provider-driven fallback reaches the component without a user click');
|
||||
assert.match(ui, /window\.removeEventListener\('gev:map-stack-changed', onChange\)/,
|
||||
'the provider-driven subscription has an explicit remover');
|
||||
assert.match(controls, /unsubscribe\?\.\(\)/,
|
||||
'component destruction releases its subscription');
|
||||
|
||||
});
|
||||
|
||||
test('Esri fallbacks report and attribute the imagery source actually rendered', () => {
|
||||
const controller = readFileSync(new URL('./mapStackController.js', import.meta.url), 'utf8');
|
||||
assert.match(
|
||||
controller,
|
||||
/effectiveStackId = 'osm';[\s\S]*?fallbackMessage = 'Esri Satellite is unavailable; using OSM'/,
|
||||
'construction failure must resolve truthfully to OSM',
|
||||
ui,
|
||||
/querySelector\(\s*'\.map-stack-chip\.active',?\s*\)\s*\|\|\s*panel\.querySelector\('\.map-stack-chip'\)/,
|
||||
);
|
||||
|
||||
const controls = readFileSync(
|
||||
new URL('./ui/mapSourceControls.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(
|
||||
controller,
|
||||
/this\._activeId = activation\?\.effectiveStackId \|\| stack\.id/,
|
||||
'the active chip must follow the effective provider, not the requested stack',
|
||||
ui,
|
||||
/return this\._mapSourceControls\.select\(stackId, \{ syncShare \}\)/,
|
||||
'the application action uses the component selection path',
|
||||
);
|
||||
assert.match(
|
||||
controller,
|
||||
/this\._syncEsriAttribution\(resolution\.effectiveStackId\)/,
|
||||
'Esri credit must follow the effective provider',
|
||||
controls,
|
||||
/onSelect: [\s\S]*?select\(id\)/,
|
||||
'chip clicks use the same component selection path',
|
||||
);
|
||||
});
|
||||
|
||||
test('repeated active Esri tile failures fall back to OSM and one transient does not', () => {
|
||||
const controller = readFileSync(new URL('./mapStackController.js', import.meta.url), 'utf8');
|
||||
assert.match(controller, /let failures = 0/);
|
||||
assert.match(controller, /if \(failures < 2 \|\| this\._esriFallbackPending\) return/);
|
||||
assert.match(controller, /this\.setStack\('osm', \{ silent: true \}\)/);
|
||||
assert.match(controller, /state\?\.activeId === 'osm'[\s\S]*?this\._emitChange\('error'\)/);
|
||||
assert.match(
|
||||
controller,
|
||||
/gen !== this\._switchGen \|\| this\._activeImageryProvider !== resolution\.provider/,
|
||||
'a stale provider error must not replace a newer user selection',
|
||||
controls,
|
||||
/await controller\.setStack\(stackId\)/,
|
||||
'selection still delegates source loading to the map controller',
|
||||
);
|
||||
assert.match(
|
||||
controls,
|
||||
/render\(controller\.getState\(\)\)/,
|
||||
'the active chip is re-synced from actual controller state',
|
||||
);
|
||||
assert.match(
|
||||
ui,
|
||||
/window\.addEventListener\('gev:map-stack-changed', onChange\)/,
|
||||
'provider-driven fallback reaches the component without a user click',
|
||||
);
|
||||
assert.match(
|
||||
ui,
|
||||
/window\.removeEventListener\('gev:map-stack-changed', onChange\)/,
|
||||
'the provider-driven subscription has an explicit remover',
|
||||
);
|
||||
assert.match(
|
||||
controls,
|
||||
/unsubscribe\?\.\(\)/,
|
||||
'component destruction releases its subscription',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user