From d2b2a7e9f7ec01135e365396188661eb29d3ffa1 Mon Sep 17 00:00:00 2001 From: OmikronApex <45161725+OmikronApex@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:35:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(tests):=20re-green=20the=20JS=20suite=20?= =?UTF-8?q?=E2=80=94=2018=20stale=20source-shape=20tests=20+=201=20real=20?= =?UTF-8?q?seek-reason=20violation=20(#740)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- static/app.js | 2 +- tests/js/highway_colors_facade.test.js | 18 +++++++++++++----- tests/js/legacy_shim_hits.test.js | 5 ++++- tests/js/live_guitar_tone_source.test.js | 4 +++- tests/js/loop_api.test.js | 1 + tests/js/song_close.test.js | 4 ++++ tests/js/v3_keep_practicing.test.js | 20 +++++++++++--------- tests/js/v3_songs_tuning.test.js | 4 +++- 8 files changed, 40 insertions(+), 18 deletions(-) diff --git a/static/app.js b/static/app.js index 0a46264..7bdcf01 100644 --- a/static/app.js +++ b/static/app.js @@ -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)); }); diff --git a/tests/js/highway_colors_facade.test.js b/tests/js/highway_colors_facade.test.js index 949b872..80a7fc5 100644 --- a/tests/js/highway_colors_facade.test.js +++ b/tests/js/highway_colors_facade.test.js @@ -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', () => { diff --git a/tests/js/legacy_shim_hits.test.js b/tests/js/legacy_shim_hits.test.js index aaea7ed..b5186da 100644 --- a/tests/js/legacy_shim_hits.test.js +++ b/tests/js/legacy_shim_hits.test.js @@ -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) { diff --git a/tests/js/live_guitar_tone_source.test.js b/tests/js/live_guitar_tone_source.test.js index 9fcb24a..ef637ce 100644 --- a/tests/js/live_guitar_tone_source.test.js +++ b/tests/js/live_guitar_tone_source.test.js @@ -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’t warn that no internal amp tone is loaded/); + // Apostrophe form drifted from the ’ entity to the literal ’ in a + // copy pass — accept entity, typographic, or plain apostrophe. + assert.match(html, /won(?:’|’|')t warn that no internal amp tone is loaded/); }); test('player audio rail exposes tone source select', () => { diff --git a/tests/js/loop_api.test.js b/tests/js/loop_api.test.js index 777cf11..5548ff5 100644 --- a/tests/js/loop_api.test.js +++ b/tests/js/loop_api.test.js @@ -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(')} diff --git a/tests/js/song_close.test.js b/tests/js/song_close.test.js index 1f09303..714830c 100644 --- a/tests/js/song_close.test.js +++ b/tests/js/song_close.test.js @@ -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); diff --git a/tests/js/v3_keep_practicing.test.js b/tests/js/v3_keep_practicing.test.js index 9e395cc..f4de396 100644 --- a/tests/js/v3_keep_practicing.test.js +++ b/tests/js/v3_keep_practicing.test.js @@ -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'); diff --git a/tests/js/v3_songs_tuning.test.js b/tests/js/v3_songs_tuning.test.js index 13352b4..2d39860 100644 --- a/tests/js/v3_songs_tuning.test.js +++ b/tests/js/v3_songs_tuning.test.js @@ -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/); });