Files
feedBack/plugins/highway_3d/tests/background_control.test.js
T
byrongamatosandClaude Sonnet 4.6 c4eebe1c9f refactor(h3d-carve-5): move H-section player-chrome bg-control to src/bg-control.js
Extracted _pc* subsystem (420 lines) from screen.js IIFE into
src/bg-control.js using a factory DI pattern (createBgControl({...})).

Exports: createBgControl({ BG_STYLE_IDS, _bgReadGlobal, _bgSubscribe,
         _bgUnsubscribe, getVenueSceneOverride })
         → { _pcAcquire, _pcRelease }
screen.js: const { _pcAcquire, _pcRelease } = createBgControl({...})

Beyond-subst (2):
1. Factory wrapper (IIFE-scope closure → DI params) — factory export pattern
   required because DI values are IIFE-scope, not ES module imports.
2. _venueSceneOverride → getVenueSceneOverride() (live accessor, 1 call site
   in _pcSync — mutable let at screen.js:1523 must be read per-call).

DI values all defined before the createBgControl call (screen.js):
- BG_STYLE_IDS: line 1435 | _bgReadGlobal: 1825
- _bgSubscribe/_bgUnsubscribe: 1917-18 | _venueSceneOverride: 1523
First _pcAcquire caller: init() in createFactory() (~line 14850 post-cut).
Construction order correct: createBgControl call before createFactory.

Surprise declared to god before commit (outbox/h3d-cut5-surprise.json):
No bc-panel.js dependency — §8's anticipation was wrong. The
_bcCreateController call at what was ~line 8082 is in _bcSyncMode
(P-section / factory scope), not the H-section. bg-control.js has ZERO
dependency on bc-panel.js.

Tests:
- tests/js/highway_3d_bg_control.test.js (new, 13 class-killers):
  stranded-caller (test 13, factory-adapted from bc-panel test 12),
  construction-order (tests 11-12), DI completeness (test 2),
  live-accessor enforcement (test 3), lifecycle (tests 4-7).
- plugins/highway_3d/tests/background_control.test.js: retargeted from
  screen.js slice → bg-control.js factory eval; all 20 existing behaviour
  tests preserved (load() uses vm.createContext + augmented return getters).
- tests/js/highway_3d_panel_controls.test.js: createBgControl stub added.

Suite: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
  base 222/222 (f69c544) → tip 235/235 (+13: 13 new class-killers)
plugin.json: 3.40.0 → 3.41.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 09:25:44 +02:00

549 lines
25 KiB
JavaScript

