feedBack/plugins/drum_highway_3d/tests/data_layer.test.js
Byron Gamatos d409d55615
feat(drum_highway_3d): audio-reactive ambience + score FX overlay (D4) (#708)
- BG_STYLES port (off/particles/lights/geometric) into a renderOrder -1
  group; _bgGetAnalyser/_bgReadBands (stems-first, one-shot #audio
  fallback, permanent-failure latch, 5ms bands cache); Ambience
  intensity slider + Audio-reactive toggle; remounts on style/intensity/
  palette change and across kit-change scene rebuilds
- Score-FX overlay canvas (guitar drawScoreFx adapted to internal
  scoring): +1 pops at the struck lane, ring pulse every 10-combo,
  milestone bursts at 25/50/100, red wash on 3+ streak break; pooled,
  cleared when idle, removed in teardown
- butterchurn/image/video deliberately out of scope
- Tests: style id validation + FX defaults (15 total)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 09:01:56 +02:00

165 lines
7.5 KiB
JavaScript

// Pure data-layer tests: load screen.js in a bare vm window and exercise the
// __test exports (no DOM, no WebGL, no network). Doubles as a lint that no
// module-scope code touches document/localStorage outside a try/catch —
// the vm window deliberately provides neither.
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');
function load() {
const window = {
console,
location: { protocol: 'http:', host: 'localhost' },
slopsmith: {},
};
window.window = window;
window.globalThis = window;
const context = vm.createContext(window);
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
vm.runInContext(src, context, { filename: 'screen.js' });
return window.slopsmithViz_drum_highway_3d;
}
test('module loads in a bare vm (no DOM / localStorage at module scope)', () => {
const factory = load();
assert.equal(typeof factory, 'function');
assert.equal(factory.contextType, 'webgl2');
});
test('_variantForHit: ghost > flam > bell > accent > normal precedence', () => {
const { _variantForHit } = load().__test;
assert.equal(_variantForHit({ g: true, f: true, v: 120 }), 'ghost');
assert.equal(_variantForHit({ f: true, v: 120 }), 'flam');
assert.equal(_variantForHit({ p: 'ride_bell' }), 'bell');
assert.equal(_variantForHit({ v: 100 }), 'accent');
assert.equal(_variantForHit({ v: 127 }), 'accent');
assert.equal(_variantForHit({ v: 99 }), 'normal');
// Missing velocity defaults to 100 → accent.
assert.equal(_variantForHit({}), 'accent');
});
test('matchesArrangement: claims drum arrangements', () => {
const matches = load().matchesArrangement;
assert.equal(matches({ has_drum_tab: true, arrangement: 'Drums' }), true);
assert.equal(matches({ has_drum_tab: true, arrangement: 'Drum Kit' }), true);
assert.equal(matches({ has_drum_tab: true, arrangement: 'Percussion' }), true);
});
test('matchesArrangement: never claims without a drum tab', () => {
const matches = load().matchesArrangement;
assert.equal(matches(null), false);
assert.equal(matches({}), false);
assert.equal(matches({ arrangement: 'Drums' }), false);
});
test('matchesArrangement: steal-guard — guitar arrangements stay with highway_3d', () => {
const matches = load().matchesArrangement;
// Full-band pack (drum_tab present) playing a guitar-family part:
// first-match-wins Auto order must not hand these to the drum highway.
for (const arr of ['Lead', 'Rhythm', 'Bass', 'Combo', 'Guitar 22', 'Alt. Lead']) {
assert.equal(matches({ has_drum_tab: true, arrangement: arr }), false, arr);
}
// Keys notation present → the keys/staff viz take it.
assert.equal(matches({ has_drum_tab: true, has_notation: true, arrangement: 'Piano' }), false);
});
test('matchesArrangement: claims packs nothing more specific can render', () => {
const matches = load().matchesArrangement;
// Drum tab + nondescript arrangement, no notation → drummable, claim it.
assert.equal(matches({ has_drum_tab: true, arrangement: '' }), true);
assert.equal(matches({ has_drum_tab: true }), true);
// Word-boundary check: "BasslineKeys"-style names don't contain a
// guitar-family word as a whole word.
assert.equal(matches({ has_drum_tab: true, arrangement: 'Bassline' }), true);
});
test('readFxSettings: defaults survive a localStorage-less environment', () => {
const { readFxSettings, FX_DEFAULTS } = load().__test;
// The vm window has no localStorage — the try/catch must eat the
// ReferenceError and hand back pure defaults (everything ON).
assert.deepEqual(readFxSettings(), FX_DEFAULTS);
assert.equal(FX_DEFAULTS.bloom, true);
});
test('MIDI map: open hi-hat is a first-class piece (46 → hh_open)', () => {
const { MIDI_TO_PIECE, HIT_TOLERANCE_S } = load().__test;
assert.equal(MIDI_TO_PIECE[46], 'hh_open');
assert.equal(MIDI_TO_PIECE[42], 'hh_closed');
assert.equal(MIDI_TO_PIECE[35], 'kick');
assert.equal(MIDI_TO_PIECE[36], 'kick');
// ±50 ms window matches the 2D drums plugin.
assert.equal(HIT_TOLERANCE_S, 0.05);
});
test('_classifyTiming: OK band is 40% of the window, sign maps early/late', () => {
const { _classifyTiming, HIT_TOLERANCE_S } = load().__test;
const tol = HIT_TOLERANCE_S; // 0.05
assert.equal(_classifyTiming(0, tol), 'OK');
assert.equal(_classifyTiming(tol * 0.4, tol), 'OK'); // boundary inclusive
assert.equal(_classifyTiming(-tol * 0.4, tol), 'OK');
// delta = note.t - now: positive → struck before the note → EARLY.
assert.equal(_classifyTiming(tol * 0.41, tol), 'EARLY');
assert.equal(_classifyTiming(-tol * 0.41, tol), 'LATE');
assert.equal(_classifyTiming(tol, tol), 'EARLY');
assert.equal(_classifyTiming(-tol, tol), 'LATE');
// Degenerate inputs read as on-time rather than throwing.
assert.equal(_classifyTiming(NaN, tol), 'OK');
});
test('FX defaults: hit-FX controls ship enabled', () => {
const { FX_DEFAULTS } = load().__test;
assert.equal(FX_DEFAULTS.sparks, true);
assert.equal(FX_DEFAULTS.timingFx, true);
assert.equal(FX_DEFAULTS.streakFx, true);
assert.equal(FX_DEFAULTS.hitFx, 0.7);
});
test('themes: table ids match the guitar highway, default is the stock palette', () => {
const { BG_THEMES, _bgThemeColors } = load().__test;
// Same id set as highway_3d's BG_THEMES (cross-instrument consistency).
assert.deepEqual(Object.keys(BG_THEMES), [
'default', 'midnight', 'charcoal', 'deeppurple', 'forest', 'warmslate',
'deepfocus', 'deepsea', 'cathode', 'cathodegreen', 'hearth',
]);
// 'default' preserves THIS plugin's original look byte-for-byte.
assert.equal(BG_THEMES.default.clear, 0x1a1a2e);
assert.equal(BG_THEMES.default.board, 0x0a0e1a);
assert.equal(BG_THEMES.default.lane, undefined); // stock stripes fallback
// Unknown ids fall back to default.
assert.equal(_bgThemeColors('nonsense'), BG_THEMES.default);
// Every non-default theme carries a lane pair (the axis differentiator).
for (const [id, t] of Object.entries(BG_THEMES)) {
if (id === 'default') continue;
assert.ok(t.lane != null && t.laneDim != null, id + ' lane pair');
assert.equal(t.clear, t.fog, id + ' clear==fog (horizon dissolve)');
}
});
test('readThemeSetting: defaults without localStorage; validates ids', () => {
const { readThemeSetting } = load().__test;
assert.equal(readThemeSetting(), 'default');
});
test('FX defaults: theme-PR controls ship enabled at stock-neutral values', () => {
const { FX_DEFAULTS } = load().__test;
assert.equal(FX_DEFAULTS.cinematic, true);
assert.equal(FX_DEFAULTS.glow, 0.5); // 0.5 = 1.0x multiplier (stock)
assert.equal(FX_DEFAULTS.vibrancy, 0.85); // ≈ the stock 0.32 stripe base
});
test('bg styles: validated id set, particles default, no out-of-scope styles', () => {
const { BG_STYLE_IDS, readBgStyleSetting } = load().__test;
// Host-realm copy — the vm array's foreign prototype trips deepEqual.
assert.deepEqual([...BG_STYLE_IDS], ['off', 'particles', 'lights', 'geometric']);
assert.equal(readBgStyleSetting(), 'particles'); // no localStorage in the vm
});
test('FX defaults: ambience + score FX ship enabled', () => {
const { FX_DEFAULTS } = load().__test;
assert.equal(FX_DEFAULTS.scoreFx, true);
assert.equal(FX_DEFAULTS.bgIntensity, 0.5);
assert.equal(FX_DEFAULTS.bgReactive, true);
});