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
11 changes: 11 additions & 0 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -4041,4 +4041,15 @@ try {
// so all `let` bindings the function touches (e.g. _followerAudio) are
// past their temporal dead zone by the time the function executes.
if (FOLLOWER) bootFollowerMode();

// Node-only export hook for tests. Placed last so every `let`/`const`
// binding referenced by the exported functions has already run its
// initializer; does not affect the browser (module is undefined there).
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
getWsUrl, resolveArrIndex, getDefaultArrangements,
panelToPrefs, migratePanelPrefs, _ctlRange,
_setArrangementsForTest(next) { arrangements = next; },
};
}
})();
161 changes: 161 additions & 0 deletions tests/screen.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use strict';
// Coverage for pure helpers in screen.js: WS URL building, arrangement
// resolution/defaulting, panel-prefs snapshot/migration, range clamping.
// 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 makeLocalStorage() {
const store = new Map();
return {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
}

function freshPlugin({ search = '', protocol = 'http:' } = {}) {
const location = { search, host: 'localhost:8420', protocol };
global.window = { location, addEventListener: () => {} };
global.document = {
getElementById: () => null,
addEventListener: () => {},
body: { appendChild: () => {} },
createElement: () => ({
style: {}, classList: { add() {}, remove() {} },
addEventListener() {}, appendChild() {}, setAttribute() {},
}),
readyState: 'loading',
};
global.localStorage = makeLocalStorage();
global.location = location;
const file = path.join(__dirname, '..', 'screen.js');
delete require.cache[require.resolve(file)];
return require(file);
}

test('getWsUrl decodes percent-encoded filenames and builds a ws:// URL', () => {
const { getWsUrl } = freshPlugin();
assert.equal(
getWsUrl('sloppak%2Fperfouts.sloppak', 0),
'ws://localhost:8420/ws/highway/sloppak/perfouts.sloppak?arrangement=0');
});

test('getWsUrl omits the arrangement param when undefined', () => {
const { getWsUrl } = freshPlugin();
assert.equal(getWsUrl('song.sloppak'), 'ws://localhost:8420/ws/highway/song.sloppak');
});

test('getWsUrl uses wss:// under https', () => {
const { getWsUrl } = freshPlugin({ protocol: 'https:' });
assert.match(getWsUrl('a.sloppak', 1), /^wss:\/\//);
});

test('resolveArrIndex is case-insensitive and finds by name', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Lead' }, { name: 'Rhythm' }, { name: 'Bass' }]);
assert.equal(mod.resolveArrIndex('rhythm'), 1);
assert.equal(mod.resolveArrIndex('BASS'), 2);
assert.equal(mod.resolveArrIndex('nope'), -1);
});

test('resolveArrIndex treats special modes as non-arrangements', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Lead' }]);
assert.equal(mod.resolveArrIndex(''), -1);
assert.equal(mod.resolveArrIndex(null), -1);
assert.equal(mod.resolveArrIndex('__lyrics__'), -1); // LYRICS_VALUE sentinel used at runtime; harmless if it isn't this literal
});

test('getDefaultArrangements prioritizes lead/rhythm/bass then fills and wraps', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([
{ name: 'Bass Guitar' }, { name: 'Rhythm Guitar' }, { name: 'Lead Guitar' }, { name: 'Extra' },
]);
// indices: 0=bass, 1=rhythm, 2=lead, 3=extra
assert.deepEqual(mod.getDefaultArrangements(2), [2, 1]); // lead, rhythm
assert.deepEqual(mod.getDefaultArrangements(4), [2, 1, 0, 3]); // lead, rhythm, bass, extra
assert.deepEqual(mod.getDefaultArrangements(5), [2, 1, 0, 3, 2]); // wraps around
});

test('getDefaultArrangements falls back to arrangement order with no named matches', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Arr A' }, { name: 'Arr B' }]);
assert.deepEqual(mod.getDefaultArrangements(2), [0, 1]);
});

test('_ctlRange fills defaults for missing/non-finite bounds', () => {
const { _ctlRange } = freshPlugin();
assert.deepEqual(_ctlRange({}), { lo: 0, hi: 1, st: 0.05 });
assert.deepEqual(_ctlRange({ min: -1, max: 2, step: 0.1 }), { lo: -1, hi: 2, st: 0.1 });
assert.deepEqual(_ctlRange({ min: NaN, max: Infinity }), { lo: 0, hi: 1, st: 0.05 });
});

test('panelToPrefs encodes a plain arrangement panel', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Lead' }]);
const panel = {
arrIndex: 0,
hw: { getInverted: () => false, getLefty: () => false, getMastery: () => 0.5 },
bar: { style: { display: '' } },
};
const prefs = mod.panelToPrefs(panel);
assert.equal(prefs.arrName, 'Lead');
assert.equal(prefs.inverted, false);
assert.equal(prefs.mastery, 0.5);
assert.equal(prefs.barHidden, false);
});

test('panelToPrefs encodes lyrics mode', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Lead' }]);
const panel = {
arrIndex: 0, lyricsMode: true, lyricsOverlayOn: true,
hw: { getInverted: () => true, getLefty: () => true, getMastery: () => 0 },
bar: { style: { display: 'none' } },
};
const prefs = mod.panelToPrefs(panel);
assert.equal(prefs.arrName, '__lyrics__'); // LYRICS_VALUE sentinel captured verbatim
assert.equal(prefs.lyrics, true);
assert.equal(prefs.barHidden, true);
});

test('panelToPrefs encodes viz mode with the underlying arrangement name', () => {
const mod = freshPlugin();
mod._setArrangementsForTest([{ name: 'Lead' }, { name: 'Bass' }]);
const panel = {
arrIndex: 1, vizMode: 'highway_3d',
hw: { getInverted: () => false, getLefty: () => false, getMastery: () => 0 },
bar: { style: { display: '' } },
};
const prefs = mod.panelToPrefs(panel);
assert.equal(prefs.arrName, '__viz__:highway_3d:Bass');
});

test('migratePanelPrefs passes through non-array input unchanged', () => {
const { migratePanelPrefs } = freshPlugin();
assert.equal(migratePanelPrefs(null), null);
assert.equal(migratePanelPrefs('nope'), 'nope');
});

test('migratePanelPrefs resets lyrics on first migration (v<2) and preserves other fields', () => {
const { migratePanelPrefs } = freshPlugin();
const out = migratePanelPrefs([{ arrName: 'Lead', lyrics: true, inverted: true }]);
assert.equal(out[0].lyrics, false);
assert.equal(out[0].inverted, true);
});

test('migratePanelPrefs migrates the legacy 3D-highway sentinel prefix', () => {
const { migratePanelPrefs } = freshPlugin();
const out = migratePanelPrefs([{ arrName: '__3d_highway__:Lead', lyrics: false }]);
assert.equal(out[0].arrName, '__viz__:highway_3d:Lead');
});

test('migratePanelPrefs does not reset lyrics once already migrated to v2', () => {
global.location = { search: '', host: 'x', protocol: 'http:' };
const mod = freshPlugin();
global.localStorage.setItem('splitscreenPrefsMigrationV', '2');
const out = mod.migratePanelPrefs([{ arrName: 'Lead', lyrics: true }]);
assert.equal(out[0].lyrics, true);
});
Loading