mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge main into the Utilities panel, rebuilt on the Vue panel rail
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
This commit is contained in:
commit
4ebb287d36
699 files changed
+46673
-20476
No files matched your search
+98
-163
@@ -1,3 +1,4 @@
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -34,14 +35,8 @@ test('panel presentation places Transit between Street Traffic and Bike Share in
|
||||
});
|
||||
|
||||
test('partial feed controls distinguish incomplete records from stale data and outages', async () => {
|
||||
const { LayerPanel, layerFeedState } = await import('./layerPanel.js');
|
||||
const classes = new Map();
|
||||
const attrs = new Map();
|
||||
const button = {
|
||||
classList: { toggle: (key, value) => classes.set(key, value) },
|
||||
dataset: {},
|
||||
setAttribute: (key, value) => attrs.set(key, value),
|
||||
};
|
||||
const { LayerPanel, layerFeedState, presentLayerToggle } =
|
||||
await import('./layerPanel.js');
|
||||
const layer = {
|
||||
id: 'ais-live-vessels',
|
||||
name: 'Live Vessels',
|
||||
@@ -57,12 +52,12 @@ test('partial feed controls distinguish incomplete records from stale data and o
|
||||
},
|
||||
};
|
||||
const panel = LayerPanel.prototype;
|
||||
panel._syncToggleButton(button, layer);
|
||||
assert.equal(button.textContent, 'PARTIAL');
|
||||
assert.equal(button.dataset.feedState, 'partial');
|
||||
assert.equal(classes.get('feed-partial'), true);
|
||||
assert.equal(classes.get('feed-stale'), false);
|
||||
assert.match(attrs.get('aria-label'), /PARTIAL/);
|
||||
let toggle = presentLayerToggle(layer);
|
||||
assert.equal(toggle.text, 'PARTIAL');
|
||||
assert.equal(toggle.feedState, 'partial');
|
||||
assert.equal(toggle.classes['feed-partial'], true);
|
||||
assert.equal(toggle.classes['feed-stale'], false);
|
||||
assert.match(toggle.ariaLabel, /PARTIAL/);
|
||||
assert.match(
|
||||
panel._buildMetaText(layer),
|
||||
/^PARTIAL · AISStream · 2 of 3 records accepted · /,
|
||||
@@ -85,12 +80,11 @@ test('partial feed controls distinguish incomplete records from stale data and o
|
||||
);
|
||||
assert.equal(layerFeedState({ ...layer.stats, loading: true }), 'loading');
|
||||
layer.stats = { ...layer.stats, partial: false };
|
||||
panel._syncToggleButton(button, layer);
|
||||
assert.equal(button.textContent, 'ON');
|
||||
assert.equal(classes.get('feed-partial'), false);
|
||||
toggle = presentLayerToggle(layer);
|
||||
assert.equal(toggle.text, 'ON');
|
||||
assert.equal(toggle.classes['feed-partial'], false);
|
||||
layer.enabled = false;
|
||||
panel._syncToggleButton(button, layer);
|
||||
assert.equal(button.textContent, 'OFF');
|
||||
assert.equal(presentLayerToggle(layer).text, 'OFF');
|
||||
});
|
||||
|
||||
test('a stale row dates cached data by its oldest tile, not the last load', async () => {
|
||||
@@ -116,10 +110,9 @@ test('a stale row dates cached data by its oldest tile, not the last load', asyn
|
||||
|
||||
test('readout rows contain only toggles and metadata; ordinary rows retain controls', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const { railFixture } = await import('./railTestFixture.mjs');
|
||||
const f = railFixture();
|
||||
const previousDocument = globalThis.document;
|
||||
globalThis.document = f.document;
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
const find = (id) => container.querySelector(`[data-layer-id="${id}"]`);
|
||||
const ids = [
|
||||
'wind',
|
||||
'weather-radar',
|
||||
@@ -160,9 +153,9 @@ test('readout rows contain only toggles and metadata; ordinary rows retain contr
|
||||
}),
|
||||
});
|
||||
try {
|
||||
panel.mount(f.container);
|
||||
panel.mount(container);
|
||||
for (const id of ids.slice(0, -1)) {
|
||||
const row = f.find((n) => n.dataset.layerId === id);
|
||||
const row = find(id);
|
||||
assert.equal(row.children.length, 2);
|
||||
assert.ok(row.querySelector('.data-name'));
|
||||
assert.ok(row.querySelector('.data-toggle-meta'));
|
||||
@@ -173,14 +166,14 @@ test('readout rows contain only toggles and metadata; ordinary rows retain contr
|
||||
'data-toggle-controls-status',
|
||||
'data-row-list',
|
||||
])
|
||||
assert.equal(row.querySelector(`.${cls}`), null);
|
||||
assert.ok(!row.querySelector(`.${cls}`), cls);
|
||||
row.querySelector('.data-toggle-btn').click();
|
||||
}
|
||||
assert.deepEqual(
|
||||
calls.map(([id]) => id),
|
||||
ids.slice(0, -1),
|
||||
);
|
||||
const ordinary = f.find((n) => n.dataset.layerId === 'other');
|
||||
const ordinary = find('other');
|
||||
assert.equal(
|
||||
ordinary.querySelector('.data-toggle-controls-info').textContent,
|
||||
'Detailed time',
|
||||
@@ -195,19 +188,16 @@ test('readout rows contain only toggles and metadata; ordinary rows retain contr
|
||||
assert.equal(typeof refresh, 'function');
|
||||
} finally {
|
||||
panel.destroy();
|
||||
globalThis.document = previousDocument;
|
||||
document.body.replaceChildren();
|
||||
}
|
||||
});
|
||||
|
||||
test('the Recent Imagery readout mounts in its rail body like the weather readout and is rebuilt or released with the panel', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const { railFixture } = await import('./railTestFixture.mjs');
|
||||
const f = railFixture();
|
||||
const body = f.document.createElement('div');
|
||||
f.document.getElementById = (id) =>
|
||||
id === 'recent-imagery-panel-body' ? body : null;
|
||||
const previousDocument = globalThis.document;
|
||||
globalThis.document = f.document;
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
const body = document.body.appendChild(document.createElement('div'));
|
||||
body.id = 'recent-imagery-panel-body';
|
||||
const mounted = [];
|
||||
const factory = (container) => {
|
||||
const readout = {
|
||||
@@ -232,10 +222,10 @@ test('the Recent Imagery readout mounts in its rail body like the weather readou
|
||||
try {
|
||||
panel.attachRecentImagery(factory);
|
||||
assert.equal(mounted.length, 0, 'no body before the panel mounts');
|
||||
panel.mount(f.container);
|
||||
panel.mount(container);
|
||||
assert.equal(mounted.length, 1);
|
||||
assert.equal(mounted[0].container, body);
|
||||
panel.mount(f.container);
|
||||
assert.ok(mounted[0].container === body);
|
||||
panel.mount(container);
|
||||
assert.equal(mounted[0].destroyed, true, 'a remount rebuilds it');
|
||||
assert.equal(mounted.length, 2);
|
||||
panel.attachRecentImagery(null);
|
||||
@@ -248,18 +238,12 @@ test('the Recent Imagery readout mounts in its rail body like the weather readou
|
||||
assert.equal(mounted.length, 3, 'a destroyed panel mounts nothing');
|
||||
} finally {
|
||||
panel.destroy();
|
||||
globalThis.document = previousDocument;
|
||||
document.body.replaceChildren();
|
||||
}
|
||||
});
|
||||
|
||||
test('an omitted source is visibly unavailable and cannot be toggled on', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const attrs = new Map();
|
||||
const button = {
|
||||
classList: { toggle() {} },
|
||||
dataset: {},
|
||||
setAttribute: (key, value) => attrs.set(key, value),
|
||||
};
|
||||
const { presentLayerToggle } = await import('./layerPanel.js');
|
||||
const layer = {
|
||||
id: 'flights',
|
||||
name: 'Flights',
|
||||
@@ -270,94 +254,63 @@ test('an omitted source is visibly unavailable and cannot be toggled on', async
|
||||
error: 'Data source not configured: flights',
|
||||
},
|
||||
};
|
||||
LayerPanel.prototype._syncToggleButton(button, layer);
|
||||
assert.equal(button.textContent, 'UNAVAILABLE');
|
||||
assert.equal(attrs.get('aria-disabled'), 'true');
|
||||
assert.match(button.title, /not configured/);
|
||||
LayerPanel.prototype._syncToggleButton(button, { ...layer, enabled: true });
|
||||
const toggle = presentLayerToggle(layer);
|
||||
assert.equal(toggle.text, 'UNAVAILABLE');
|
||||
assert.equal(toggle.ariaDisabled, true);
|
||||
assert.match(toggle.title, /not configured/);
|
||||
assert.equal(
|
||||
attrs.get('aria-disabled'),
|
||||
'false',
|
||||
presentLayerToggle({ ...layer, enabled: true }).ariaDisabled,
|
||||
false,
|
||||
'an active layer can always be switched off',
|
||||
);
|
||||
});
|
||||
|
||||
test('row text keeps a hyphenated code whole and never starts a line with the separator', async () => {
|
||||
const { setRowText } = await import('./layerPanel.js');
|
||||
const previousDocument = globalThis.document;
|
||||
const element = () => ({
|
||||
className: '',
|
||||
textContent: '',
|
||||
children: [],
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
},
|
||||
});
|
||||
globalThis.document = { createElement: element };
|
||||
try {
|
||||
const text = (node) =>
|
||||
node.children.length
|
||||
? node.children.map((child) => child.textContent).join('')
|
||||
: node.textContent;
|
||||
const kept = (node) =>
|
||||
node.children
|
||||
.filter((child) => child.className === 'data-keep')
|
||||
.map((child) => child.textContent);
|
||||
const { rowTextRuns } = await import('./layerRowText.js');
|
||||
const text = (runs) => runs.map((run) => run.text).join('');
|
||||
const kept = (runs) => runs.filter((run) => run.keep).map((run) => run.text);
|
||||
|
||||
const name = element();
|
||||
setRowText(name, 'Локальный ADS-B');
|
||||
assert.equal(text(name), 'Локальный ADS-B');
|
||||
assert.deepEqual(kept(name), ['ADS-B']);
|
||||
const name = rowTextRuns('Локальный ADS-B');
|
||||
assert.equal(text(name), 'Локальный ADS-B');
|
||||
assert.deepEqual(kept(name), ['ADS-B']);
|
||||
|
||||
const meta = element();
|
||||
setRowText(meta, 'UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.equal(text(meta), 'UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.deepEqual(kept(meta), [' ·', ' ·', ' ·']);
|
||||
assert.ok(
|
||||
meta.children.every((child) => !child.textContent.startsWith('·')),
|
||||
'the space before each dot is held to the piece it closes',
|
||||
);
|
||||
const meta = rowTextRuns('UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.equal(text(meta), 'UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.deepEqual(kept(meta), [' ·', ' ·', ' ·']);
|
||||
assert.ok(
|
||||
meta.every((run) => !run.text.startsWith('·')),
|
||||
'the space before each dot is held to the piece it closes',
|
||||
);
|
||||
|
||||
const plain = element();
|
||||
setRowText(plain, 'Satellites');
|
||||
assert.equal(plain.textContent, 'Satellites');
|
||||
assert.equal(plain.children.length, 0);
|
||||
assert.equal(rowTextRuns('Satellites'), null, 'plain text needs no runs');
|
||||
|
||||
const compound = element();
|
||||
setRowText(compound, 'Kartierte ALPR-Kameras · ADS-B-Flüge');
|
||||
assert.equal(text(compound), 'Kartierte ALPR-Kameras · ADS-B-Flüge');
|
||||
assert.deepEqual(
|
||||
kept(compound),
|
||||
[' ·', 'ADS-B'],
|
||||
'a compound may break at its hyphen; the code inside it stays whole',
|
||||
);
|
||||
const compound = rowTextRuns('Kartierte ALPR-Kameras · ADS-B-Flüge');
|
||||
assert.equal(text(compound), 'Kartierte ALPR-Kameras · ADS-B-Flüge');
|
||||
assert.deepEqual(
|
||||
kept(compound),
|
||||
[' ·', 'ADS-B'],
|
||||
'a compound may break at its hyphen; the code inside it stays whole',
|
||||
);
|
||||
|
||||
const long = element();
|
||||
setRowText(long, 'see https-proxy-endpoint-config-x');
|
||||
assert.deepEqual(kept(long), [], 'a long hyphenated run may still wrap');
|
||||
} finally {
|
||||
globalThis.document = previousDocument;
|
||||
}
|
||||
const long = rowTextRuns('see https-proxy-endpoint-config-x');
|
||||
assert.deepEqual(
|
||||
kept(long || []),
|
||||
[],
|
||||
'a long hyphenated run may still wrap',
|
||||
);
|
||||
});
|
||||
|
||||
test('the unavailable chip may abbreviate, but its accessible name keeps the full state', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const attrs = new Map();
|
||||
const button = {
|
||||
classList: { toggle() {} },
|
||||
dataset: {},
|
||||
setAttribute: (key, value) => attrs.set(key, value),
|
||||
};
|
||||
LayerPanel.prototype._syncToggleButton(button, {
|
||||
const { presentLayerToggle } = await import('./layerPanel.js');
|
||||
const toggle = presentLayerToggle({
|
||||
id: 'local-firms',
|
||||
name: 'FIRMS Active Fires',
|
||||
enabled: false,
|
||||
stats: { sourceUnavailable: true, error: 'KEY REQUIRED' },
|
||||
});
|
||||
assert.equal(button.textContent, 'UNAVAILABLE');
|
||||
assert.equal(button.dataset.feedState, 'unavailable');
|
||||
assert.match(attrs.get('aria-label'), /^Active Fires: UNAVAILABLE\./);
|
||||
assert.equal(toggle.text, 'UNAVAILABLE');
|
||||
assert.equal(toggle.feedState, 'unavailable');
|
||||
assert.match(toggle.ariaLabel, /^Active Fires: UNAVAILABLE\./);
|
||||
});
|
||||
|
||||
test('the Weather readout receives enabled air-quality alongside weather layers', async () => {
|
||||
@@ -391,30 +344,12 @@ test('the Weather readout receives enabled air-quality alongside weather layers'
|
||||
// Directions renders in the Utilities panel's list; Data Layers keeps the rest.
|
||||
async function utilitiesFixture({ withUtilities = true } = {}) {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const { railFixture } = await import('./railTestFixture.mjs');
|
||||
const f = railFixture();
|
||||
// Rows are looked up by layer id on refresh; a render clears with innerHTML.
|
||||
const findable = (node) => {
|
||||
Object.defineProperty(node, 'innerHTML', {
|
||||
set(html) {
|
||||
if (html === '') for (const child of [...node.children]) child.remove();
|
||||
},
|
||||
});
|
||||
const querySelector = node.querySelector.bind(node);
|
||||
node.querySelector = (selector) => {
|
||||
const id = /^\[data-layer-id="(.+)"\]$/.exec(selector)?.[1];
|
||||
return id
|
||||
? f.find((child) => child.dataset?.layerId === id, node)
|
||||
: querySelector(selector);
|
||||
};
|
||||
return node;
|
||||
};
|
||||
findable(f.container);
|
||||
const utilities = findable(f.document.createElement('div'));
|
||||
f.document.getElementById = (id) =>
|
||||
withUtilities && id === 'utilities-toggles' ? utilities : null;
|
||||
const previousDocument = globalThis.document;
|
||||
globalThis.document = f.document;
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
container.id = 'data-toggles';
|
||||
const utilities = document.createElement('div');
|
||||
utilities.id = 'utilities-toggles';
|
||||
if (withUtilities) document.body.append(utilities);
|
||||
const layers = ['radio', 'directions', 'flights'].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
@@ -433,65 +368,65 @@ async function utilitiesFixture({ withUtilities = true } = {}) {
|
||||
hasRowControls: () => false,
|
||||
subscribeRowControls: () => () => {},
|
||||
});
|
||||
panel.mount(f.container);
|
||||
panel.mount(container);
|
||||
const headings = (root) =>
|
||||
root.children
|
||||
.filter((node) => node.tagName === 'H3')
|
||||
.map((node) => node.textContent);
|
||||
[...root.querySelectorAll('.data-layer-group-heading')].map((node) =>
|
||||
node.textContent.trim(),
|
||||
);
|
||||
const rowIds = (root) =>
|
||||
root.children
|
||||
.filter((node) => node.dataset?.layerId)
|
||||
.map((node) => node.dataset.layerId);
|
||||
[...root.querySelectorAll('[data-layer-id]')].map(
|
||||
(node) => node.dataset.layerId,
|
||||
);
|
||||
const restore = () => {
|
||||
panel.destroy();
|
||||
globalThis.document = previousDocument;
|
||||
document.body.replaceChildren();
|
||||
};
|
||||
return { f, panel, utilities, layers, headings, rowIds, restore };
|
||||
return { container, utilities, panel, layers, headings, rowIds, restore };
|
||||
}
|
||||
|
||||
test('Directions renders in the Utilities list without a heading, and Radio under Audio', async () => {
|
||||
const { f, panel, utilities, headings, rowIds, restore } =
|
||||
const { container, utilities, panel, headings, rowIds, restore } =
|
||||
await utilitiesFixture();
|
||||
try {
|
||||
assert.deepEqual(rowIds(utilities), ['directions']);
|
||||
assert.deepEqual(headings(utilities), []);
|
||||
assert.deepEqual(rowIds(f.container), ['flights', 'radio']);
|
||||
assert.deepEqual(headings(f.container), ['Movement', 'Audio']);
|
||||
panel.mount(f.container);
|
||||
assert.deepEqual(rowIds(container), ['flights', 'radio']);
|
||||
assert.deepEqual(headings(container), ['Movement', 'Audio']);
|
||||
panel.mount(container);
|
||||
assert.deepEqual(
|
||||
rowIds(utilities),
|
||||
['directions'],
|
||||
'a remount replaces it',
|
||||
);
|
||||
panel.destroy();
|
||||
assert.deepEqual(rowIds(utilities), [], 'destroy releases both lists');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a refresh updates the Directions row in the Utilities list', async () => {
|
||||
const { f, panel, utilities, layers, restore } = await utilitiesFixture();
|
||||
const { utilities, panel, layers, restore } = await utilitiesFixture();
|
||||
try {
|
||||
const row = f.find(
|
||||
(node) => node.dataset?.layerId === 'directions',
|
||||
utilities,
|
||||
);
|
||||
const toggle = row.querySelector('.data-toggle-btn');
|
||||
assert.ok(!toggle.className.includes('active'));
|
||||
const toggle = () =>
|
||||
utilities.querySelector('[data-layer-id="directions"] .data-toggle-btn');
|
||||
assert.ok(!toggle().classList.contains('active'));
|
||||
layers.find(({ id }) => id === 'directions').enabled = true;
|
||||
panel._refreshTogglePanel();
|
||||
assert.ok(toggle.className.includes('active'));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.ok(toggle().classList.contains('active'));
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('without the Utilities list, Directions stays in Data Layers under Utilities', async () => {
|
||||
const { f, headings, rowIds, restore } = await utilitiesFixture({
|
||||
const { container, headings, rowIds, restore } = await utilitiesFixture({
|
||||
withUtilities: false,
|
||||
});
|
||||
try {
|
||||
assert.deepEqual(rowIds(f.container), ['flights', 'directions', 'radio']);
|
||||
assert.deepEqual(headings(f.container), ['Movement', 'Utilities', 'Audio']);
|
||||
assert.deepEqual(rowIds(container), ['flights', 'directions', 'radio']);
|
||||
assert.deepEqual(headings(container), ['Movement', 'Utilities', 'Audio']);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user