mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Conflicts in es, ru, zh-CN and zh-Hant catalogs: both sides appended keys at the end (cyclone errors from this branch, routing refusals from main); kept both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
390 lines
12 KiB
JavaScript
390 lines
12 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { runInNewContext } from 'node:vm';
|
|
|
|
test('panel presentation places Transit between Street Traffic and Bike Share in Movement', () => {
|
|
const source = readFileSync(
|
|
new URL('./layerPanel.js', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const declarations = source.slice(
|
|
source.indexOf('const PANEL_GROUPS ='),
|
|
source.indexOf('const PANEL_POSITIONS ='),
|
|
);
|
|
const order = JSON.parse(
|
|
runInNewContext(
|
|
`const msg = (text) => text;\n${declarations}\nJSON.stringify(PANEL_ORDER)`,
|
|
),
|
|
);
|
|
assert.deepEqual(
|
|
order.filter(({ label }) => label === 'Movement').map(({ id }) => id),
|
|
[
|
|
'satellites',
|
|
'flights',
|
|
'military',
|
|
'local-adsb',
|
|
'ais-live-vessels',
|
|
'traffic',
|
|
'transit',
|
|
'bikeshare',
|
|
],
|
|
);
|
|
assert.equal(order.filter(({ id }) => id === 'transit').length, 1);
|
|
});
|
|
|
|
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 layer = {
|
|
id: 'ais-live-vessels',
|
|
name: 'Live Vessels',
|
|
source: 'AISStream',
|
|
enabled: true,
|
|
stats: {
|
|
partial: true,
|
|
stale: false,
|
|
count: 2,
|
|
acceptedRowCount: 2,
|
|
rawRowCount: 3,
|
|
lastUpdate: Date.now(),
|
|
},
|
|
};
|
|
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/);
|
|
assert.match(
|
|
panel._buildMetaText(layer),
|
|
/^PARTIAL · AISStream · 2 of 3 records accepted · /,
|
|
);
|
|
assert.match(
|
|
panel._buildMetaText({
|
|
...layer,
|
|
stats: { ...layer.stats, rawRowCount: 2 },
|
|
}),
|
|
/incomplete snapshot/,
|
|
);
|
|
assert.equal(layerFeedState({ ...layer.stats, stale: true }), 'stale');
|
|
assert.equal(
|
|
layerFeedState({ ...layer.stats, error: 'Connection lost' }),
|
|
'degraded',
|
|
);
|
|
assert.equal(
|
|
layerFeedState({ ...layer.stats, status: 'unavailable' }),
|
|
'unavailable',
|
|
);
|
|
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);
|
|
layer.enabled = false;
|
|
panel._syncToggleButton(button, layer);
|
|
assert.equal(button.textContent, 'OFF');
|
|
});
|
|
|
|
test('a stale row dates cached data by its oldest tile, not the last load', async () => {
|
|
const { LayerPanel } = await import('./layerPanel.js');
|
|
const panel = LayerPanel.prototype;
|
|
const layer = {
|
|
id: 'traffic',
|
|
name: 'Street Traffic',
|
|
source: 'TomTom',
|
|
enabled: true,
|
|
stats: {
|
|
mode: 'live',
|
|
stale: true,
|
|
count: 4,
|
|
lastUpdate: Date.now(),
|
|
oldestTileAt: Date.now() - 3 * 3600_000,
|
|
},
|
|
};
|
|
assert.match(panel._buildMetaText(layer), /^STALE · TomTom · 3h ago$/);
|
|
delete layer.stats.oldestTileAt;
|
|
assert.match(panel._buildMetaText(layer), /^STALE · TomTom · just now$/);
|
|
});
|
|
|
|
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;
|
|
const ids = [
|
|
'wind',
|
|
'weather-radar',
|
|
'weather-satellite',
|
|
'weather-lightning',
|
|
'weather-cyclones',
|
|
'other',
|
|
];
|
|
let refresh;
|
|
const layers = ids.map((id) => ({
|
|
id,
|
|
name: id,
|
|
icon: '*',
|
|
source: 'Source',
|
|
enabled: true,
|
|
showInTogglePanel: true,
|
|
stats: {},
|
|
}));
|
|
const calls = [];
|
|
const panel = new LayerPanel({
|
|
getLayers: () => layers,
|
|
isEnabled: () => true,
|
|
setEnabled: (...args) => calls.push(args),
|
|
setLayerParams: (...args) => calls.push(args),
|
|
hasRowControls: () => true,
|
|
subscribeRowControls: (_id, callback) => {
|
|
refresh = callback;
|
|
},
|
|
getRowControls: (id) => ({
|
|
readout: id !== 'other',
|
|
summary: { status: 'Loading' },
|
|
info: 'Detailed time',
|
|
chips: [{ id: 'config', label: 'Soft', params: { opacity: 'light' } }],
|
|
legend: [{ label: 'Rain', color: '#abcdef' }],
|
|
list: {
|
|
items: [{ id: 'storm', text: 'Storm', params: { focus: true } }],
|
|
},
|
|
}),
|
|
});
|
|
try {
|
|
panel.mount(f.container);
|
|
for (const id of ids.slice(0, -1)) {
|
|
const row = f.find((n) => n.dataset.layerId === id);
|
|
assert.equal(row.children.length, 2);
|
|
assert.ok(row.querySelector('.data-name'));
|
|
assert.ok(row.querySelector('.data-toggle-meta'));
|
|
for (const cls of [
|
|
'data-toggle-chip',
|
|
'data-count',
|
|
'data-toggle-controls',
|
|
'data-toggle-controls-status',
|
|
'data-row-list',
|
|
])
|
|
assert.equal(row.querySelector(`.${cls}`), null);
|
|
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');
|
|
assert.equal(
|
|
ordinary.querySelector('.data-toggle-controls-info').textContent,
|
|
'Detailed time',
|
|
);
|
|
assert.ok(ordinary.querySelector('.data-toggle-legend-item'));
|
|
ordinary.querySelector('.data-toggle-chip').click();
|
|
assert.deepEqual(calls.at(-1), [
|
|
'other',
|
|
{ opacity: 'light' },
|
|
{ origin: 'user' },
|
|
]);
|
|
assert.equal(typeof refresh, 'function');
|
|
} finally {
|
|
panel.destroy();
|
|
globalThis.document = previousDocument;
|
|
}
|
|
});
|
|
|
|
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;
|
|
const mounted = [];
|
|
const factory = (container) => {
|
|
const readout = {
|
|
container,
|
|
destroyed: false,
|
|
destroy() {
|
|
this.destroyed = true;
|
|
},
|
|
};
|
|
mounted.push(readout);
|
|
return readout;
|
|
};
|
|
const panel = new LayerPanel({
|
|
getLayers: () => [],
|
|
isEnabled: () => false,
|
|
setEnabled() {},
|
|
setLayerParams() {},
|
|
hasRowControls: () => false,
|
|
subscribeRowControls() {},
|
|
getRowControls: () => null,
|
|
});
|
|
try {
|
|
panel.attachRecentImagery(factory);
|
|
assert.equal(mounted.length, 0, 'no body before the panel mounts');
|
|
panel.mount(f.container);
|
|
assert.equal(mounted.length, 1);
|
|
assert.equal(mounted[0].container, body);
|
|
panel.mount(f.container);
|
|
assert.equal(mounted[0].destroyed, true, 'a remount rebuilds it');
|
|
assert.equal(mounted.length, 2);
|
|
panel.attachRecentImagery(null);
|
|
assert.equal(mounted[1].destroyed, true);
|
|
panel.attachRecentImagery(factory);
|
|
assert.equal(mounted.length, 3);
|
|
panel.destroy();
|
|
assert.equal(mounted[2].destroyed, true);
|
|
panel.attachRecentImagery(factory);
|
|
assert.equal(mounted.length, 3, 'a destroyed panel mounts nothing');
|
|
} finally {
|
|
panel.destroy();
|
|
globalThis.document = previousDocument;
|
|
}
|
|
});
|
|
|
|
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 layer = {
|
|
id: 'flights',
|
|
name: 'Flights',
|
|
enabled: false,
|
|
stats: {
|
|
sourceUnavailable: true,
|
|
status: 'unavailable',
|
|
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 });
|
|
assert.equal(
|
|
attrs.get('aria-disabled'),
|
|
'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 name = element();
|
|
setRowText(name, 'Локальный 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 plain = element();
|
|
setRowText(plain, 'Satellites');
|
|
assert.equal(plain.textContent, 'Satellites');
|
|
assert.equal(plain.children.length, 0);
|
|
|
|
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 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;
|
|
}
|
|
});
|
|
|
|
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, {
|
|
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\./);
|
|
});
|
|
|
|
test('the Weather readout receives enabled air-quality alongside weather layers', async () => {
|
|
const { LayerPanel } = await import('./layerPanel.js');
|
|
const layers = ['wind', 'air-quality', 'other'].map((id) => ({
|
|
id,
|
|
icon: '*',
|
|
enabled: true,
|
|
}));
|
|
layers.push({ id: 'weather-radar', icon: '*', enabled: false });
|
|
const panel = new LayerPanel({
|
|
getLayers: () => layers,
|
|
getRowControls: (id) => ({ readout: true, summary: { label: id } }),
|
|
});
|
|
let entries = null;
|
|
panel._weatherPanel = {
|
|
update: (next) => {
|
|
entries = next;
|
|
},
|
|
};
|
|
panel._refreshWeatherPanel();
|
|
assert.deepEqual(
|
|
entries.map(({ id, summary }) => [id, summary.label]),
|
|
[
|
|
['wind', 'wind'],
|
|
['air-quality', 'air-quality'],
|
|
],
|
|
);
|
|
});
|