Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -2101,4 +2101,14 @@ window.slopsmithViz_drums = createFactory;
// slopsmith→feedBack rename: host viz picker looks up `window.feedBackViz_<id>`.
window.feedBackViz_drums = window.slopsmithViz_drums;

// Node-only export hook for tests; browsers keep the window.*Viz_drums wiring.
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
noteToMidi, _rgbStr, _validateCustomMapping, _drumTabHitsToNotes,
_applyLanePreset, _getActiveDrumMap, _midiToLaneIdx, _songNoteToLaneIdx,
_midiResolveSaved, DRUM_LANES, PIECE_DEFAULT_MIDI,
matchesArrangement: createFactory.matchesArrangement,
};
}

})();
140 changes: 140 additions & 0 deletions tests/screen.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use strict';
// Coverage for pure helpers in screen.js: MIDI mapping, lane presets,
// drum-tab hit conversion, custom-mapping validation, arrangement matching.
// Runs under the org reusable CI as `node tests/screen.test.js`.
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');

function freshPlugin() {
global.window = {};
global.localStorage = { getItem: () => null, setItem: () => {} };
global.document = { addEventListener: () => {} };
const file = path.join(__dirname, '..', 'screen.js');
delete require.cache[require.resolve(file)];
return require(file);
}

test('noteToMidi maps string/fret to a MIDI number (24 semitones per string)', () => {
const mod = freshPlugin();
assert.equal(mod.noteToMidi(0, 0), 0);
assert.equal(mod.noteToMidi(1, 12), 36);
});

test('_rgbStr formats rgb()/rgba() from 0..1 channel floats', () => {
const mod = freshPlugin();
assert.equal(mod._rgbStr(1, 0, 0), 'rgb(255,0,0)');
assert.equal(mod._rgbStr(1, 0, 0, 0.5), 'rgba(255,0,0,0.5)');
});

test('_validateCustomMapping keeps only in-range MIDI keys with known lane ids', () => {
const mod = freshPlugin();
const clean = mod._validateCustomMapping({ '38': 'snare', '200': 'kick', '40': 'nope-lane' });
assert.deepEqual({ ...clean }, { 38: 'snare' }); // clean has a null prototype
});

test('_validateCustomMapping rejects non-objects, arrays, and prototype-poisoning keys', () => {
const mod = freshPlugin();
assert.equal(mod._validateCustomMapping(null), null);
assert.equal(mod._validateCustomMapping('nope'), null);
assert.equal(mod._validateCustomMapping([1, 2]), null);
// Empty after filtering -> null, not {}.
assert.equal(mod._validateCustomMapping({ '__proto__': 'kick' }), null);
assert.equal(mod._validateCustomMapping({}), null);
});

test('_drumTabHitsToNotes converts known piece-ids to {t,s,f} and sorts by time', () => {
const mod = freshPlugin();
const notes = mod._drumTabHitsToNotes([
{ p: 'snare', t: 1.0, v: 120 },
{ p: 'kick', t: 0.5, v: 80 },
]);
assert.equal(notes.length, 2);
assert.equal(notes[0].t, 0.5);
assert.equal(notes[0]._piece, 'kick');
assert.equal(notes[0].ac, false); // v=80 < 100
assert.equal(notes[1].ac, true); // v=120 >= 100
});

test('_drumTabHitsToNotes drops unknown piece-ids and non-finite/negative timestamps', () => {
const mod = freshPlugin();
const notes = mod._drumTabHitsToNotes([
{ p: 'cowbell', t: 1.0 }, // unknown piece
{ p: 'snare', t: -1 }, // negative time
{ p: 'snare', t: NaN }, // non-finite
{ p: 'snare', t: 2.0 }, // kept
]);
assert.equal(notes.length, 1);
assert.equal(notes[0].t, 2.0);
});

test('_drumTabHitsToNotes emits a leading grace note for flams', () => {
const mod = freshPlugin();
const notes = mod._drumTabHitsToNotes([{ p: 'snare', t: 1.0, v: 100, f: true }]);
assert.equal(notes.length, 2);
const [grace, main] = notes;
assert.equal(grace._noScore, true);
assert.ok(grace.t < main.t);
assert.equal(main.t, 1.0);
});

test('_drumTabHitsToNotes ignores a non-array payload', () => {
const mod = freshPlugin();
assert.deepEqual(mod._drumTabHitsToNotes(null), []);
assert.deepEqual(mod._drumTabHitsToNotes('nope'), []);
});

test('lane preset switch rebuilds DRUM_LANES and the default map', () => {
const mod = freshPlugin();
mod._applyLanePreset('rb4');
assert.deepEqual(mod.DRUM_LANES.map(l => l.id), ['hihat', 'snare', 'tom1', 'tom3', 'crash', 'ride', 'kick']);
// In rb4, mid-tom notes 45/47 fold into tom1 (no separate tom2 lane).
assert.equal(mod._midiToLaneIdx(45), mod.DRUM_LANES.findIndex(l => l.id === 'tom1'));

mod._applyLanePreset('phase_shift_8');
assert.deepEqual(mod.DRUM_LANES.map(l => l.id),
['hihat', 'snare', 'tom1', 'tom2', 'tom3', 'crash', 'ride', 'kick']);
});

test('_applyLanePreset falls back to phase_shift_8 for an unknown preset name', () => {
const mod = freshPlugin();
mod._applyLanePreset('not-a-real-preset');
assert.deepEqual(mod.DRUM_LANES.map(l => l.id),
['hihat', 'snare', 'tom1', 'tom2', 'tom3', 'crash', 'ride', 'kick']);
});

test('_midiToLaneIdx/_songNoteToLaneIdx resolve unmapped notes to -1', () => {
const mod = freshPlugin();
assert.equal(mod._midiToLaneIdx(999), -1);
assert.equal(mod._songNoteToLaneIdx(999), -1);
});

test('_midiResolveSaved matches by stored key first, falls back to legacy bare id', () => {
const mod = freshPlugin();
const sources = [{ id: 'dev1', key: 'webmidi:dev1' }];
assert.equal(mod._midiResolveSaved('webmidi:dev1', sources), 'webmidi:dev1');
assert.equal(mod._midiResolveSaved('dev1', sources), 'webmidi:dev1');
assert.equal(mod._midiResolveSaved('nope', sources), null);
});

test('matchesArrangement trusts has_drum_tab regardless of arrangement name', () => {
const mod = freshPlugin();
assert.equal(mod.matchesArrangement({ has_drum_tab: true, arrangement: 'Lead' }), true);
});

test('matchesArrangement matches drum-pattern arrangement names', () => {
const mod = freshPlugin();
assert.equal(mod.matchesArrangement({ arrangement: 'Drums' }), true);
assert.equal(mod.matchesArrangement({ arrangement: 'Lead Guitar' }), false);
});

test('matchesArrangement rejects falsy songInfo', () => {
const mod = freshPlugin();
assert.equal(mod.matchesArrangement(null), false);
});

test('PIECE_DEFAULT_MIDI covers the canonical piece set used by _drumTabHitsToNotes', () => {
const mod = freshPlugin();
assert.equal(mod.PIECE_DEFAULT_MIDI.kick, 36);
assert.equal(mod.PIECE_DEFAULT_MIDI.snare, 38);
});
Loading