feedBack/tests/js/v3_keep_practicing.test.js
OmikronApex d2b2a7e9f7
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>
2026-07-03 14:35:07 +02:00

77 lines
3.6 KiB
JavaScript

// Pins the practice-aware library home in static/v3/songs.js:
// - a "Repertoire" progress meter (mastered / total library songs), and
// - a "Keep practicing" shelf (recently played, not yet mastered).
// Both reuse existing data (/api/stats/best already in state.accuracy, and
// /api/stats/recent) and are shown only on the unfiltered grid front door.
//
// Source-level only — same strategy as tests/js/v3_az_rail.test.js.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const SONGS_JS = path.join(__dirname, '..', '..', 'static', 'v3', 'songs.js');
const src = fs.readFileSync(SONGS_JS, 'utf8');
test('repertoire uses the same mastery threshold as the green accuracy badge', () => {
assert.match(src, /const\s+MASTERY_ACCURACY\s*=\s*0\.9/);
assert.match(
src,
/function\s+_repertoireCounts[\s\S]*?v\s*>=\s*MASTERY_ACCURACY\s*\)\s*mastered\+\+;\s*else\s+learning\+\+/,
'repertoire counts must bucket scored songs into mastered/learning at MASTERY_ACCURACY',
);
});
test('the home is the unfiltered grid front door, local provider only', () => {
assert.match(
src,
/function\s+libHomeVisible[\s\S]*?state\.view === 'grid'[\s\S]*?state\.provider === 'local'[\s\S]*?!state\.selectMode[\s\S]*?!state\.q[\s\S]*?activeFilterCount\(\)\s*===\s*0/,
'libHomeVisible must require grid view, the local provider, no select mode, no search, no active filters',
);
});
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,
/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',
);
});
test('the meter + shelf fetch together and a stale render is discarded', () => {
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');
});
test('the repertoire denominator is the unfiltered library total', () => {
assert.match(src, /\/api\/library\/stats\?provider='/);
assert.match(src, /total_songs\s*\?\?\s*stats\.total/);
assert.match(src, /Math\.round\(\(mastered\s*\/\s*total\)\s*\*\s*100\)/);
});
test('the home + #v3-lib-home host are wired into render and reload', () => {
assert.match(src, /id="v3-lib-home"/, 'render() must include the #v3-lib-home host');
assert.match(src, /function reload\s*\([\s\S]*?updateLibraryHome\(\)/,
'reload() must refresh/toggle the home');
assert.match(src, /function applyScoreRefresh[\s\S]*?renderLibraryHome\(\)/,
'a new score must refresh the meter + shelf');
});
test('shelf cards play the song on click', () => {
assert.match(
src,
/querySelectorAll\('\.v3-kp-card'\)[\s\S]*?window\.playSong\(enc\(fn\)/,
'a shelf card click must call window.playSong with the recents filename',
);
});