mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 12:21:49 +00:00
fix(nav): the library sometimes showed the legacy screen — map 'home' inside showScreen (#923)
Testers: "randomly, when moving to the library from another menu option, the library shows the
old interface — never when a song ends."
━━━ WHAT WAS ACTUALLY HAPPENING ━━━
#home is the PRE-V3 library screen. The v3 shell replaced it with #v3-songs, and the mapping DID
exist — but only inside WRAPPERS on window.showScreen, and only for callers that go through
`window`. THREE independent parties monkey-patch it, each capturing whatever happens to be there
at the time:
app.js publishes the raw function
-> shell.js wraps it, adding the home -> v3-songs mapping
-> the stems plugin wraps it AGAIN (src/main.js:1029), capturing the current value
Plugins load ASYNCHRONOUSLY. The chain links up in whatever order the race settles, and any
capture taken before shell.js installs — or any re-assignment after it — silently drops the
mapping. Hence "randomly".
AND THE INTERNAL CALLERS NEVER TOUCHED window.showScreen AT ALL. closeCurrentSong and the
Esc-from-settings shortcut call the IMPORTED showScreen, which no wrapper ever sees. Reproduced
in a browser: the unwrapped function with 'home' lands on the dead legacy screen EVERY time.
"Never when a song ends" is the tell, and it is what identified the mechanism: closeCurrentSong
resolves its target through _resolvePlayerOrigin(), which ALREADY applies this mapping. That one
path was fine — which is exactly why the bug looked random rather than total.
PRE-EXISTING, not a regression from the module carve: the onclick="showScreen('home')" links and
the wrapper-only mapping both date to 2026-06-22.
━━━ THE FIX ━━━
The guard lives inside showScreen now: ONE place, in the function every caller routes through,
instead of a chain of monkey-patches that must each remember. Wrapper order stops mattering, and
the module-internal callers are covered for the first time.
Verified in a browser: the raw, unwrapped showScreen('home') — which reproduced as #home — now
lands on #v3-songs, and cannot be undone by any wrapper order.
━━━ AND A [P1] I INTRODUCED, WHICH CODEX CAUGHT ━━━
My first cut mapped BOTH 'home' and 'v3-home', copied straight from _resolvePlayerOrigin.
That is correct THERE and wrong HERE. _resolvePlayerOrigin computes where to RETURN TO after a
song, and landing on the Songs list from the dashboard is the right behaviour. But #v3-home is
the v3 DASHBOARD — a real screen that the shell's Home nav, the onboarding tour and the dashboard
re-render listener all target. Redirecting it would have made Home unreachable.
A LEGACY ALIAS IS NOT THE SAME THING AS A RETURN TARGET. Only 'home' is mapped now, and a test
pins that: re-adding 'v3-home' to the guard fails it.
4 tests, bite-tested both ways.
node 1049, pytest 2425, ESLint 0, Codex 0.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
57e7db5c2a
commit
f27d4f623c
@ -105,6 +105,45 @@ export let _settingsOriginScreen = 'home';
|
||||
|
||||
// ── Screen Navigation ─────────────────────────────────────────────────────
|
||||
export async function showScreen(id) {
|
||||
// ── 'home' is the LEGACY library screen. Always route it to the v3 Songs list. ──
|
||||
//
|
||||
// The v3 shell replaced #home with #v3-songs. That mapping DID exist — but only inside
|
||||
// wrappers on `window.showScreen`, and only for callers that go through `window`:
|
||||
//
|
||||
// app.js publishes the raw fn -> shell.js wraps it (adding the mapping)
|
||||
// -> the stems plugin wraps it AGAIN, capturing whatever
|
||||
// happened to be there at the time
|
||||
//
|
||||
// Two ways that fails, and testers hit both:
|
||||
//
|
||||
// 1. ORDER. Three independent parties monkey-patch window.showScreen, each capturing the
|
||||
// current value. Plugins load ASYNCHRONOUSLY, so the chain links up in whatever order
|
||||
// the race settles — and any capture taken before shell.js installs, or any
|
||||
// re-assignment after it, silently drops the mapping.
|
||||
//
|
||||
// 2. THE INTERNAL CALLERS NEVER TOUCHED window.showScreen AT ALL. closeCurrentSong and the
|
||||
// Esc-from-settings shortcut call the IMPORTED showScreen directly, so no wrapper ever
|
||||
// sees them. Verified in a browser: the unwrapped function with 'home' lands on the dead
|
||||
// legacy screen every single time.
|
||||
//
|
||||
// Hence "randomly, when moving to the library from another menu option" — and "never when a
|
||||
// song ends", because closeCurrentSong resolves its target through _resolvePlayerOrigin(),
|
||||
// which already applies this mapping.
|
||||
//
|
||||
// So it lives HERE now: ONE guard in the function every caller routes through, rather than a
|
||||
// chain of monkey-patches that must each remember.
|
||||
//
|
||||
// ONLY 'home'. NOT 'v3-home'. _resolvePlayerOrigin() maps BOTH — correctly, because it
|
||||
// computes where to RETURN TO after a song, and coming back to the Songs list from the
|
||||
// dashboard is the right behaviour. Copying that condition here was a [P1] (Codex caught it):
|
||||
// #v3-home is the v3 DASHBOARD, a real screen the shell's Home nav, the onboarding tour and
|
||||
// the dashboard re-render listener all target. Redirecting it would make Home unreachable.
|
||||
//
|
||||
// A legacy alias is not the same thing as a return target.
|
||||
if (id === 'home' && document.getElementById('v3-songs')) {
|
||||
id = 'v3-songs';
|
||||
}
|
||||
|
||||
// Capture the previous screen before changing active classes
|
||||
const prevScreenId = document.querySelector('.screen.active')?.id;
|
||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||
|
||||
85
tests/js/show_screen_legacy_home.test.js
Normal file
85
tests/js/show_screen_legacy_home.test.js
Normal file
@ -0,0 +1,85 @@
|
||||
// showScreen('home') must never land on the LEGACY library screen when v3 is present.
|
||||
//
|
||||
// Testers: "randomly, when moving to the library from another menu option, the library shows the
|
||||
// old interface — never when a song ends."
|
||||
//
|
||||
// #home is the pre-v3 library screen. The v3 shell replaced it with #v3-songs, and the mapping
|
||||
// DID exist — but only inside wrappers on `window.showScreen`, which fail two ways:
|
||||
//
|
||||
// 1. ORDER. THREE independent parties monkey-patch window.showScreen, each capturing whatever
|
||||
// is there at the time: app.js publishes the raw function, shell.js wraps it to add the
|
||||
// mapping, and the stems plugin wraps it again. Plugins load ASYNCHRONOUSLY, so the chain
|
||||
// links up in whatever order the race settles. A capture taken before shell.js installs —
|
||||
// or any re-assignment after it — silently drops the mapping. Hence "randomly".
|
||||
//
|
||||
// 2. THE INTERNAL CALLERS BYPASS window.showScreen ENTIRELY. closeCurrentSong and the
|
||||
// Esc-from-settings shortcut call the IMPORTED showScreen, which no wrapper ever sees.
|
||||
// Verified in a browser: the unwrapped function with 'home' lands on #home, always.
|
||||
//
|
||||
// "Never when a song ends" is the tell: closeCurrentSong resolves its target through
|
||||
// _resolvePlayerOrigin(), which already applied the mapping — so that one path was fine.
|
||||
//
|
||||
// The guard now lives inside showScreen itself: one place every caller routes through.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const SESSION_JS = path.join(__dirname, '..', '..', 'static', 'js', 'session.js');
|
||||
const src = () => fs.readFileSync(SESSION_JS, 'utf8');
|
||||
|
||||
function bodyOf(name) {
|
||||
const s = src();
|
||||
const at = s.indexOf(`export async function ${name}(`);
|
||||
assert.notEqual(at, -1, `${name} not found`);
|
||||
let depth = 0;
|
||||
for (let i = s.indexOf('{', at); i < s.length; i++) {
|
||||
if (s[i] === '{') depth++;
|
||||
else if (s[i] === '}' && --depth === 0) return s.slice(at, i + 1);
|
||||
}
|
||||
throw new Error('unbalanced');
|
||||
}
|
||||
|
||||
test('showScreen maps the legacy #home library to #v3-songs', () => {
|
||||
const fn = bodyOf('showScreen');
|
||||
assert.match(
|
||||
fn,
|
||||
/id\s*===\s*'home'[\s\S]{0,80}getElementById\('v3-songs'\)[\s\S]{0,60}id\s*=\s*'v3-songs'/,
|
||||
"showScreen must route 'home' to 'v3-songs' ITSELF — relying on a wrapper over "
|
||||
+ 'window.showScreen loses the mapping whenever a plugin wraps it first, and misses the '
|
||||
+ 'module-internal callers (closeCurrentSong, Esc-from-settings) altogether',
|
||||
);
|
||||
});
|
||||
|
||||
test('the guard runs BEFORE the screen is activated', () => {
|
||||
const fn = bodyOf('showScreen');
|
||||
const guard = fn.search(/id\s*=\s*'v3-songs'/);
|
||||
const activate = fn.indexOf('classList.add(\'active\')');
|
||||
assert.ok(guard !== -1 && activate !== -1);
|
||||
assert.ok(guard < activate,
|
||||
'the mapping must be applied before the screen is activated, or #home is shown first');
|
||||
});
|
||||
|
||||
test('the guard is conditional on v3 actually being present', () => {
|
||||
const fn = bodyOf('showScreen');
|
||||
assert.match(fn, /getElementById\('v3-songs'\)/,
|
||||
'the mapping must check #v3-songs exists — without it there is nowhere to route to');
|
||||
});
|
||||
|
||||
test('it does NOT redirect v3-home — the dashboard is a real screen', () => {
|
||||
// Codex [P1] on the first cut. _resolvePlayerOrigin() maps BOTH 'home' and 'v3-home' —
|
||||
// correctly, because it computes where to RETURN TO after a song, and landing on the Songs
|
||||
// list from the dashboard is right. Copying that condition into showScreen is NOT: #v3-home
|
||||
// is the v3 DASHBOARD, which the shell's Home nav, the onboarding tour and the dashboard
|
||||
// re-render listener all target. Redirecting it makes Home unreachable.
|
||||
//
|
||||
// A legacy alias is not the same thing as a return target.
|
||||
const fn = bodyOf('showScreen');
|
||||
// the condition, i.e. everything between `if (` and the `{` that opens `id = 'v3-songs'`
|
||||
const m = fn.match(/if \(([\s\S]*?)\)\s*\{\s*id = 'v3-songs';/);
|
||||
assert.ok(m, 'the legacy-home guard was not found');
|
||||
assert.doesNotMatch(m[1], /v3-home/,
|
||||
"showScreen must NOT redirect 'v3-home' — that is the dashboard, not the legacy library");
|
||||
assert.match(m[1], /id === 'home'/, "it must still redirect the legacy 'home'");
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user