Files
gods-eye-view/src/ui/displayControls.test.mjs
T

187 lines
5.9 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { bindDisplayControls } from './displayControls.js';
function element(value = '') {
const el = new EventTarget();
el.value = value;
el.dataset = {};
return el;
}
test('sonar on/off control cannot fire after disposal', () => {
const sonarButton = element();
const calls = [];
const control = bindDisplayControls({
elements: { sonarButton },
actions: { toggleSonar: () => calls.push('toggle') },
});
sonarButton.dispatchEvent(new Event('click'));
control.destroy();
sonarButton.dispatchEvent(new Event('click'));
assert.deepEqual(calls, ['toggle']);
});
test('controls read current values without preventing native input behavior', () => {
const bloomSlider = element('24');
const hudLayout = element('tactical');
const calls = [];
const control = bindDisplayControls({
elements: { bloomSlider, hudLayout },
actions: {
setBloomIntensity: (value) => calls.push(value),
setHudLayout: (value) => calls.push(value),
},
});
const event = new Event('input', { cancelable: true });
bloomSlider.dispatchEvent(event);
bloomSlider.value = '37';
bloomSlider.dispatchEvent(new Event('input'));
hudLayout.value = 'minimal';
hudLayout.dispatchEvent(new Event('change'));
assert.deepEqual(calls, [24, 37, 'minimal']);
assert.equal(event.defaultPrevented, false);
control.destroy();
});
test('destroyed and replaced controls cannot issue stale actions', () => {
const bloomButton = element();
let oldCalls = 0;
let newCalls = 0;
const first = bindDisplayControls({
elements: { bloomButton },
actions: { toggleBloom: () => oldCalls++ },
});
bloomButton.dispatchEvent(new Event('click'));
first.destroy();
const second = bindDisplayControls({
elements: { bloomButton },
actions: { toggleBloom: () => newCalls++ },
});
first.destroy();
bloomButton.dispatchEvent(new Event('click'));
assert.equal(oldCalls, 1);
assert.equal(newCalls, 1);
second.destroy();
bloomButton.dispatchEvent(new Event('click'));
assert.equal(newCalls, 1);
});
test('the Cyber sonar control is an optional owned display action', () => {
const sonarButton = element();
const sonarRingsSlider = element('9');
const sonarRangeSlider = element('110');
const sonarIntensitySlider = element('65');
const sonarOpacitySlider = element('84');
const sonarSectorSlider = element('32');
const calls = [];
const control = bindDisplayControls({
elements: {
sonarButton,
sonarRingsSlider,
sonarRangeSlider,
sonarIntensitySlider,
sonarOpacitySlider,
sonarSectorSlider,
},
actions: {
toggleSonar: () => calls.push('toggle'),
setSonarRings: (value) => calls.push(['rings', value]),
setSonarRange: (value) => calls.push(['range', value]),
setSonarIntensity: (value) => calls.push(['intensity', value]),
setSonarOpacity: (value) => calls.push(['opacity', value]),
setSonarSector: (value) => calls.push(['sector', value]),
},
});
sonarButton.dispatchEvent(new Event('click'));
sonarRingsSlider.dispatchEvent(new Event('input'));
sonarRangeSlider.dispatchEvent(new Event('input'));
sonarIntensitySlider.dispatchEvent(new Event('input'));
sonarOpacitySlider.dispatchEvent(new Event('input'));
sonarSectorSlider.dispatchEvent(new Event('input'));
assert.deepEqual(calls, [
'toggle',
['rings', 9],
['range', 110],
['intensity', 65],
['opacity', 84],
['sector', 32],
]);
control.destroy();
sonarButton.dispatchEvent(new Event('click'));
assert.equal(calls.length, 6);
});
test('style, allocation and model choices retain their current data attributes', () => {
const style = element();
const allocation = element();
const mode = element();
style.dataset.style = 'thermal';
allocation.dataset.allocation = 'balanced';
mode.dataset.mode = 'all';
const calls = [];
const control = bindDisplayControls({
elements: {
styleButtons: [style],
allocationButtons: [allocation],
modelModeButtons: [mode],
},
actions: {
setStyle: (value) => calls.push(value),
setAllocation: (value) => calls.push(value),
setModelsMode: (value) => calls.push(value),
},
});
for (const el of [style, allocation, mode])
el.dispatchEvent(new Event('click'));
mode.dataset.mode = 'unknown';
mode.dispatchEvent(new Event('click'));
assert.deepEqual(calls, ['thermal', 'balanced', 'all', 'proximity']);
control.destroy();
});
test('optional controls are absent safely and subscriptions stay instance-owned', () => {
const calls = [];
const a = element();
const b = element();
const one = bindDisplayControls({
elements: { fadeSlider: null, outsideOpacitySlider: a },
actions: { setOutsideOpacity: () => calls.push('a') },
});
const two = bindDisplayControls({
elements: { fadeSlider: b },
actions: { setFade: () => calls.push('b') },
});
one.destroy();
a.dispatchEvent(new Event('input'));
b.dispatchEvent(new Event('input'));
assert.deepEqual(calls, ['b']);
two.destroy();
});
test('the native Scope checkbox reads checked state on change and disposes its listener', () => {
const scopeToggle = element();
scopeToggle.checked = true;
const scopeFeatherSlider = element('200');
const calls = [];
const controls = bindDisplayControls({
elements: { scopeToggle, scopeFeatherSlider },
actions: {
setScopeEnabled: (value) => calls.push(['enabled', value]),
setScopeFeather: (value) => calls.push(['feather', value]),
},
});
scopeToggle.checked = false;
scopeToggle.dispatchEvent(new Event('change'));
scopeToggle.checked = true;
scopeToggle.dispatchEvent(new Event('change'));
scopeFeatherSlider.dispatchEvent(new Event('input'));
controls.destroy();
scopeToggle.dispatchEvent(new Event('change'));
assert.deepEqual(calls, [
['enabled', false],
['enabled', true],
['feather', 200],
]);
});