mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
From the re-shot Russian screenshots:
- Translated: weather frame/imagery status errors, HUD summary provenance
("НЕДОСТУПНО: СУДА AIS ОНЛАЙН"), Bhote Koshi map labels, cockpit signal
distances (КМ). The HUD summary fields are separated with " · " in every
language, so the style name and view band no longer read as one phrase.
- Numbers follow the locale: cockpit altitude ("26 175"), precipitation.
- No breaks inside codes and dates: ADS-B and similar in layer rows,
Sentinel-2, cyclone issue dates (word joiner; the UI font has no U+2011),
no-break space after label colons in cyclone details.
- Layout: count lines under ALPR, short unavailable chip (context
"status chip"), roomier draw segments, tighter CCTV calibration chips,
two-line beat titles and media card titles, wrapping weather status.
- Wording: ru advisory is «сводка» (was «бюллетень»; "Advisory center" is
«Центр циклона по сводке»), Street View is «Просмотр улиц», OPACITY
«НЕПРОЗР.», synced «синхр.», Area «Зона»; the import dialog no longer says
"this scene" for several scenes. Glossary updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { afterEach } from 'node:test';
|
|
import { formatAwarenessLabel } from './data/militaryAwarenessEngine.js';
|
|
import { CockpitViewController } from './ui/cockpitController.js';
|
|
const previousDocument = globalThis.document;
|
|
afterEach(() => {
|
|
globalThis.document = previousDocument;
|
|
});
|
|
|
|
// Model native focus loss on removal AND on ordinary DOM moves. Keeping an
|
|
// object reference alone is insufficient if reconciliation disconnects it.
|
|
function fixture() {
|
|
const document = { activeElement: null };
|
|
let focusLosses = 0;
|
|
let focusCalls = 0;
|
|
class Node {
|
|
constructor(tagName) {
|
|
this.tagName = tagName.toUpperCase();
|
|
this.children = [];
|
|
this.parentNode = null;
|
|
this.dataset = {};
|
|
this.attributes = new Map();
|
|
this.className = '';
|
|
this._text = '';
|
|
this.classList = {
|
|
add: (...names) => {
|
|
this.className = [
|
|
...new Set([
|
|
...this.className.split(/\s+/).filter(Boolean),
|
|
...names,
|
|
]),
|
|
].join(' ');
|
|
},
|
|
};
|
|
}
|
|
get isConnected() {
|
|
return this === document.body || Boolean(this.parentNode?.isConnected);
|
|
}
|
|
get firstElementChild() {
|
|
return this.children[0] || null;
|
|
}
|
|
get nextElementSibling() {
|
|
return (
|
|
this.parentNode?.children[this.parentNode.children.indexOf(this) + 1] ||
|
|
null
|
|
);
|
|
}
|
|
get textContent() {
|
|
return (
|
|
this._text + this.children.map((node) => node.textContent).join('')
|
|
);
|
|
}
|
|
set textContent(value) {
|
|
this.replaceChildren();
|
|
this._text = String(value);
|
|
}
|
|
setAttribute(name, value) {
|
|
this.attributes.set(name, String(value));
|
|
}
|
|
getAttribute(name) {
|
|
return this.attributes.get(name) ?? null;
|
|
}
|
|
contains(node) {
|
|
return (
|
|
this === node || this.children.some((child) => child.contains(node))
|
|
);
|
|
}
|
|
append(...nodes) {
|
|
for (const node of nodes) this.insertBefore(node, null);
|
|
}
|
|
insertBefore(node, anchor) {
|
|
assert.ok(
|
|
anchor === null || anchor.parentNode === this,
|
|
'insert anchor belongs to its parent',
|
|
);
|
|
if (node === anchor) return node;
|
|
node.remove();
|
|
const index =
|
|
anchor === null ? this.children.length : this.children.indexOf(anchor);
|
|
this.children.splice(index, 0, node);
|
|
node.parentNode = this;
|
|
return node;
|
|
}
|
|
remove() {
|
|
if (!this.parentNode) return;
|
|
if (this.contains(document.activeElement)) {
|
|
document.activeElement = document.body;
|
|
focusLosses += 1;
|
|
}
|
|
this.parentNode.children.splice(
|
|
this.parentNode.children.indexOf(this),
|
|
1,
|
|
);
|
|
this.parentNode = null;
|
|
}
|
|
replaceChildren(...nodes) {
|
|
for (const child of [...this.children]) child.remove();
|
|
this._text = '';
|
|
this.append(...nodes);
|
|
}
|
|
matches(selector) {
|
|
if (selector === 'button[data-signal-layer][data-signal-id]') {
|
|
return (
|
|
this.tagName === 'BUTTON' &&
|
|
this.dataset.signalLayer != null &&
|
|
this.dataset.signalId != null
|
|
);
|
|
}
|
|
return selector.startsWith('.')
|
|
? this.className.split(/\s+/).includes(selector.slice(1))
|
|
: this.tagName === selector.toUpperCase();
|
|
}
|
|
closest(selector) {
|
|
for (let node = this; node; node = node.parentNode)
|
|
if (node.matches(selector)) return node;
|
|
return null;
|
|
}
|
|
querySelectorAll(selector) {
|
|
return this.children.flatMap((child) => [
|
|
...(child.matches(selector) ? [child] : []),
|
|
...child.querySelectorAll(selector),
|
|
]);
|
|
}
|
|
focus() {
|
|
assert.ok(
|
|
this.isConnected,
|
|
'a focus destination must still be connected',
|
|
);
|
|
focusCalls += 1;
|
|
document.activeElement = this;
|
|
}
|
|
}
|
|
document.body = new Node('body');
|
|
document.createElement = (tag) => new Node(tag);
|
|
document.activeElement = document.body;
|
|
globalThis.document = document;
|
|
const controller = Object.create(CockpitViewController.prototype);
|
|
controller.services = { formatAwarenessLabel };
|
|
controller.signalList = new Node('ol');
|
|
controller.signalToggle = new Node('button');
|
|
controller.briefTabs = [
|
|
new Node('button'),
|
|
new Node('button'),
|
|
new Node('button'),
|
|
];
|
|
controller.briefPageIndex = 0;
|
|
controller.signalItems = [];
|
|
controller.signalSignatures = new Map();
|
|
controller.scheduleContextLayout = () => {};
|
|
const display = new Node('button');
|
|
const radio = new Node('button');
|
|
const outside = new Node('button');
|
|
document.body.append(
|
|
controller.signalToggle,
|
|
controller.signalList,
|
|
...controller.briefTabs,
|
|
display,
|
|
radio,
|
|
outside,
|
|
);
|
|
const selected = [];
|
|
controller.services.militaryAwarenessLayer = {
|
|
focusTarget: (...args) => selected.push(args),
|
|
};
|
|
const click = (event) => controller.handleSignalClick(event);
|
|
const buttons = () => controller.signalList.querySelectorAll('button');
|
|
const tab = () => {
|
|
const nodes = document.body.querySelectorAll('button');
|
|
const index = nodes.indexOf(document.activeElement);
|
|
nodes[(index + 1) % nodes.length].focus();
|
|
};
|
|
return {
|
|
controller,
|
|
document,
|
|
buttons,
|
|
display,
|
|
radio,
|
|
outside,
|
|
selected,
|
|
click,
|
|
tab,
|
|
losses: () => focusLosses,
|
|
focusCalls: () => focusCalls,
|
|
};
|
|
}
|
|
|
|
const item = (id, overrides = {}) => ({
|
|
key: `flight:flights:${id}`,
|
|
tone: 'nearby',
|
|
title: `Flight ${id}`,
|
|
detail: 'COMMERCIAL FLIGHT · 2 KM',
|
|
target: { layerId: 'flights', id },
|
|
timestamp: 1_700_000_000_000,
|
|
...overrides,
|
|
});
|
|
const render = (f, items) => {
|
|
f.controller.signalItems = items;
|
|
f.controller.renderCockpitSignals();
|
|
};
|
|
const snapshot = (distanceM = 2000) => ({
|
|
evaluatedAt: 1_700_000_000_000,
|
|
subject: { layerId: 'flights', id: 'current', label: 'CURRENT' },
|
|
cohorts: [
|
|
{
|
|
id: 'flights',
|
|
count: 1,
|
|
nearest: [{ id: 'nearby', label: 'NEARBY', distanceM }],
|
|
},
|
|
],
|
|
});
|
|
|
|
test('actual Context refresh retains the focused signal button across unchanged and changed details', () => {
|
|
const f = fixture();
|
|
f.controller.updateCockpitSignals(snapshot(), 0);
|
|
const target = f.buttons()[1];
|
|
target.focus();
|
|
const calls = f.focusCalls();
|
|
for (let tick = 0; tick < 8; tick += 1)
|
|
f.controller.updateCockpitSignals(snapshot(2500), 0);
|
|
assert.equal(f.buttons()[1], target);
|
|
assert.equal(f.document.activeElement, target);
|
|
assert.equal(f.losses(), 0);
|
|
assert.equal(
|
|
f.focusCalls(),
|
|
calls,
|
|
'refresh must preserve focus without repeatedly restoring it',
|
|
);
|
|
assert.match(f.controller.signalList.textContent, /2\.5\u00a0KM/);
|
|
});
|
|
|
|
test('signal content and accessible name update without replacing the active target', () => {
|
|
const f = fixture();
|
|
render(f, [item('a')]);
|
|
const target = f.buttons()[0];
|
|
target.focus();
|
|
render(f, [
|
|
item('a', {
|
|
title: 'RENAMED',
|
|
tone: 'track',
|
|
detail: 'CURRENT',
|
|
timestamp: 1_700_000_001_000,
|
|
}),
|
|
]);
|
|
assert.equal(f.buttons()[0], target);
|
|
assert.equal(target.getAttribute('aria-label'), 'Select flight RENAMED');
|
|
assert.match(target.textContent, /RENAMED/);
|
|
assert.match(f.controller.signalList.textContent, /CURRENT/);
|
|
assert.equal(
|
|
f.controller.signalList.children[0].className,
|
|
'track actionable',
|
|
);
|
|
assert.equal(f.document.activeElement, target);
|
|
assert.equal(f.losses(), 0);
|
|
});
|
|
|
|
test('signal ranking changes move surrounding rows without disconnecting the focused row', () => {
|
|
const f = fixture();
|
|
render(
|
|
f,
|
|
['a', 'b', 'c', 'd'].map((id) => item(id)),
|
|
);
|
|
const target = f.buttons()[1];
|
|
target.focus();
|
|
const calls = f.focusCalls();
|
|
for (const order of [
|
|
['d', 'c', 'b', 'a'],
|
|
['b', 'c', 'd', 'a'],
|
|
['a', 'd', 'c', 'b'],
|
|
]) {
|
|
render(
|
|
f,
|
|
order.map((id) => item(id)),
|
|
);
|
|
assert.deepEqual(
|
|
f.buttons().map((button) => button.dataset.signalId),
|
|
order,
|
|
);
|
|
assert.equal(f.document.activeElement, target);
|
|
assert.equal(f.losses(), 0);
|
|
}
|
|
assert.equal(f.focusCalls(), calls);
|
|
});
|
|
|
|
test('a new pushed signal preserves existing focus and delegated selection reads the current target', () => {
|
|
const f = fixture();
|
|
render(f, [item('a'), item('b')]);
|
|
const target = f.buttons()[1];
|
|
target.focus();
|
|
f.controller.pushCockpitSignal(
|
|
'status',
|
|
'warning',
|
|
'INPUT UNKNOWN',
|
|
'SOURCE UNAVAILABLE',
|
|
);
|
|
assert.equal(f.document.activeElement, target);
|
|
assert.equal(f.losses(), 0);
|
|
const event = {
|
|
target: target.children[0],
|
|
prevented: false,
|
|
preventDefault() {
|
|
this.prevented = true;
|
|
},
|
|
};
|
|
f.click(event);
|
|
assert.equal(event.prevented, true);
|
|
assert.deepEqual(f.selected, [['flights', 'b', { origin: 'user' }]]);
|
|
});
|
|
|
|
test('removing the focused identity uses the existing briefing tab once and permits forward traversal', () => {
|
|
const f = fixture();
|
|
render(f, [item('a'), item('b')]);
|
|
const removed = f.buttons()[1];
|
|
removed.focus();
|
|
const calls = f.focusCalls();
|
|
render(f, [item('a')]);
|
|
assert.equal(removed.isConnected, false);
|
|
assert.equal(f.document.activeElement, f.controller.briefTabs[0]);
|
|
assert.equal(f.focusCalls(), calls + 1);
|
|
f.tab();
|
|
f.tab();
|
|
f.tab();
|
|
assert.equal(f.document.activeElement, f.display);
|
|
render(f, [item('a', { detail: 'UPDATED' })]);
|
|
assert.equal(
|
|
f.document.activeElement,
|
|
f.display,
|
|
'later refresh must not pull focus back',
|
|
);
|
|
f.tab();
|
|
assert.equal(f.document.activeElement, f.radio);
|
|
});
|
|
|
|
test('a changed target cannot reuse a focused action for another flight', () => {
|
|
const f = fixture();
|
|
render(f, [item('a', { key: 'shared-status-key' })]);
|
|
const removed = f.buttons()[0];
|
|
removed.focus();
|
|
render(f, [
|
|
item('b', {
|
|
key: 'shared-status-key',
|
|
target: { layerId: 'military', id: 'b' },
|
|
}),
|
|
]);
|
|
const target = f.buttons()[0];
|
|
assert.notEqual(target, removed);
|
|
assert.equal(removed.isConnected, false);
|
|
assert.equal(f.document.activeElement, f.controller.briefTabs[0]);
|
|
f.click({ target, preventDefault() {} });
|
|
assert.deepEqual(f.selected, [['military', 'b', { origin: 'user' }]]);
|
|
});
|
|
|
|
test('empty, informational and duplicate-key rows have no stale or aliased actions', () => {
|
|
const f = fixture();
|
|
render(f, [item('a'), item('a')]);
|
|
const [first, second] = f.buttons();
|
|
assert.notEqual(first, second);
|
|
second.focus();
|
|
render(f, [item('a'), item('a')]);
|
|
assert.deepEqual(f.buttons(), [first, second]);
|
|
assert.equal(f.document.activeElement, second);
|
|
render(f, [item('status', { target: null, title: 'UNKNOWN' })]);
|
|
assert.equal(f.buttons().length, 0);
|
|
assert.match(f.controller.signalList.textContent, /UNKNOWN/);
|
|
assert.equal(f.document.activeElement, f.controller.briefTabs[0]);
|
|
f.outside.focus();
|
|
render(f, []);
|
|
assert.equal(f.controller.signalList.children.length, 0);
|
|
render(f, [item('a')]);
|
|
assert.notEqual(f.buttons()[0], first);
|
|
assert.equal(f.document.activeElement, f.outside);
|
|
});
|