Files
gods-eye-view/src/ui/displayPanelActions.test.mjs
T
Sameh Khamis 3c3f8e3e67 feat(display): answer the Display controls through the store with UiSegmented
The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.

Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
2026-11-01 09:56:47 -08:00

174 lines
5.6 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('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]]);
});