fix(tests): re-green the JS suite — 18 stale source-shape tests + 1 real seek-reason violation (#740)

main's JS suite has been red since the recent v3-library and player
refactors landed. 17 of 18 failures were test harnesses/regexes that
went stale behind real, intentional code changes; one was a genuine
contract violation in the code.

Code fix:
- session-resume seek passed 'resume' as its _audioSeek reason; the
  documented contract (enforced by song_seek.test.js) requires
  multi-word kebab-case. Renamed to 'session-resume' — no consumer
  string-matches specific reasons, so this is rename-safe.

Test updates (each pins the CURRENT contract):
- highway_colors_facade: inject HWC_PRESETS + applyHighwayStringPreset
  (new preset feature); lock presets/applyPreset into the surface test
- loop_api: stub _updateEditRegionBtn (new edit-region UI hook)
- song_close: sandbox gets window.feedBack.playQueue; assert a real
  close abandons the queue (the new queue-aware behavior)
- v3_keep_practicing: the shelf moved from client-side /api/stats/recent
  dedupe+gating to the server-side practice-suggestions recommender —
  tests now pin that (fetch, arrangement-aware card click, Promise.all)
- v3_songs_tuning: card row variable renamed song → shown (grouped cards)
- live_guitar_tone_source: accept literal ’ where ’ drifted in copy
- legacy_shim_hits: normalize CRLF before fixed-width region() slicing
  (Windows-only failure; char windows shrank by one char per line)

Suite: 987/987 locally (Windows), previously 968/987 (and 18 red on CI).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex 2026-07-03 14:35:07 +02:00 committed by GitHub
parent b6442dda75
commit d2b2a7e9f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 40 additions and 18 deletions

View File

@ -6195,7 +6195,7 @@ window.feedBack.on('song:ready', () => {
setSpeed(pend.speed);
}
} catch (_) { /* speed restore is best-effort */ }
Promise.resolve(_audioSeek(Math.max(0, Number(pend.position) || 0), 'resume'))
Promise.resolve(_audioSeek(Math.max(0, Number(pend.position) || 0), 'session-resume'))
.then(() => { if (_autoplayExitEnabled() && !isPlaying) return togglePlay(); })
.catch((err) => console.warn('[app] resume failed:', err));
});

View File

