// src/ui/displayPanel.test.mjs — the Display panel renders bloom and sharpen // from what VisualSettings publishes, and shows its initial markup before. import '../testSupport/registerDom.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import { createPinia } from 'pinia'; import { createApp, nextTick } from 'vue'; import { useDisplayStore } from './stores/display.js'; import DisplayPanel from './views/panels/DisplayPanel.vue'; import { VisualSettings } from './visualSettings.js'; function fixture(t) { document.body.replaceChildren(); const host = document.body.appendChild(document.createElement('div')); const pinia = createPinia(); const app = createApp(DisplayPanel).use(pinia); app.mount(host); t.after(() => app.unmount()); const $ = (id) => document.getElementById(id); return { store: useDisplayStore(pinia), $ }; } /** A VisualSettings over stand-in effects, publishing to `store`. */ function settings(store) { const owner = Object.create(VisualSettings.prototype); const effects = { bloomEnabled: false, sharpenEnabled: false, bloomIntensity: 0, sharpenIntensity: 0.49, applyBloomIntensity(value) { this.bloomIntensity = value; }, applySharpenIntensity(value) { this.sharpenIntensity = value; }, setBloomEnabled(on) { this.bloomEnabled = on; }, setSharpenEnabled(on) { this.sharpenEnabled = on; }, syncBloomEnabled() {}, }; const shares = []; Object.assign(owner, { store, _visualEffects: effects, _sharpenPct: 49, services: { governorRequestRender() {} }, _syncShareState: () => shares.push(true), _layoutRightPanels() {}, }); return { owner, effects, shares }; } test('before VisualSettings renders, the panel shows its initial markup', (t) => { const { $ } = fixture(t); assert.equal($('bloom-toggle').className, 'pp-toggle-btn'); assert.equal($('bloom-slider-row').className, 'pp-slider-row'); assert.equal($('bloom-intensity-slider').value, '0'); assert.equal($('bloom-intensity-value').textContent, '0%'); assert.equal($('sharpen-intensity-slider').value, '49'); assert.equal($('sharpen-intensity-value').textContent, '49%'); }); test('bloom and sharpen render their state, slider and readout', async (t) => { const { store, $ } = fixture(t); const { owner, effects, shares } = settings(store); owner._setBloomEnabled(true); owner._setBloomIntensity(140.4); await nextTick(); assert.equal($('bloom-toggle').classList.contains('active'), true); assert.equal($('bloom-slider-row').classList.contains('visible'), true); assert.equal($('bloom-intensity-slider').value, '140'); assert.equal($('bloom-intensity-value').textContent, '140%'); assert.equal(effects.bloomIntensity, 140); owner._setSharpenEnabled(true); owner._setSharpenIntensity(130); await nextTick(); assert.equal($('sharpen-toggle').classList.contains('active'), true); assert.equal($('sharpen-slider-row').classList.contains('visible'), true); assert.equal($('sharpen-intensity-slider').value, '100', 'clamped to 100'); assert.equal($('sharpen-intensity-value').textContent, '100%'); assert.equal(effects.sharpenIntensity, 1); assert.equal(owner.sharpenIntensityPct, 100); assert.ok(shares.length > 0, 'changes reach the share state'); owner._setBloomEnabled(false); await nextTick(); assert.equal($('bloom-toggle').classList.contains('active'), false); assert.equal($('bloom-slider-row').classList.contains('visible'), false); assert.equal($('bloom-intensity-value').textContent, '140%', 'the amount stays'); }); test('a publish never rewrites a slider the user is dragging', async (t) => { const { store, $ } = fixture(t); const { owner } = settings(store); owner._setSharpenIntensity(30); await nextTick(); const slider = $('sharpen-intensity-slider'); let writes = 0; const descriptor = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(slider), 'value', ); Object.defineProperty(slider, 'value', { get: () => descriptor.get.call(slider), set: (next) => { writes++; descriptor.set.call(slider, next); }, }); // The input handler applies the dragged value; the publish that follows // carries the same value, so the slider is left alone. owner._setSharpenIntensity(30); owner._setBloomEnabled(true); await nextTick(); assert.equal(writes, 0); assert.equal(slider.value, '30'); }); /** VisualSettings with a stand-in detection engine. */ function detectionSettings(store) { const { owner } = settings(store); const engine = { mode: 'OFF', tuning: { densityPct: 50, allocationStrategy: 'ELASTIC' }, fade: { fadeRatio: 0.07, outsideOpacity: 0.01 }, }; Object.assign(owner, { _detectionDensityPct: 50, _detectionFadePct: 7, _detectionOutsidePct: 1, _detectionModeLabel: null, _detectionAllocationShown: null, viewer: { scene: { requestRender() {} } }, }); owner.services = { ...owner.services, getDetectionMode: () => engine.mode, setDetectionModeByLabel: (label) => { engine.mode = label; }, getDetectionTuning: () => ({ ...engine.tuning }), setDetectionTuning: (next) => Object.assign(engine.tuning, next), getKeyholeFadeTuning: () => ({ ...engine.fade }), setKeyholeFadeTuning: (next) => Object.assign(engine.fade, next), }; return { owner, engine }; } test('detection renders its mode, rows, sliders and allocation', async (t) => { const { store, $ } = fixture(t); const { owner, engine } = detectionSettings(store); const button = $('detection-toggle'); assert.equal(button.querySelector('.pp-label').textContent, 'DETECT'); assert.equal(button.getAttribute('aria-label'), 'Detection overlay'); owner._setDetectionMode('DENSE'); owner._setDetectionDensity(80); await nextTick(); assert.equal(button.querySelector('.pp-label').textContent, 'DENSE'); assert.equal(button.getAttribute('aria-pressed'), 'true'); assert.equal(button.getAttribute('aria-label'), 'Detection overlay: dense'); assert.equal(button.className, 'pp-toggle-btn active panoptic'); for (const id of [ 'detection-slider-row', 'detection-allocation-row', 'detection-fade-row', 'detection-opacity-row', ]) assert.equal($(id).classList.contains('visible'), true, id); assert.equal($('detection-density-slider').value, '75', 'nearest stop'); assert.equal($('detection-density-value').textContent, '75%'); assert.equal(engine.tuning.densityPct, 75); owner._setDetectionFade({ fadePct: 55 }); owner._setDetectionAllocation('weighted', { persist: false }); await nextTick(); assert.equal($('detection-fade-slider').value, '40', 'clamped to 40'); assert.equal($('detection-fade-value').textContent, '40%'); assert.equal($('detection-opacity-value').textContent, '1%', 'kept'); assert.equal(engine.fade.fadeRatio, 0.4); const weighted = $('detection-allocation-weighted'); const elastic = $('detection-allocation-elastic'); assert.equal(weighted.classList.contains('active'), true); assert.equal(weighted.getAttribute('aria-checked'), 'true'); assert.equal(elastic.classList.contains('active'), false); assert.equal(elastic.getAttribute('aria-checked'), 'false'); assert.deepEqual(owner.getDetectionState(), { detectionMode: 'DENSE', densityPct: 75, allocationStrategy: 'WEIGHTED', fadePct: 40, outsideOpacityPct: 1, }); owner._setDetectionMode('OFF'); await nextTick(); assert.equal(button.querySelector('.pp-label').textContent, 'DETECT'); assert.equal(button.getAttribute('aria-label'), 'Detection overlay: off'); assert.equal(button.className, 'pp-toggle-btn'); assert.equal($('detection-slider-row').classList.contains('visible'), false); });