mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-14 12:47:10 +00:00
refactor(app): carve count-in (and the song-credits overlay) out of app.js (R3a) (#890)
static/js/count-in.js (389) — bodies VERBATIM. app.js 8,223 → 7,913. The third slice out of the strongly-connected core, and the first that WRITES shared state rather than only reading it. #889's container is what makes it possible. imports: loops (setLoop/loopA/loopB — a count-in inside an A-B loop must begin at A), audio-el, player-state, host hooks : _audioSeek, setPlayButtonState, _songEventPayload, togglePlay + a jucePlayer getter Nothing imports count-in back — app.js and section-practice both reach it through the seam — so the graph stays acyclic. app.js's autoplay path used to reach IN and set this module's credits timers itself (_creditsTimer, _creditsHideOnPlay) and read _countingIn. It cannot now, and should not have to, so the module exports the OPERATIONS instead — armCreditsHideOnPlay(), scheduleCreditsHide(), holdCreditsThen(start), isCountingIn() — and owns its own timer invariants. Third time this has happened (section-practice's resetSelection, loops' state) and each time the constraint produced better code than was there before: the module keeps its own promises instead of trusting a caller 6,000 lines away to zero the right fields. THE no-undef GATE FOUND FIVE MISSED MEMBERS, one at a time: showSongCreditsOverlay and startSongCountIn (my name regex matched startCountIn, not startSongCountIn), then _creditLineLabel, _CREDITS_MAX_MS, and _CREDIT_ROLE_VERBS. A call-graph closure does not see a const table; only the undefined-symbol pass does. AND A REAL TRAP: I computed _CREDIT_ROLE_VERBS's span against the ALREADY-MODIFIED app.js and applied it to the clean one — the line numbers had drifted, so the slice would have cut somewhere else entirely. Recomputed every span from the clean file with acorn. Never carry line numbers across an edit. VERIFIED. A/B against origin/main in two browsers with a real song: playback state, the public feedBack.isPlaying mirror, audio position, cancel-count-in — IDENTICAL, zero page errors. Unit coverage moved with the code: loop_restart's count-in cancellation-token test and the 5 song_credits_overlay tests now read count-in.js; loop_restart's sandbox gains a `host` object routed at its EXISTING stubs, so every assertion is unchanged. HONEST LIMIT: I could not make the count-in OVERLAY actually render headlessly — its autoplay path needs a fresh-load _pendingAutostart that a scripted playSong() never arms. Behaviour is identical to main on every probe and the unit tests cover the logic, but the on-screen 1-2-3-4 and the credits card want a human look. pytest 2396, node 1040/1040, ESLint 0 (no-cycle clean), tailwind clean, Codex 0. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5fb28d5c5a
commit
11f8c36b61
@@ -14,7 +14,8 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
// startCountIn was carved out of app.js into its own module (R3a).
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'count-in.js');
|
||||
|
||||
// Pull a function body by declaration prefix (e.g. `async function startCountIn`)
|
||||
// and brace-matching to the closing brace. Skips an optional `( ... )` param
|
||||
@@ -111,12 +112,23 @@ function buildSandbox() {
|
||||
__emitCalls: emitCalls,
|
||||
queueMicrotask,
|
||||
};
|
||||
// startCountIn was carved into static/js/count-in.js and now reaches back into
|
||||
// app.js through the host seam (static/js/host.js). Point the seam at the SAME
|
||||
// stubs the sandbox already had: the assertions below are unchanged, they just
|
||||
// travel through the indirection the real code now uses.
|
||||
sandbox.host = {
|
||||
_audioSeek: (...a) => sandbox._audioSeek(...a),
|
||||
setPlayButtonState: () => {},
|
||||
_songEventPayload: () => ({}),
|
||||
togglePlay: () => {},
|
||||
jucePlayer: () => sandbox.jucePlayer,
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
test('loop:restart fires once when wrap path runs', async () => {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
|
||||
const startCountInSrc = extractFunction(src, 'async function startCountIn');
|
||||
|
||||
// Sanity check: the change under test is present at all. Catches
|
||||
@@ -167,7 +179,7 @@ test('loop:restart aborts when seek lands far from loopA (JUCE rollback)', async
|
||||
// _audioSeek resolves with completed:true but r.to !== loopA. The
|
||||
// wrap handler must abort instead of running beginCount on the wrong
|
||||
// position and emitting a misleading loop:restart.
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
|
||||
const startCountInSrc = extractFunction(src, 'async function startCountIn');
|
||||
|
||||
const sandbox = buildSandbox();
|
||||
@@ -202,7 +214,7 @@ test('count-in cancellation token bails delayed callbacks (rewindStep + tick)',
|
||||
// teardown can interrupt an in-flight count-in. Behavioral simulation
|
||||
// of timer cancellation is out of scope for the static extractor; this
|
||||
// verifies the contract is wired into the source.
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
|
||||
const fn = extractFunction(src, 'async function startCountIn');
|
||||
// Captures gen at entry
|
||||
assert.match(fn, /const gen = _countInGen/, 'startCountIn must capture _countInGen at entry');
|
||||
@@ -218,7 +230,7 @@ test('loop:restart fires after highway.setTime, before beginCount', () => {
|
||||
// Source-order assertion on the A-B wrap path only. Section-practice
|
||||
// `opts.immediate` also emits loop:restart but is a separate entry path;
|
||||
// the wrap handler lives inside the `_audioSeek(loopA, 'loop-wrap')` then.
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
|
||||
const fn = extractFunction(src, 'async function startCountIn');
|
||||
const wrapMarker = "_audioSeek(loopA, 'loop-wrap')";
|
||||
const wrapStart = fn.indexOf(wrapMarker);
|
||||
|
||||
@@ -14,8 +14,9 @@ const vm = require('node:vm');
|
||||
|
||||
const { extractFunction } = require('./test_utils');
|
||||
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
const SRC = fs.readFileSync(APP_JS, 'utf8');
|
||||
// the song-credits overlay was carved out of app.js into its own module (R3a).
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'count-in.js');
|
||||
const SRC = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
|
||||
|
||||
// Minimal fake DOM element: records className, children, and textContent.
|
||||
// Setting textContent clears children (matching real DOM) so we can assert
|
||||
|
||||
Reference in New Issue
Block a user