refactor(ui): own Display control subscriptions and cleanup

This commit is contained in:
Sameh Khamis committed 2026-09-12 21:25:04 -07:00
1 parent 795ed68192
commit ffc65dd844
11 files changed
+307 -166

No files matched your search

+2
View File
@@ -13,6 +13,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Extract Display control bindings with synchronous listener cleanup; preserve existing visual actions and native input behavior.
- Extract application shortcuts and shader-parameter controls into reusable UI
components, preserving inputs and cleaning up listeners on rebuild/disposal.
+7
View File
@@ -243,3 +243,10 @@ Clearing permits reuse; destruction is final. Neither module imports the app,
renderer, persistence or services. The facade retains panel visibility, share
restore claims and render scheduling. Both controls are destroyed before the
facade's asynchronous teardown can yield.
## Display controls
`ui/display` owns Display button, selector and slider subscriptions. It receives
DOM elements and explicit actions, imports no application or effect singleton,
and releases every listener on destruction. Settings and rendering remain with
the caller.
+7
View File
@@ -1,5 +1,12 @@
# God's Eye View Current State
## Display control ownership
Display button, selector and slider listeners have a single destroyable owner.
The application supplies actions and retains effect settings, restore claims and
rendering. Native keyboard editing, model modes and current defaults are preserved.
Destroying the UI removes these listeners before asynchronous teardown.
## Application shortcuts and shader parameter controls
`ui/input` supplies the bubbling application shortcut listener and generated
+2 -1
View File
@@ -122,6 +122,7 @@
"./ui/panels": "./src/ui/panelDisclosure.js",
"./ui/surfaces": "./src/ui/surfaceKeyboard.js",
"./ui/layout": "./src/ui/panelRails.js",
"./ui/input": "./src/ui/visualInput.js"
"./ui/input": "./src/ui/visualInput.js",
"./ui/display": "./src/ui/displayControls.js"
}
}
+5
View File
@@ -294,5 +294,10 @@
"src/ui/styleParameters.js"
],
"external": []
},
"display-controls": {
"exports": ["./ui/display"],
"modules": ["src/ui/displayControls.js"],
"external": []
}
}
+38
View File
@@ -140,6 +140,34 @@ try {
hudBefore,
),
);
const displayBefore = await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager._setBloomEnabled(true);
manager._bloomSlider.value = '80';
manager._bloomSlider.dispatchEvent(new Event('input', { bubbles: true }));
manager._bloomSlider.focus();
return { value: Number(manager._bloomSlider.value), contrast: manager._bloomStage.uniforms.contrast };
});
await page.keyboard.press('ArrowRight');
check('native Display slider updates effect state', await page.evaluate((before) => {
const manager = window.__godsEyeView.styleManager;
return Number(manager._bloomSlider.value) > before.value && manager._bloomStage.uniforms.contrast < before.contrast;
}, displayBefore));
check('Display buttons toggle current state once', await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
const before = manager.bloomEnabled;
manager._bloomBtn.click();
const changed = manager.bloomEnabled !== before;
manager._bloomBtn.click();
return changed && manager.bloomEnabled === before;
}));
check('Display rebind removes old listeners', await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager._initUI();
const before = manager.bloomEnabled;
manager._bloomBtn.click();
return manager.bloomEnabled !== before;
}));
fs.mkdirSync('qa-shots/visual-input', { recursive: true });
await page.screenshot({ path: 'qa-shots/visual-input/desktop.png' });
await page.setViewport({ width: 620, height: 900 });
@@ -184,6 +212,16 @@ try {
);
}),
);
check('destroyed Display controls cannot change settings', await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager._displayControls.destroy();
const before = manager.bloomEnabled;
const contrast = manager._bloomStage.uniforms.contrast;
manager._bloomBtn.click();
manager._bloomSlider.value = '15';
manager._bloomSlider.dispatchEvent(new Event('input', { bubbles: true }));
return manager.bloomEnabled === before && manager._bloomStage.uniforms.contrast === contrast;
}));
check('runtime has no uncaught browser errors', errors.length === 0);
} finally {
await browser.close();
+3 -1
View File
@@ -246,7 +246,9 @@ test('detection-on-by-default is a default, not an operator override', () => {
// preset consults the flag, and the detection button sets it.
assert.match(uiSource, /if \(preset\.detection && !this\._detectionUserOverridden\) \{/,
'a style preset still yields to an operator who changed detection by hand');
const detectionButton = uiBlock("this._detectionBtn.addEventListener('click'", 'cycleDetectionMode()');
const displayActions = uiSource.slice(uiSource.indexOf('this._displayControls ='));
const detectionButton = displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:'));
assert.match(detectionButton, /cycleDetectionMode\(\)/, 'the button still invokes the detection action');
assert.match(detectionButton, /this\._detectionUserOverridden = true;/,
'and the detection control still claims the override when the operator uses it');
+15 -27
View File
@@ -419,16 +419,16 @@ test('every explicit visual UI gesture claims restore authority before it mutate
const gestureRoutes = [
['toggleHud: () => {', 'toggleOrbit: () =>', 'this.hud.toggle()', 'HUD hotkey'],
['cycleDetection: () => {', 'toggleCctv: () =>', 'cycleDetectionMode()', 'detection hotkey'],
['// Bloom toggle', '// Bloom intensity slider', 'this._setBloomEnabled(', 'bloom button'],
['// Bloom intensity slider', '// Sharpen toggle', 'this._setBloomIntensity(', 'bloom slider'],
['// Sharpen toggle', '// Scope mask', 'this._setSharpenEnabled(', 'sharpen button'],
["this._scopeBtn?.addEventListener('click'", "this._scopeFeatherSlider?.addEventListener('input'", 'setScopeMaskEnabled(', 'scope button'],
["this._scopeFeatherSlider?.addEventListener('input'", 'if (this._sharpenSlider)', 'setScopeMaskFeather(', 'scope feather slider'],
["this._sharpenSlider.addEventListener('input'", 'if (this._hudLayoutSelect)', 'this._applySharpenIntensity(', 'sharpen slider'],
["this._hudLayoutSelect.addEventListener('change'", 'if (this._cleanViewBtn)', 'this._setHudVariant(', 'HUD layout select'],
["this._detectionDensitySlider.addEventListener('input'", 'for (const button of this._detectionAllocationBtns)', 'this._applyDetectionDensityFromUi()', 'detection density slider'],
["button.addEventListener('click'", 'for (const slider of [this._detectionFadeSlider', 'this._setDetectionAllocation(', 'detection allocation button'],
["slider?.addEventListener('input'", 'if (this._celestialBtn)', 'this._applyDetectionFadeFromUi()', 'detection fade controls'],
['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'],
['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'],
['toggleSharpen:', 'toggleScope:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
['toggleScope:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'toggleScope control'],
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'],
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'],
['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'],
['setFade:', 'toggleCelestial:', 'this._applyDetectionFadeFromUi()', 'setFade control'],
];
for (const [start, end, mutation, label] of gestureRoutes) {
const startIndex = initUi.indexOf(start);
@@ -437,23 +437,10 @@ test('every explicit visual UI gesture claims restore authority before it mutate
assertClaimsBefore(initUi.slice(startIndex, endIndex), mutation, label);
}
const hudToggle = sourceBlock(' _initHUDToggle() {', ' _initCockpitDisplayPortal() {');
assertClaimsBefore(
hudToggle.slice(
hudToggle.indexOf("this._hudBtn.addEventListener('click'"),
hudToggle.indexOf('if (this._hudLayoutSelect)'),
),
'this.hud.toggle()',
'HUD button',
);
assertClaimsBefore(
hudToggle.slice(
hudToggle.indexOf("this._detectionBtn.addEventListener('click'"),
hudToggle.indexOf('this._cockpitDisplayToggleBtn'),
),
'cycleDetectionMode()',
'detection button',
);
const displayActions = initUi.slice(initUi.indexOf('this._displayControls ='));
assertClaimsBefore(displayActions.slice(displayActions.indexOf('toggleHud:'), displayActions.indexOf('cycleDetection:')), 'this.hud.toggle()', 'HUD button');
assertClaimsBefore(displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:')), 'cycleDetectionMode()', 'detection button');
});
// Contacts OWNS detection while it is active (forced Dense @ 75%). That makes
@@ -703,5 +690,6 @@ test('visual input listeners are revoked before asynchronous UI teardown', () =>
assert.ok(firstAwait > 0);
const synchronous = disposal.slice(0, firstAwait);
assert.match(synchronous, /this\._applicationShortcuts\?\.destroy\(\)/);
assert.match(synchronous, /this\._displayControls\?\.destroy\(\)/);
assert.match(synchronous, /this\._styleParameters\?\.destroy\(\)/);
});
+110 -137
View File
@@ -1,3 +1,4 @@
import { bindDisplayControls } from './ui/displayControls.js';
import { bindApplicationShortcuts, createStyleParameters } from './ui/visualInput.js';
import { layoutLeftPanelRail, layoutRightPanelRail } from './ui/panelRails.js';
import { bindPanelDisclosure, collapsePanelOnEscape, createHoverDisclosure } from './ui/panelDisclosure.js';
@@ -3395,11 +3396,6 @@ export class StyleManager {
* @returns {void}
*/
_initUI() {
// Style buttons
document.querySelectorAll('.style-btn').forEach(btn => {
btn.addEventListener('click', () => this.setStyle(btn.dataset.style));
});
this._applicationShortcuts?.destroy();
this._applicationShortcuts = bindApplicationShortcuts({
documentRef: document,
@@ -3432,108 +3428,109 @@ export class StyleManager {
},
});
// Bloom toggle
this._bloomBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomEnabled(!this.bloomEnabled);
this._displayControls?.destroy();
this._displayControls = bindDisplayControls({
elements: {
styleButtons: document.querySelectorAll('.style-btn'),
bloomButton: this._bloomBtn, bloomSlider: this._bloomSlider,
sharpenButton: this._sharpenBtn, sharpenSlider: this._sharpenSlider,
scopeButton: this._scopeBtn, scopeFeatherSlider: this._scopeFeatherSlider,
hudLayout: this._hudLayoutSelect, hudButton: this._hudBtn,
cleanViewButton: this._cleanViewBtn, cleanViewExitButton: this._cleanViewExitBtn,
densitySlider: this._detectionDensitySlider, detectionButton: this._detectionBtn,
allocationButtons: this._detectionAllocationBtns,
fadeSliders: [this._detectionFadeSlider, this._detectionOpacitySlider],
celestialButton: this._celestialBtn,
modelsButton: this._models3dBtn,
modelModeButtons: this._models3dBtn ? this._models3dModeBtns : [],
},
actions: {
setStyle: (style) => this.setStyle(style),
toggleBloom: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomEnabled(!this.bloomEnabled);
},
setBloomIntensity: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomIntensity(value);
},
toggleSharpen: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setSharpenEnabled(!this.sharpenEnabled);
},
toggleScope: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const next = !isScopeMaskEnabled();
setScopeMaskEnabled(next);
this._scopeBtn.classList.toggle('active', next);
this._scopeBtn.setAttribute('aria-pressed', String(next));
this._syncShareState();
},
setScopeFeather: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = Math.max(0, Math.min(100, value || 0));
if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
this._syncShareState();
},
setSharpenIntensity: (pct) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
if (this._sharpenSliderValue) this._sharpenSliderValue.textContent = `${pct}%`;
this._applySharpenIntensity(pct / 100);
this._syncShareState();
},
setHudLayout: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setHudVariant(value);
},
toggleCleanView: () => this.toggleCleanView(),
exitCleanView: () => this.toggleCleanView(false),
setDensity: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
const pct = canonicalizeDensity(value);
this._detectionDensitySlider.value = String(pct);
if (this._detectionDensityValue) this._detectionDensityValue.textContent = `${pct}%`;
this._applyDetectionDensityFromUi();
this._syncShareState();
},
setAllocation: (value) => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
this._setDetectionAllocation(value);
},
setFade: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._applyDetectionFadeFromUi();
this._syncShareState();
},
toggleCelestial: () => {
const ringIsVisible = !!this.celestialRing?.visible;
if (!this.celestialRingEnabled || !ringIsVisible) {
this.setCelestialRingEnabled(true, { focus: true });
} else {
this.setCelestialRingEnabled(false);
}
},
toggleHud: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this.hud.toggle();
this._updateHudButtonState();
this._syncShareState();
},
cycleDetection: () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
cycleDetectionMode();
this._syncShareState();
},
toggleModels: () => {
this._setModels3dEnabled(!this._models3dEnabled);
this._syncModels3dModeRow();
},
setModelsMode: (mode) => this._setModels3dMode(mode),
},
});
// Bloom intensity slider
this._bloomSlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setBloomIntensity(parseInt(this._bloomSlider.value, 10));
});
// Sharpen toggle
this._sharpenBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setSharpenEnabled(!this.sharpenEnabled);
});
// Scope mask — the explicit circular viewport treatment (owner ask:
// standalone toggle + featherable edge; see src/scopeMask.js).
this._scopeBtn?.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const next = !isScopeMaskEnabled();
setScopeMaskEnabled(next);
this._scopeBtn.classList.toggle('active', next);
this._scopeBtn.setAttribute('aria-pressed', String(next));
this._syncShareState();
});
this._scopeFeatherSlider?.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = Math.max(0, Math.min(100, parseInt(this._scopeFeatherSlider.value, 10) || 0));
if (this._scopeFeatherValue) this._scopeFeatherValue.textContent = `${pct}%`;
setScopeMaskFeather(pct / 100);
this._syncShareState();
});
if (this._sharpenSlider) {
this._sharpenSlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
const pct = parseInt(this._sharpenSlider.value, 10);
if (this._sharpenSliderValue) {
this._sharpenSliderValue.textContent = `${pct}%`;
}
this._applySharpenIntensity(pct / 100);
this._syncShareState();
});
}
if (this._hudLayoutSelect) {
this._hudLayoutSelect.addEventListener('change', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._setHudVariant(this._hudLayoutSelect.value);
});
}
if (this._cleanViewBtn) {
this._cleanViewBtn.addEventListener('click', () => this.toggleCleanView());
}
if (this._cleanViewExitBtn) {
this._cleanViewExitBtn.addEventListener('click', () => this.toggleCleanView(false));
}
if (this._detectionDensitySlider) {
this._detectionDensitySlider.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
const pct = canonicalizeDensity(this._detectionDensitySlider.value);
this._detectionDensitySlider.value = String(pct);
if (this._detectionDensityValue) {
this._detectionDensityValue.textContent = `${pct}%`;
}
this._applyDetectionDensityFromUi();
this._syncShareState();
});
}
for (const button of this._detectionAllocationBtns) {
button.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
this._setDetectionAllocation(button.dataset.allocation);
});
}
for (const slider of [this._detectionFadeSlider, this._detectionOpacitySlider]) {
slider?.addEventListener('input', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._applyDetectionFadeFromUi();
this._syncShareState();
});
}
if (this._celestialBtn) {
this._celestialBtn.addEventListener('click', () => {
const ringIsVisible = !!this.celestialRing?.visible;
if (!this.celestialRingEnabled || !ringIsVisible) {
this.setCelestialRingEnabled(true, { focus: true });
} else {
this.setCelestialRingEnabled(false);
}
});
}
}
/**
@@ -9395,26 +9392,15 @@ export class StyleManager {
this._layoutRightPanels();
}
_syncModels3dModeRow() {
if (this._models3dModeRow) this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
this._layoutRightPanels();
}
_initModels3dToggle() {
if (!this._models3dBtn) return;
// The Proximity/All mode row is revealed only while 3D is on (mirrors the DETECT slider row).
const syncModeRow = () => {
if (this._models3dModeRow) this._models3dModeRow.classList.toggle('visible', this._models3dEnabled);
this._layoutRightPanels();
};
this._models3dBtn.addEventListener('click', () => {
this._setModels3dEnabled(!this._models3dEnabled);
syncModeRow();
});
for (const btn of this._models3dModeBtns) {
if (!btn) continue;
btn.addEventListener('click', () => {
const mode = btn.dataset.mode === 'all' ? 'all' : 'proximity';
this._setModels3dMode(mode);
});
}
this._syncModels3dButtonState();
syncModeRow();
this._syncModels3dModeRow();
}
_setModels3dEnabled(enabled) {
@@ -9447,13 +9433,6 @@ export class StyleManager {
}
_initHUDToggle() {
this._hudBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this.hud.toggle();
this._updateHudButtonState();
this._syncShareState();
});
if (this._hudLayoutSelect) {
this._hudLayoutSelect.value = 'tactical';
}
@@ -9461,13 +9440,6 @@ export class StyleManager {
this.hud.setMode('on');
this._updateHudButtonState();
// Detection toggle button
this._detectionBtn.addEventListener('click', () => {
this.shareLinkManager?.claimRestoreLane?.('visual');
this._detectionUserOverridden = true;
cycleDetectionMode();
this._syncShareState();
});
this._cockpitDisplayToggleBtn?.addEventListener('click', () => {
const open = this._cockpitDisplayToggleBtn.getAttribute('aria-expanded') === 'true';
this._setCockpitDisclosure?.('display', !open);
@@ -9690,6 +9662,7 @@ export class StyleManager {
if (this._globalLoadingStatus) this._globalLoadingStatus.hidden = true;
this._disposed = true;
this._applicationShortcuts?.destroy();
this._displayControls?.destroy();
this._styleParameters?.destroy();
for (const control of this._panelDisclosureControls || []) control.destroy();
this._panelDisclosureControls = [];
+41
View File
@@ -0,0 +1,41 @@
/**
* Bind Display controls to application actions. Inputs remain native DOM controls;
* the caller owns settings and effect state. Destroy before replacing the view.
* @param {{ elements: object, actions: object }} options
* @returns {{ destroy(): void }}
*/
export function bindDisplayControls({ elements, actions }) {
const removers = [];
const listen = (element, type, action, read) => {
if (!element) return;
const handler = () => actions[action](...(read ? [read(element)] : []));
element.addEventListener(type, handler);
removers.push(() => element.removeEventListener(type, handler));
};
const integer = (element) => parseInt(element.value, 10);
for (const [name, action] of [
['bloomButton', 'toggleBloom'], ['sharpenButton', 'toggleSharpen'],
['scopeButton', 'toggleScope'], ['cleanViewButton', 'toggleCleanView'],
['cleanViewExitButton', 'exitCleanView'], ['celestialButton', 'toggleCelestial'],
['hudButton', 'toggleHud'], ['detectionButton', 'cycleDetection'],
['modelsButton', 'toggleModels'],
]) listen(elements[name], 'click', action);
for (const [name, action] of [
['bloomSlider', 'setBloomIntensity'], ['sharpenSlider', 'setSharpenIntensity'],
['scopeFeatherSlider', 'setScopeFeather'],
]) listen(elements[name], 'input', action, integer);
listen(elements.densitySlider, 'input', 'setDensity', (el) => el.value);
listen(elements.hudLayout, 'change', 'setHudLayout', (el) => el.value);
for (const el of elements.styleButtons || [])
listen(el, 'click', 'setStyle', (el) => el.dataset.style);
for (const el of elements.allocationButtons || [])
listen(el, 'click', 'setAllocation', (el) => el.dataset.allocation);
for (const el of elements.modelModeButtons || [])
listen(el, 'click', 'setModelsMode', (el) => el.dataset.mode === 'all' ? 'all' : 'proximity');
for (const el of elements.fadeSliders || []) listen(el, 'input', 'setFade');
return {
destroy() {
for (const remove of removers.splice(0)) remove();
},
};
}
+77
View File
@@ -0,0 +1,77 @@
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('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('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: { fadeSliders: [null, a] }, actions: { setFade: () => calls.push('a') } });
const two = bindDisplayControls({ elements: { fadeSliders: [b] }, actions: { setFade: () => calls.push('b') } });
one.destroy();
a.dispatchEvent(new Event('input'));
b.dispatchEvent(new Event('input'));
assert.deepEqual(calls, ['b']);
two.destroy();
});