// Player-chrome background control.
//
// The control mounts a Background picker (style / Reactive / Intensity) into
// the player's Plugin Controls popover so the background can be changed
// mid-song. Two things about it are easy to get wrong and invisible when they
// are:
//
// * It is REFCOUNTED. Several renderer instances can be live at once (a
// splitscreen host creates one per panel), but the settings it writes are
// global — N controls would be N ways to set one value, and a leaked
// refcount pins a dead control in the UI. The multi-instance behaviour is
// exercised here with stubbed instances; it is NOT verified against a real
// splitscreen session, whose visualizer does not currently work.
// * It GREYS OUT controls the active style ignores. Not every background
// style reads `intensity`, and none of them read audio bands under
// Butterchurn, so a live-looking knob that does nothing is a real bug.
//
// h3d-carve-5: the _pc* block was moved from screen.js to src/bg-control.js.
// load() now evaluates the factory module (stripping the `export` keyword),
// calls createBgControl({DI}) with stubbed deps, and injects test-only getters
// into the return object so the private state vars remain observable.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const SCREEN_JS = path.join(__dirname, '..', 'screen.js');
const BG_CONTROL_JS = path.join(__dirname, '..', 'src', 'bg-control.js');
// What each style is expected to consume, derived by reading the BG_STYLES
// bodies in screen.js — deliberately NOT read from the plugin's own _PC_USES
// table, which would only assert that the table equals itself.
// intensity: true => the style's build() reads settings.intensity
// reactive: true => the style's update() dereferences its `bands` argument
// 'butterchurn' is a mode, not a BG_STYLES fog-scenery entry: _bcSyncMode
// owns its controller and drives its own audio tap + canvas opacity (only
// the fog-scenery half falls through to BG_STYLES.off), so both are false.
const EXPECTED_USES = {
off: { intensity: false, reactive: false },
particles: { intensity: true, reactive: true },
silhouettes: { intensity: true, reactive: true },
lights: { intensity: true, reactive: true },
geometric: { intensity: true, reactive: true },
image: { intensity: true, reactive: false },
video: { intensity: false, reactive: false },
butterchurn: { intensity: false, reactive: false },
};
const BG_STYLE_IDS = ['off', 'particles', 'silhouettes', 'lights', 'geometric', 'butterchurn', 'image', 'video'];
// Minimal DOM: only what the control touches.
function makeDom() {
class El {
constructor(tag) {
this.tagName = String(tag).toUpperCase();
this.children = [];
this.parentNode = null;
this.listeners = {};
this.style = { cssText: '' };
this.disabled = false;
this._on = false;
}
appendChild(c) { c.parentNode = this; this.children.push(c); return c; }
removeChild(c) {
const i = this.children.indexOf(c);
if (i >= 0) this.children.splice(i, 1);
c.parentNode = null;
return c;
}
addEventListener(t, fn) { (this.listeners[t] || (this.listeners[t] = [])).push(fn); }
setAttribute(k, v) { this[k] = v; }
removeAttribute(k) { delete this[k]; }
get isConnected() {
let n = this;
while (n.parentNode) n = n.parentNode;
return n === root;
}
querySelector(sel) {
const m = /^option\[value="(.+)"\]$/.exec(sel);
const want = m ? m[1] : null;
const walk = (n) => {
for (const c of n.children) {
if (want != null && c.tagName === 'OPTION' && c.value === want) return c;
const r = walk(c);
if (r) return r;
}
return null;
};
return walk(this);
}
fire(type) { (this.listeners[type] || []).forEach((fn) => fn()); }
}
const root = new El('root');
const slot = new El('div');
root.appendChild(slot);
return { El, root, slot };
}
function load({ store: initialStore } = {}) {
// h3d-carve-5: load from src/bg-control.js (factory module) instead of
// slicing screen.js. Strip `export` for vm eval; inject test-only getters
// into the return object so private _pc* state vars remain observable.
const bgSrc = fs.readFileSync(BG_CONTROL_JS, 'utf8');
const stripped = bgSrc.replace(/^export\s+/gm, '');
// Augment the factory's return with accessor getters for private state so
// all existing test assertions (api.el, api.sel, api.refs, ...) keep working.
const instrumented = stripped.replace(
/return\s*\{\s*_pcAcquire\s*,\s*_pcRelease\s*\}/,
'return { _pcAcquire, _pcRelease,'
+ ' get el() { return _pcEl; },'
+ ' get sel() { return _pcSel; },'
+ ' get react() { return _pcReactive; },'
+ ' get intens() { return _pcIntensity; },'
+ ' get reason() { return _pcReason; },'
+ ' get refs() { return _pcRefs; } }',
);
assert.notEqual(instrumented, stripped, 'return-augmentation anchor not found in bg-control.js');
const dom = makeDom();
const store = Object.assign({
style: 'particles',
reactive: true,
intensity: 0.5,
customImageDataUrl: '',
customVideoName: '',
}, initialStore);
const bus = {};
const listeners = new Set();
const emit = (key) => { for (const fn of listeners) fn(key); };
const writes = [];
const timers = [];
// DI dependencies as vm-globals. Tests mutate sandbox._venueSceneOverride
// directly; getVenueSceneOverride in the factory call reads it via the vm global.
const sandbox = {
console,
BG_STYLE_IDS,
_venueSceneOverride: false,
_bgReadGlobal: (key) => store[key],
_bgSubscribe: (fn) => listeners.add(fn),
_bgUnsubscribe: (fn) => listeners.delete(fn),
setTimeout: (fn) => { timers.push(fn); return timers.length; },
clearTimeout: () => {},
document: {
createElement: (t) => new dom.El(t),
// The Settings-panel mirror looks these up; absent here so it no-ops.
getElementById: () => null,
},
window: {
feedBack: {
uiVersion: 'v3', // _pcSlot gates on this (docs/plugin-v3-ui.md)
ui: { playerControlSlot: () => dom.slot },
// The real bus is an EventTarget wrapper exposing on/off. Modelled
// here so the screen:changed subscription — and its removal — are
// observable.
on: (ev, fn) => { (bus[ev] || (bus[ev] = [])).push(fn); },
off: (ev, fn) => {
const l = bus[ev];
if (!l) return;
const i = l.indexOf(fn);
if (i >= 0) l.splice(i, 1);
},
},
h3dBgSetStyle: (v) => { writes.push(['style', v]); store.style = v; emit('style'); },
h3dBgSetReactive: (v) => { writes.push(['reactive', v]); store.reactive = v; emit('reactive'); },
h3dBgSetIntensity: (v) => { writes.push(['intensity', v]); store.intensity = v; emit('intensity'); },
},
};
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
// Step 1: define createBgControl in the vm context.
vm.runInContext(instrumented, sandbox);
// Step 2: call the factory; DI values are vm-globals so the call names them directly.
const api = vm.runInContext(
'createBgControl({ BG_STYLE_IDS, _bgReadGlobal, _bgSubscribe, _bgUnsubscribe,'
+ ' getVenueSceneOverride: () => _venueSceneOverride })',
sandbox,
);
const fireScreenChanged = () => (bus['screen:changed'] || []).slice().forEach((fn) => fn());
const screenHooks = () => (bus['screen:changed'] || []).length;
return { api, dom, store, emit, writes, timers, sandbox, listenerCount: () => listeners.size, fireScreenChanged, screenHooks };
}
// Slice the real _bgReadSetting + _bgReadGlobal out of screen.js and run them
// against a localStorage stub. The main suite stubs both helpers identically,
// so it can't tell the #2 refactor from a no-op; this one proves the actual
// helper bodies differ where they must: _bgReadGlobal ignores a per-panel
// override that _bgReadSetting(panelKey, ...) still honours.
test('_bgReadGlobal reads the global slot, ignoring per-panel overrides', () => {
const src = fs.readFileSync(SCREEN_JS, 'utf8');
const rgStart = src.indexOf(' function _bgReadSetting(panelKey, key) {');
const rgEnd = src.indexOf(' // Shared "stored string -> bool" coercion');
assert.ok(rgStart !== -1 && rgEnd > rgStart, 'could not slice the read helpers');
const block = src.slice(rgStart, rgEnd);
const storage = new Map();
const sandbox = {
localStorage: { getItem: (k) => (storage.has(k) ? storage.get(k) : null) },
_bgCoerce: (_key, v) => v, // identity: we test key resolution, not coercion
_bgMemFallback: Object.create(null),
BG_DEFAULTS: { style: 'particles' },
};
sandbox.globalThis = sandbox;
const api = vm.runInNewContext(block + '\n({ _bgReadSetting, _bgReadGlobal, _bgMemFallback })', sandbox);
storage.set('h3d_bg_style', 'lights'); // global
storage.set('h3d_bg_panel3_style', 'geometric'); // a per-panel override
// The renderer, reading with a panel key, honours the per-panel override...
assert.equal(api._bgReadSetting('panel3', 'style'), 'geometric');
// ...but the shared control's global read must NOT see it - this is the
// whole point of #2 (previously _bgReadSetting(null, ...) relied on
// 'h3d_bg_null_style' never existing).
assert.equal(api._bgReadGlobal('style'), 'lights');
// In-memory staged value wins over the persisted global (matches
// _bgReadSetting's precedence).
api._bgMemFallback.style = 'aurora';
assert.equal(api._bgReadGlobal('style'), 'aurora');
delete api._bgMemFallback.style;
// Nothing stored -> BG_DEFAULTS.
assert.equal(api._bgReadGlobal('style'), 'lights');
storage.delete('h3d_bg_style');
assert.equal(api._bgReadGlobal('style'), 'particles');
});
test('mounts one control into the player-control slot', () => {
const { api, dom } = load();
api._pcAcquire();
assert.equal(dom.slot.children.length, 1);
assert.ok(api.sel, 'style dropdown was not created');
assert.equal(api.sel.children.length, BG_STYLE_IDS.length, 'one option per style');
});
test('multiple renderer instances share a single control', () => {
const { api, dom } = load();
api._pcAcquire();
api._pcAcquire();
api._pcAcquire();
api._pcAcquire();
assert.equal(dom.slot.children.length, 1, 'four instances must not mount four controls');
assert.equal(api.refs, 4);
api._pcRelease();
api._pcRelease();
api._pcRelease();
assert.equal(dom.slot.children.length, 1, 'still held by the last instance');
api._pcRelease();
assert.equal(dom.slot.children.length, 0, 'last release must unmount');
assert.equal(api.el, null);
});
test('binds the screen hook on a retry when the bus was not ready at acquire', () => {
const ctl = load();
// Cold load: on a fresh page the renderer can init before the event bus is
// wired AND before the rail popover exists. Simulate both being absent.
const savedOn = ctl.sandbox.window.feedBack.on;
const savedUi = ctl.sandbox.window.feedBack.ui;
delete ctl.sandbox.window.feedBack.on;
ctl.sandbox.window.feedBack.ui = {}; // no playerControlSlot -> mount fails
ctl.api._pcAcquire();
assert.equal(ctl.screenHooks(), 0, 'nothing to bind to yet');
assert.equal(ctl.api.el, null, 'no slot yet, so nothing mounted');
// Bus + slot come online; the retry tick must bind the hook, not only mount.
ctl.sandbox.window.feedBack.on = savedOn;
ctl.sandbox.window.feedBack.ui = savedUi;
ctl.timers.shift()(); // run one retry tick
assert.equal(ctl.screenHooks(), 1, 'the retry tick failed to bind the screen hook');
assert.ok(ctl.api.el, 'and it should have mounted too');
ctl.api._pcRelease();
});
test('the last release unbinds the screen:changed hook', () => {
const ctl = load();
ctl.api._pcAcquire();
assert.equal(ctl.screenHooks(), 1, 'acquire should subscribe once');
ctl.api._pcAcquire();
ctl.api._pcRelease();
assert.equal(ctl.screenHooks(), 1, 'a partial release must keep the hook');
ctl.api._pcRelease();
assert.equal(ctl.screenHooks(), 0, 'the hook outlived the control');
// And re-acquiring must re-subscribe exactly once, not zero times (the
// bind is guarded on _pcScreenHook, so failing to null it would leave the
// control permanently deaf to chrome rebuilds).
ctl.api._pcAcquire();
assert.equal(ctl.screenHooks(), 1, 're-acquire did not re-subscribe');
ctl.api._pcRelease();
});
test('teardown unsubscribes from the settings bus', () => {
const ctl = load();
ctl.api._pcAcquire();
assert.equal(ctl.listenerCount(), 1);
ctl.api._pcRelease();
assert.equal(ctl.listenerCount(), 0, 'listener leaked after unmount');
});
test('tracks changes made from the Settings page', () => {
const { api, store, emit } = load();
api._pcAcquire();
store.style = 'lights';
emit('style');
assert.equal(api.sel.value, 'lights');
});
test('custom media options stay disabled until something is uploaded', () => {
const { api, store, emit } = load();
api._pcAcquire();
assert.equal(api.sel.querySelector('option[value="image"]').disabled, true);
store.customImageDataUrl = 'data:image/png;base64,AAAA';
emit('customImageDataUrl');
assert.equal(api.sel.querySelector('option[value="image"]').disabled, false);
assert.equal(api.sel.querySelector('option[value="video"]').disabled, true, 'video is independent');
});
test('re-mounts into a fresh slot when the player chrome is rebuilt', () => {
const { api, dom, sandbox, listenerCount } = load();
api._pcAcquire();
const first = api.el;
dom.root.removeChild(dom.slot);
const fresh = new dom.El('div');
dom.root.appendChild(fresh);
sandbox.window.feedBack.ui.playerControlSlot = () => fresh;
api._pcAcquire();
assert.equal(fresh.children.length, 1, 'did not remount into the new slot');
assert.notEqual(api.el, first, 'stale node was reused');
assert.equal(listenerCount(), 1, 'remount must not double-subscribe');
});
test('a non-v3 host mounts nothing (uiVersion gate)', () => {
const ctl = load();
ctl.sandbox.window.feedBack.uiVersion = 'v2'; // pre-v3 shell
ctl.api._pcAcquire();
assert.equal(ctl.api.el, null, 'must not mount when uiVersion is not v3');
assert.equal(ctl.dom.slot.children.length, 0);
// A non-v3 shell has no slot and never will, so no retry should be scheduled
// at all — the loop is for a not-yet-built v3 slot, not for polling v2.
assert.equal(ctl.timers.length, 0, 'a non-v3 host must not schedule the retry loop');
ctl.api._pcRelease();
});
test('a host with no player-control slot mounts nothing and does not throw', () => {
const { api, dom, sandbox, timers } = load();
sandbox.window.feedBack.ui = {};
api._pcAcquire();
assert.equal(api.el, null);
assert.equal(dom.slot.children.length, 0);
let guard = 0;
while (timers.length && guard++ < 100) timers.shift()();
assert.ok(guard < 100, 'retry loop did not terminate');
});
test('intensity writes once on release, not on every drag step', () => {
const { api, writes } = load();
api._pcAcquire();
for (const v of ['0.10', '0.20', '0.30', '0.40', '0.50']) {
api.intens.value = v;
api.intens.fire('input');
}
assert.equal(writes.filter((w) => w[0] === 'intensity').length, 0,
'dragging must not write — every write rebuilds the background scene');
api.intens.fire('change');
assert.equal(writes.filter((w) => w[0] === 'intensity').length, 1,
'releasing must write exactly once');
});
test('the dropdown and Reactive pill drive the real setters', () => {
const { api, store, writes } = load();
api._pcAcquire();
api.sel.value = 'geometric';
api.sel.fire('change');
assert.equal(store.style, 'geometric');
const before = store.reactive;
api.react.fire('click');
assert.equal(store.reactive, !before, 'Reactive pill must toggle');
assert.ok(writes.some((w) => w[0] === 'reactive'));
});
test('exposes state and reasons to assistive tech', () => {
const ctl = load({ store: { style: 'image', reactive: true } }); // image: reactive inert
ctl.api._pcAcquire();
// The reason live-region must be a REAL mounted element with the id the
// controls reference - not a dangling pointer. Assert resolution, not a
// literal (a wrong id in code would still equal the literal).
const reason = ctl.api.reason;
assert.ok(reason, 'the reason span was not created');
assert.equal(reason.id, 'h3d-pc-reason');
assert.equal(reason.parentNode, ctl.api.el, 'the reason span must be mounted in the control');
// aria-pressed: a toggle button must expose its state. image greys
// Reactive, so not-pressed AND disabled, and it points at the reason.
assert.equal(ctl.api.react['aria-pressed'], 'false', 'greyed toggle is not pressed');
assert.equal(ctl.api.react['aria-disabled'], 'true');
// Pointer must resolve to the actual span's id (kills a wrong-id mutation),
// and the span must carry the current reason text (kills a never-set-text
// mutation).
assert.equal(ctl.api.react['aria-describedby'], reason.id, 'inert control must reference the reason span');
assert.equal(reason.textContent, 'This background does not react to audio', 'reason text must match the style');
assert.equal(ctl.api.intens['aria-describedby'], undefined, 'an ENABLED control carries no reason');
// The intensity describe path: a style where INTENSITY is inert.
ctl.store.style = 'video'; ctl.emit('style');
assert.equal(ctl.api.intens.disabled, true, 'precondition: video greys intensity');
assert.equal(ctl.api.intens['aria-describedby'], reason.id, 'inert intensity must reference the reason');
assert.equal(reason.textContent, 'The video plays as-is - nothing to adjust here');
// Both enabled: describedby drops, aria-pressed follows the value.
ctl.store.style = 'particles'; ctl.store.reactive = true; ctl.emit('style');
assert.equal(ctl.api.react['aria-describedby'], undefined, 'enabled control drops the reason');
assert.equal(ctl.api.intens['aria-describedby'], undefined);
assert.equal(ctl.api.react['aria-pressed'], 'true', 'reactive on for particles');
ctl.store.reactive = false; ctl.emit('reactive');
assert.equal(ctl.api.react['aria-pressed'], 'false', 'aria-pressed follows the value');
// Accessible names on the non-label controls.
assert.equal(ctl.api.sel['aria-label'], 'Background style');
assert.equal(ctl.api.intens['aria-label'], 'Background intensity');
ctl.api._pcRelease();
});
test('greys out exactly the controls each style ignores', () => {
const { api, store, emit } = load();
api._pcAcquire();
for (const [style, want] of Object.entries(EXPECTED_USES)) {
store.style = style;
emit('style');
assert.equal(!api.intens.disabled, want.intensity, `${style}: intensity enabled-ness`);
assert.equal(!api.react.disabled, want.reactive, `${style}: reactive enabled-ness`);
}
});
test('the Venue override greys the whole Background group', () => {
const ctl = load({ store: { style: 'particles' } }); // a style that uses both
ctl.api._pcAcquire();
assert.equal(ctl.api.intens.disabled, false, 'precondition: both enabled off-venue');
assert.equal(ctl.api.react.disabled, false);
// Venue turns on: the effective style is now 'venue', which uses neither.
// The transition arrives on the settings bus as the 'venueScene' key.
ctl.sandbox._venueSceneOverride = true;
ctl.emit('venueScene');
assert.equal(ctl.api.intens.disabled, true, 'intensity should grey under Venue');
assert.equal(ctl.api.react.disabled, true, 'reactive should grey under Venue');
assert.equal(ctl.api.sel.disabled, true, 'the dropdown should be inert under Venue too');
assert.match(ctl.api.intens.title, /venue/i, 'reason should mention Venue');
// All three inert controls point at the reason under Venue (kills a
// 'describe reactive only' regression on the select/intensity paths).
const vReason = ctl.api.reason.id;
assert.equal(ctl.api.sel['aria-describedby'], vReason, 'select must reference the reason under Venue');
assert.equal(ctl.api.intens['aria-describedby'], vReason, 'intensity must reference the reason under Venue');
assert.equal(ctl.api.react['aria-describedby'], vReason, 'reactive must reference the reason under Venue');
assert.match(ctl.api.reason.textContent, /venue/i, 'the reason span carries the Venue text');
// The dropdown still shows the stored style (venue has no option), but
// selecting must not write while it's inert.
assert.equal(ctl.api.sel.value, 'particles');
const before = ctl.writes.length;
ctl.api.sel.value = 'lights';
ctl.api.sel.fire('change');
assert.equal(ctl.writes.length, before, 'a disabled dropdown must not write');
// Venue off: controls come back per the stored style.
ctl.sandbox._venueSceneOverride = false;
ctl.emit('venueScene');
assert.equal(ctl.api.intens.disabled, false, 'intensity re-enables when Venue exits');
assert.equal(ctl.api.react.disabled, false);
assert.equal(ctl.api.sel.disabled, false, 'the dropdown re-enables when Venue exits');
assert.equal(ctl.api.sel.title, 'Background style', 'the base tooltip must come back, not blank');
assert.equal(ctl.api.sel['aria-describedby'], undefined, 'select drops the reason off-Venue');
assert.equal(ctl.api.intens['aria-describedby'], undefined, 'intensity drops the reason off-Venue');
ctl.api._pcRelease();
});
test('an unknown style enables both controls (fails open)', () => {
const { api, store, emit } = load();
api._pcAcquire();
store.style = 'some_future_style';
emit('style');
assert.equal(api.intens.disabled, false);
assert.equal(api.react.disabled, false);
});
test('greyed-out controls cannot reach the setters', () => {
const { api, store, emit, writes } = load();
api._pcAcquire();
store.style = 'video'; // uses neither setting
emit('style');
const before = writes.length;
api.intens.fire('change');
api.react.fire('click');
assert.equal(writes.length, before, 'an inert control must not write');
});
test('greyed-out controls explain themselves on hover', () => {
const { api, store, emit } = load();
api._pcAcquire();
store.style = 'butterchurn';
emit('style');
assert.match(api.react.title, /butterchurn/i);
assert.match(api.intens.title, /butterchurn/i);
});
// A native-disabled <button>/<input> fires no pointer events, so its own
// `title` never shows on hover. The reason must therefore also sit on the
// non-disabled wrapper, and the disabled control must let the hover fall
// through (pointer-events:none) — otherwise the "says why on hover" feature is
// dead in the browser while these tests pass on the swallowed control title.
test('the greyed-out reason reaches a hoverable wrapper', () => {
const { api, store, emit } = load();
api._pcAcquire();
store.style = 'video'; // uses neither setting
emit('style');
assert.match(api.react.parentNode.title, /nothing to adjust/i,
'reactive reason must be on the wrapper, not only the disabled pill');
assert.equal(api.react.style.pointerEvents, 'none',
'disabled pill must pass hover through to its wrapper');
assert.match(api.intens.parentNode.title, /nothing to adjust/i,
'intensity reason must be on the wrapper, not only the disabled slider');
assert.equal(api.intens.style.pointerEvents, 'none',
'disabled slider must pass hover through to its wrapper');
// ...and an enabled style clears the wrapper so the control's own title wins.
store.style = 'particles';
emit('style');
assert.equal(api.react.parentNode.title, '');
assert.equal(api.intens.parentNode.title, '');
assert.equal(api.intens.style.pointerEvents, '');
});