@ -35,10 +35,11 @@ function buildFacade() {
'return _hwcInstallFacade;',
].join('\n');
const params = [
'window', 'HWC_SLOTS', 'console',
'window', 'HWC_SLOTS', 'HWC_PRESETS', 'console',
'getHighwayStringColors', 'getHighwayDefaultSlotColors', '_hwcMergedSlotColors',
'_hwcSlotKeysForChart', '_hwcEffectiveIndexColors', '_hwcChartShape',
'applyHighwayStringColors', 'encodeHighwayColorShare', 'decodeHighwayColorShare',
'applyHighwayStringColors', 'applyHighwayStringPreset',
'encodeHighwayColorShare', 'decodeHighwayColorShare',
];
const listeners = {};
@ -64,14 +65,19 @@ function buildFacade() {
_hwcEffectiveIndexColors: (map, sc, isBass) => ['eff', sc, isBass],
_hwcChartShape: () => ({ sc: 6, isBass: false }),
applyHighwayStringColors: (m) => { calls.push(['apply', m]); },
applyHighwayStringPreset: (id) => { calls.push(['preset', id]); return true; },
encodeHighwayColorShare: (n, m) => 'SLOPHWY2.CODE',
decodeHighwayColorShare: (c) => ({ name: 'x', colors: {} }),
};
const HWC_PRESETS = [
{ id: 'stock', label: 'Stock', colors: { lowE: '#cc0000' } },
];
const installer = new Function(...params, body)(
win, HWC_SLOTS, console,
win, HWC_SLOTS, HWC_PRESETS, console,
stubs.getHighwayStringColors, stubs.getHighwayDefaultSlotColors, stubs._hwcMergedSlotColors,
stubs._hwcSlotKeysForChart, stubs._hwcEffectiveIndexColors, stubs._hwcChartShape,
stubs.applyHighwayStringColors, stubs.encodeHighwayColorShare, stubs.decodeHighwayColorShare,
stubs.applyHighwayStringColors, stubs.applyHighwayStringPreset,
stubs.encodeHighwayColorShare, stubs.decodeHighwayColorShare,
);
installer();
return { api: win.feedBack.highwayColors, win, bus, calls, installer, stubs };
@ -87,11 +93,13 @@ test('facade exposes the documented surface', () => {
const { api } = buildFacade();
assert.equal(api.version, 1);
for (const m of ['get', 'getDefaults', 'getResolved', 'keysForChart', 'toEffective',
'getCurrent', 'apply', 'encodeShare', 'decodeShare', 'onChange', 'offChange']) {
'getCurrent', 'apply', 'applyPreset', 'encodeShare', 'decodeShare', 'onChange', 'offChange']) {
assert.equal(typeof api[m], 'function', `highwayColors.${m} must be a function`);
}
assert.deepEqual(api.slots.map((s) => s.key),
['highE', 'B', 'G', 'D', 'A', 'lowE', 'low7', 'low8'], 'slots in display order');
// One-click presets: exposed as detached [{ id, label, colors }] copies.
assert.deepEqual(api.presets, [{ id: 'stock', label: 'Stock', colors: { lowE: '#cc0000' } }]);
});
test('facade read methods delegate to the manager', () => {

View File

@ -74,7 +74,10 @@ const APP_JS = path.join(ROOT, 'static', 'app.js');
const LIBRARY_JS = path.join(ROOT, 'static', 'capabilities', 'library.js');
function source(file) {
return fs.readFileSync(file, 'utf8');
// Normalize CRLF: region() slices fixed CHARACTER windows, so on a
// Windows checkout (autocrlf) every line costs one extra char and the
// assertion target can fall outside the window.
return fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
}
function region(src, needle, length = 1200) {

View File

@ -40,7 +40,9 @@ test('settings UI exposes tone source select with all options', () => {
assert.match(html, /value="external_hardware"/);
assert.match(html, /value="spark_control_x"/);
assert.match(html, /Live guitar tone source/);
assert.match(html, /won&rsquo;t warn that no internal amp tone is loaded/);
// Apostrophe form drifted from the &rsquo; entity to the literal in a
// copy pass — accept entity, typographic, or plain apostrophe.
assert.match(html, /won(?:&rsquo;||')t warn that no internal amp tone is loaded/);
});
test('player audio rail exposes tone source select', () => {

View File

@ -107,6 +107,7 @@ function loadFunctions(sandbox, src) {
sectionPracticeModeCalls.push({ on, opts: opts || {} });
}
function _updateSectionPracticeHighlight(ct) {}
function _updateEditRegionBtn() {}
${extractFunction(src, 'function clearLoop(')}
${extractFunction(src, 'function _syncSavedLoopSelection()')}
${extractFunction(src, 'async function setLoop(')}

View File

@ -42,7 +42,10 @@ function loadClose(sandbox, src) {
globalThis.__seekCalls = 0;
globalThis.__playSongCalls = 0;
globalThis.__clearLoopCalls = 0;
globalThis.__queueClearCalls = 0;
globalThis.__audioCurrentTimeSets = [];
// closeCurrentSong abandons any play-queue before leaving the player.
var window = { feedBack: { playQueue: { clear() { globalThis.__queueClearCalls++; } } } };
var audio = {
_t: 42,
get currentTime() { return this._t; },
@ -75,6 +78,7 @@ test('closeCurrentSong uses _playerOriginScreen when set', async () => {
await sandbox.__closeCurrentSong();
assert.equal(sandbox.__showScreenCalls.length, 1);
assert.equal(sandbox.__showScreenCalls[0], 'favorites');
assert.equal(sandbox.__queueClearCalls, 1, 'a real close abandons the play-queue');
assert.equal(sandbox.__restartCalls, 0);
assert.equal(sandbox.__seekCalls, 0);
assert.equal(sandbox.__playSongCalls, 0);

View File

@ -31,21 +31,23 @@ test('the home is the unfiltered grid front door, local provider only', () => {
);
});
test('the shelf is recently-played, not-yet-mastered songs (per-song, deduped)', () => {
assert.match(src, /\/api\/stats\/recent\?limit=/);
// Mastery is gated on the per-SONG best (state.accuracy, what the badge
// shows), not the per-arrangement recents row, and each filename appears
// once — so no green-badged "keep practicing" card and no duplicates.
test('the shelf is the server-side practice-suggestions recommender', () => {
// The old client-side pipeline (fetch /api/stats/recent, dedupe by
// filename, gate on state.accuracy) moved server-side: the growth-edge
// recommender gates (not-mastered) + aggregates per song and picks the
// arrangement closest to mastery. The client renders its rows as-is.
assert.match(src, /\/api\/library\/practice-suggestions\?limit=/);
// A shelf card click opens the row's recommended arrangement, not the
// song's default.
assert.match(
src,
/const\s+best\s*=\s*acc\[r\.filename\][\s\S]*?best\s*>=\s*MASTERY_ACCURACY/,
'the shelf must gate on the per-song best (state.accuracy) at MASTERY_ACCURACY',
/data-arr="[\s\S]*?getAttribute\('data-arr'\)[\s\S]*?playSong\(enc\(fn\), arr === '' \? undefined : Number\(arr\)\)/,
'shelf cards must pass the recommended arrangement to playSong',
);
assert.match(src, /seen\.has\(r\.filename\)/, 'the shelf must dedupe recents by filename');
});
test('the meter + shelf fetch together and a stale render is discarded', () => {
assert.match(src, /Promise\.all\(\[[\s\S]*?library\/stats[\s\S]*?stats\/recent/,
assert.match(src, /Promise\.all\(\[[\s\S]*?library\/stats[\s\S]*?practice-suggestions/,
'the two reads must be issued together (Promise.all), not sequentially');
assert.match(src, /_homeToken[\s\S]*?_homeToken !== myToken/,
'a stale render must be superseded by a newer one via a token');

View File

@ -64,7 +64,9 @@ const helpers = loadTuningHelpers();
test('v3 songs.js uses display helpers for album-art tuning badge', () => {
const src = fs.readFileSync(SONGS_JS, 'utf8');
assert.match(src, /displayTuningName\(song\.tuning_name \|\| song\.tuning\)/);
// The card renderer's row variable was renamed song → shown when grouped
// cards landed (the badge reads the representative chart); accept either.
assert.match(src, /displayTuningName\((?:song|shown)\.tuning_name \|\| (?:song|shown)\.tuning\)/);
assert.match(src, /displayTuningTargets/);
assert.match(src, /parseRawTuningOffsets/);
});