Files
gods-eye-view/src/ui/displayPanelActions.test.mjs
T
Sameh Khamis a6e85db8be feat(draw): answer the Draw controls through the display store
The draw tool no longer looks up or listens on its controls. It connects
toggleDraw, setDrawShape, setDrawColor, setDrawLabel and clearDraw to the
display store, which now holds several owners and lets the newest owner
of an action answer it, and it publishes the colour and label beside the
shape, state and hint. The Draw panel renders the shape choice with
UiSegmented and binds the label and colour to what the tool publishes.

The label field's own Enter listener goes: the session's capture-phase
key handler already finishes the shape from that field.
2026-11-01 10:10:27 -08:00

233 lines
7.5 KiB
JavaScript

// src/ui/displayPanelActions.test.mjs — the Display panel's controls, the
// clean-view exit and the dock's style buttons call the Display controls'
// owner through the display store, and only while it is connected.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApp, h, nextTick } from 'vue';
import { createPinia } from 'pinia';
import { useDisplayStore } from './stores/display.js';
import { DisplayBindings } from './displayBindings.js';
import CommandDockView from './views/CommandDockView.vue';
import DisplayControlsView from './views/DisplayControlsView.vue';
function mount(t) {
document.body.replaceChildren();
const pinia = createPinia();
const store = useDisplayStore(pinia);
const app = createApp({
render: () => [h(DisplayControlsView), h(CommandDockView)],
}).use(pinia);
app.mount(document.body.appendChild(document.createElement('div')));
t.after(() => app.unmount());
return store;
}
const $ = (id) => document.getElementById(id);
function recorder(store) {
const calls = [];
const actions = new Proxy(
{},
{
get:
(_, name) =>
(...args) =>
calls.push([name, ...args]),
},
);
return { calls, token: store.connect(actions) };
}
function input(el, value, type = 'input') {
el.value = String(value);
el.dispatchEvent(new Event(type, { bubbles: true }));
}
test('buttons, sliders, the layout select and the Scope checkbox call their actions', async (t) => {
const store = mount(t);
const { calls } = recorder(store);
for (const id of [
'hud-toggle',
'cyber-sonar-toggle',
'detection-toggle',
'models3d-toggle',
'celestial-toggle',
'clean-view-toggle',
'bloom-toggle',
'sharpen-toggle',
'clean-view-exit',
])
$(id).click();
input($('hud-layout-select'), 'minimal', 'change');
input($('cyber-sonar-rings'), 5);
input($('cyber-sonar-range'), 90);
input($('cyber-sonar-intensity'), 40);
input($('cyber-sonar-opacity'), 60);
input($('cyber-sonar-sector'), 30);
input($('detection-density-slider'), 75);
input($('detection-fade-slider'), 12);
input($('detection-opacity-slider'), 3);
input($('scope-feather-slider'), 150);
input($('bloom-intensity-slider'), 24);
input($('sharpen-intensity-slider'), 37);
$('scope-toggle').checked = false;
$('scope-toggle').dispatchEvent(new Event('change', { bubbles: true }));
assert.deepEqual(calls, [
['toggleHud'],
['toggleSonar'],
['cycleDetection'],
['toggleModels'],
['toggleCelestial'],
['toggleCleanView'],
['toggleBloom'],
['toggleSharpen'],
['exitCleanView'],
['setHudLayout', 'minimal'],
['setSonarRings', 5],
['setSonarRange', 90],
['setSonarIntensity', 40],
['setSonarOpacity', 60],
['setSonarSector', 30],
// Density passes the slider's own value, as it always has.
['setDensity', '75'],
['setFade', 12],
['setOutsideOpacity', 3],
['setScopeFeather', 150],
['setBloomIntensity', 24],
['setSharpenIntensity', 37],
['setScopeEnabled', false],
]);
});
test('the Draw controls call the draw tool and show what it publishes', async (t) => {
const store = mount(t);
const { calls } = recorder(store);
$('draw-toggle').click();
document.querySelector('[data-shape="pin"]').click();
input($('draw-label-input'), 'Camp');
input($('draw-color-select'), 'red', 'change');
$('draw-clear').click();
assert.deepEqual(calls, [
['toggleDraw'],
['setDrawShape', 'pin'],
['setDrawLabel', 'Camp'],
['setDrawColor', 'red'],
['clearDraw'],
]);
store.publishDraw({
active: true,
shape: 'line',
color: 'green',
label: '',
hint: 'Click to add points.',
});
await nextTick();
const line = document.querySelector('[data-shape="line"]');
assert.equal(line.getAttribute('aria-checked'), 'true');
assert.ok(line.classList.contains('active'));
assert.equal($('draw-color-select').value, 'green');
assert.equal($('draw-label-input').value, '', 'a finished shape empties it');
assert.equal($('draw-hint').textContent, 'Click to add points.');
assert.equal($('draw-toggle').getAttribute('aria-pressed'), 'true');
});
test('style, allocation and model choices keep their ids and data attributes', async (t) => {
const store = mount(t);
const { calls } = recorder(store);
document.querySelector('.style-btn[data-style="thermal"]').click();
$('detection-allocation-weighted').click();
$('models3d-mode-all').click();
$('models3d-mode-proximity').click();
assert.deepEqual(calls, [
['setStyle', 'thermal'],
['setAllocation', 'WEIGHTED'],
['setModelsMode', 'all'],
['setModelsMode', 'proximity'],
]);
assert.equal(
$('detection-allocation-weighted').dataset.allocation,
'WEIGHTED',
);
assert.equal($('models3d-mode-all').dataset.mode, 'all');
// Their selection follows what the owners present.
assert.equal(
$('models3d-mode-proximity').getAttribute('aria-checked'),
'true',
);
store.publishModels3d({ pressed: true, mode: 'all', rowVisible: true });
await nextTick();
assert.equal($('models3d-mode-all').getAttribute('aria-checked'), 'true');
assert.ok($('models3d-mode-all').classList.contains('active'));
});
test('a detached or replaced owner hears nothing', (t) => {
const store = mount(t);
const first = recorder(store);
$('bloom-toggle').click();
store.detach(first.token);
$('bloom-toggle').click();
const second = recorder(store);
store.detach(first.token);
$('bloom-toggle').click();
assert.deepEqual(first.calls, [['toggleBloom']]);
assert.deepEqual(second.calls, [['toggleBloom']]);
});
test('DisplayBindings answers the controls from init until destroy', (t) => {
const store = mount(t);
const calls = [];
const bindings = new DisplayBindings({
viewer: null,
services: { isScopeMaskEnabled: () => false, cycleDetectionMode: () => {} },
elements: { _locationSearch: document.createElement('input') },
operations: {
_setScope: () => {},
_setBloomEnabled: (enabled) => calls.push(['bloom', enabled]),
},
readState: () => ({ bloomEnabled: false }),
claimDetection: () => {},
displayStore: store,
});
bindings._initUI();
// Initialising again replaces its own connection rather than stacking one.
bindings._initUI();
$('bloom-toggle').click();
assert.deepEqual(calls, [['bloom', true]]);
// A newer owner takes over; the old one's destroy leaves it connected.
const next = recorder(store);
bindings.destroy();
$('bloom-toggle').click();
assert.deepEqual(calls, [['bloom', true]]);
assert.deepEqual(next.calls, [['toggleBloom']]);
store.detach(next.token);
bindings._initUI();
bindings.destroy();
$('bloom-toggle').click();
assert.deepEqual(calls, [['bloom', true]]);
});
test('several owners answer their own actions, the newest first', (t) => {
const store = mount(t);
const calls = [];
const owner = (tag, names) =>
store.connect(
Object.fromEntries(
names.map((name) => [name, () => calls.push([tag, name])]),
),
);
const display = owner('display', ['toggleBloom', 'setStyle']);
const draw = owner('draw', ['toggleDraw']);
const newer = owner('newer', ['toggleBloom']);
$('bloom-toggle').click();
$('draw-toggle').click();
store.detach(newer);
$('bloom-toggle').click();
store.detach(draw);
$('draw-toggle').click();
store.detach(display);
$('bloom-toggle').click();
assert.deepEqual(calls, [
['newer', 'toggleBloom'],
['draw', 'toggleDraw'],
['display', 'toggleBloom'],
]);
});