mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 21:01:40 +00:00
feat(player): global autoplay & auto-exit option (songs + lessons) (#558)
* fix(v3): pedal click opens the plugin's screen, not its settings The v3 Pedalboard's settingsTarget() resolved settings-first, so a plugin that ships both a screen and a settings panel (notably the bundled Audio Engine) could only ever reach its settings from the pedalboard — its actual page was unreachable. Flip to screen-first (stompbox metaphor: step on the pedal, see the pedal), falling back to settings when there is no screen. Keep a settings fallback in openPluginSettings() when a declared screen isn't mounted yet (installing/failed) so settings-bearing plugins are never stranded on a toast. Drive the pedal aria-label off the same target so it never promises the wrong surface. Update the unit test contract to screen > settings > none. Fixes #555 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(player): global autoplay & auto-exit option (songs + lessons) Single Settings toggle (autoplayExit, default ON) that auto-starts a song once it's ready and returns to the launching menu when it ends. Auto-exit defers while a results/score overlay is on top (heuristic + holdAutoExit() contract) so a scoring plugin's screen drives the exit. Player origin is now context-aware (lessons return to the lessons screen via setReturnScreen()), fixing lesson completion bouncing to the library. Core-only; songs and lessons share the playSong -> highway path. Adds a read-only window.slopsmith.autoplayExit getter + holdAutoExit()/setReturnScreen() for plugins. Unit tests for the pure helpers (_autoplayExitEnabled, _resolvePlayerOrigin, _resultsOverlayVisible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f79efe2516
commit
4dc5936712
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Autoplay & auto-exit — a global "click it, it plays; finish, you're back at the menu" option (default ON).** New single Settings toggle (`autoplayExit` in `localStorage`, surfaced in both the v3 and classic settings screens; absence of the key = enabled) that closes the friction at both ends of the play loop. **Autoplay:** `playSong()` previously loaded a chart paused, requiring a Play press; a one-shot flag armed per fresh load is now consumed by the next `song:ready` (highway.js) to auto-start via the existing `togglePlay()` path (HTML5 + `_juceMode` + count-in). Arrangement switches / seeks reuse the same `song:ready` event but never arm the flag, so they don't auto-restart. **Auto-exit:** on `song:ended`, core returns to the launching menu after a short grace delay — unless a visible full-screen results/dialog overlay is on top (detected via `[role=dialog][aria-modal]` / `.fixed.inset-0` with a `getClientRects()` visibility test that works for `position:fixed`), in which case the return is deferred so that score screen's own Close button (calling `window.closeCurrentSong()`) drives the exit. A plugin can also defer explicitly via the new `window.slopsmith.holdAutoExit()` (called synchronously from its own `song:ended` handler — core's listener runs first). Both paths mean **no external plugin PR is required** for a results screen to be respected. **Context-aware destination:** the player's remembered origin (`_playerOriginScreen`) now honours any real launch screen instead of clamping to library/home/favorites, and a one-shot `window.slopsmith.setReturnScreen(id)` override lets the lessons catalog (`static/v3/lessons.js`) send a finished lesson back to the lessons screen — not the song library — even though the external tutorials plugin owns the `playSong` call. Also exposes a read-only `window.slopsmith.autoplayExit` getter for plugins. Songs and lessons share the same `playSong` → highway path, so both inherit the behaviour. Core-only (`static/app.js`, `static/v3/lessons.js`, both `index.html`s); the end-of-song score screen itself remains a plugin. Optional polish (not required — the overlay heuristic already covers it): external scoring/note-detection plugins (e.g. SlopScale) may call `holdAutoExit()` + `closeCurrentSong()` for an exact, heuristic-free handoff.
|
||||
- **"Song Editor" promoted to a first-class v3 sidebar item.** The editor
|
||||
plugin (`id: editor`) now gets its own dedicated sidebar entry — under the
|
||||
Library group, just below Songs — via the existing `PROMOTED_PLUGINS`
|
||||
|
||||
152
static/app.js
152
static/app.js
@ -3239,6 +3239,8 @@ async function loadSettings() {
|
||||
document.getElementById('demucs-server-url').value = data.demucs_server_url || '';
|
||||
const leftyEl = document.getElementById('setting-lefty');
|
||||
if (leftyEl) leftyEl.checked = highway.getLefty();
|
||||
const autoplayExitEl = document.getElementById('setting-autoplay-exit');
|
||||
if (autoplayExitEl) autoplayExitEl.checked = _autoplayExitEnabled();
|
||||
// Restore master-difficulty slider from persisted value (defaults
|
||||
// to 100 when the key is absent — no behaviour change for users
|
||||
// who've never touched the slider).
|
||||
@ -5693,6 +5695,135 @@ document.addEventListener('visibilitychange', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Autoplay & auto-exit (global option, default ON) ──────────────────
|
||||
// One toggle (`autoplayExit` in localStorage) that (a) auto-starts a song
|
||||
// once it's ready and (b) returns to the launching menu when the song
|
||||
// ends. Absence of the key means enabled. The behaviour lives in core
|
||||
// (app.js, shared by the v3 + classic UIs); the end-of-song *score*
|
||||
// screen, when present, is a plugin and hooks the contract below.
|
||||
function _autoplayExitEnabled() {
|
||||
try { return localStorage.getItem('autoplayExit') !== '0'; } catch (_) { return true; }
|
||||
}
|
||||
// Settings checkbox setter (onchange="setAutoplayExit(this.checked)").
|
||||
window.setAutoplayExit = function (on) {
|
||||
try { localStorage.setItem('autoplayExit', on ? '1' : '0'); } catch (_) { /* private mode */ }
|
||||
const el = document.getElementById('setting-autoplay-exit');
|
||||
if (el && el.checked !== !!on) el.checked = !!on;
|
||||
};
|
||||
// Read-only view for plugins (e.g. a scoring plugin deciding whether to
|
||||
// auto-return after its results screen closes).
|
||||
Object.defineProperty(window.slopsmith, 'autoplayExit', {
|
||||
get: _autoplayExitEnabled, configurable: true,
|
||||
});
|
||||
// One-shot launcher override for the player's return destination.
|
||||
window.slopsmith.setReturnScreen = function (id) {
|
||||
window.slopsmith._nextReturnScreen = id || null;
|
||||
};
|
||||
// Resolve where the player should return on Esc / close / auto-exit.
|
||||
// A one-shot setReturnScreen() override wins (consumed here) — used by the
|
||||
// lessons catalog so a lesson returns to the lessons screen rather than the
|
||||
// library, even though the external tutorials plugin owns the playSong call.
|
||||
// Otherwise remember the actual launch screen; the element-exists guard
|
||||
// keeps the classic v2 UI (no #v3-* ids) from being stranded on a missing
|
||||
// screen, and unknown launches fall back to 'home'. The dashboard — classic
|
||||
// 'home' and the v3 shell's 'v3-home' — returns to the Songs list when it
|
||||
// exists (dashboard actions call playSong() directly, so its id is the
|
||||
// active screen at launch).
|
||||
function _resolvePlayerOrigin() {
|
||||
const override = window.slopsmith && window.slopsmith._nextReturnScreen;
|
||||
if (window.slopsmith) window.slopsmith._nextReturnScreen = null;
|
||||
if (override && document.getElementById(override)) return override;
|
||||
const launchFrom = document.querySelector('.screen.active');
|
||||
const launchId = launchFrom && launchFrom.id;
|
||||
if (launchId && launchId !== 'player' && document.getElementById(launchId)) {
|
||||
return ((launchId === 'home' || launchId === 'v3-home') && document.getElementById('v3-songs'))
|
||||
? 'v3-songs' : launchId;
|
||||
}
|
||||
return 'home';
|
||||
}
|
||||
|
||||
// Autoplay: one-shot flag armed by each fresh playSong(), consumed by the
|
||||
// next song:ready. song:ready also fires on arrangement switches / seeks,
|
||||
// which never arm the flag, so those don't auto-restart.
|
||||
let _pendingAutostart = false;
|
||||
window.slopsmith.on('song:ready', () => {
|
||||
if (!_pendingAutostart) return;
|
||||
_pendingAutostart = false;
|
||||
if (!_autoplayExitEnabled() || isPlaying) return;
|
||||
// Reuse the Play button's start path (handles HTML5 + _juceMode + count-in).
|
||||
Promise.resolve(togglePlay()).catch((err) => console.warn('[app] autoplay failed:', err));
|
||||
});
|
||||
|
||||
// Auto-exit: when the song ends, return to the launching menu. A scoring
|
||||
// plugin that shows an end-of-song results screen calls holdAutoExit() to
|
||||
// defer this; the user closing that screen (its Close button calls
|
||||
// window.closeCurrentSong()) performs the exit. With no results screen the
|
||||
// grace timer returns to the menu on its own.
|
||||
const AUTO_EXIT_GRACE_MS = 1500;
|
||||
let _autoExitTimer = null;
|
||||
let _autoExitHeld = false;
|
||||
// Bumped every time the auto-exit state is reset (new song via playSong, and
|
||||
// each song:ended). A hold's release() captures the generation at hold time
|
||||
// and no-ops once it changes, so a plugin that drops or fires its release
|
||||
// handle after the player has moved on can never navigate a fresh session —
|
||||
// callers don't need to balance the handle.
|
||||
let _autoExitGen = 0;
|
||||
function _clearAutoExit() {
|
||||
if (_autoExitTimer) { clearTimeout(_autoExitTimer); _autoExitTimer = null; }
|
||||
_autoExitHeld = false;
|
||||
_autoExitGen++;
|
||||
}
|
||||
// Heuristic safety net for score-screen plugins that don't (yet) call
|
||||
// holdAutoExit(): if a visible full-screen results/dialog overlay is on top
|
||||
// when the grace timer fires, defer the auto-return and let that screen's
|
||||
// own close button drive the exit (its Close should call closeCurrentSong).
|
||||
// getClientRects() is used for the visibility test because it reports
|
||||
// position:fixed overlays correctly, unlike offsetParent.
|
||||
function _resultsOverlayVisible() {
|
||||
let nodes;
|
||||
try {
|
||||
nodes = document.querySelectorAll('[role="dialog"][aria-modal="true"], .fixed.inset-0');
|
||||
} catch (_) { return false; }
|
||||
for (const el of nodes) {
|
||||
if (!el || el.id === 'player') continue; // never the player itself
|
||||
if (el.classList && el.classList.contains('hidden')) continue;
|
||||
if (el.getClientRects && el.getClientRects().length > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Plugins call this synchronously from their own song:ended handler (core
|
||||
// runs first, so the timer is already pending) to claim the exit.
|
||||
window.slopsmith.holdAutoExit = function () {
|
||||
if (_autoExitTimer) { clearTimeout(_autoExitTimer); _autoExitTimer = null; }
|
||||
_autoExitHeld = true;
|
||||
const gen = _autoExitGen;
|
||||
let released = false;
|
||||
return function release() {
|
||||
// No-op once released, or once the session has moved on (a newer
|
||||
// playSong / song:ended bumped the generation) — so a stale handle
|
||||
// never navigates away from a fresh song.
|
||||
if (released || gen !== _autoExitGen) return;
|
||||
released = true;
|
||||
if (typeof window.closeCurrentSong === 'function') window.closeCurrentSong();
|
||||
};
|
||||
};
|
||||
window.slopsmith.on('song:ended', () => {
|
||||
_clearAutoExit();
|
||||
if (!_autoplayExitEnabled()) return;
|
||||
// Only auto-exit from the player screen (ignore stale/duplicate ends).
|
||||
const active = document.querySelector('.screen.active');
|
||||
if (!active || active.id !== 'player') return;
|
||||
_autoExitTimer = setTimeout(() => {
|
||||
_autoExitTimer = null;
|
||||
if (_autoExitHeld) return; // a plugin explicitly claimed the exit
|
||||
if (_resultsOverlayVisible()) return; // a score/results overlay is up; let it drive the exit
|
||||
const cur = document.querySelector('.screen.active');
|
||||
if (cur && cur.id === 'player' && typeof window.closeCurrentSong === 'function') {
|
||||
window.closeCurrentSong();
|
||||
}
|
||||
}, AUTO_EXIT_GRACE_MS);
|
||||
});
|
||||
|
||||
// Abort controller for cancelling pending requests when entering player
|
||||
let artAbortController = null;
|
||||
|
||||
@ -5750,21 +5881,14 @@ async function playSong(filename, arrangement, options) {
|
||||
_hideSectionPracticeBar();
|
||||
|
||||
currentFilename = filename;
|
||||
// A fresh load arms autoplay; a pending auto-exit from the previous
|
||||
// song is no longer relevant.
|
||||
_pendingAutostart = true;
|
||||
_clearAutoExit();
|
||||
// Remember which screen the player was launched from so Esc /
|
||||
// navigation back from the player returns the user there
|
||||
// (slopsmith#126). Falls back to 'home' if launched from
|
||||
// somewhere unexpected (settings, a plugin screen, etc.).
|
||||
const _launchFrom = document.querySelector('.screen.active');
|
||||
const _launchId = _launchFrom && _launchFrom.id;
|
||||
const _origin = (_launchId === 'v3-songs' || _launchId === 'home' || _launchId === 'favorites')
|
||||
? _launchId : 'home';
|
||||
// In the v3 shell, `home` launches return to the v3 Songs screen. But this
|
||||
// file is shared with the classic v2 UI, where #v3-songs does not exist —
|
||||
// remapping there would make Esc call showScreen('v3-songs'), which throws
|
||||
// on the missing element and strands the user on a blank screen. Only remap
|
||||
// when the target screen is actually present.
|
||||
_playerOriginScreen = (_origin === 'home' && document.getElementById('v3-songs'))
|
||||
? 'v3-songs' : _origin;
|
||||
// navigation back from the player (and auto-exit) returns the user
|
||||
// there (slopsmith#126).
|
||||
_playerOriginScreen = _resolvePlayerOrigin();
|
||||
showScreen('player');
|
||||
|
||||
// Wait for previous WebSocket to fully close before opening new one
|
||||
|
||||
@ -299,6 +299,13 @@
|
||||
<span class="text-sm text-gray-300">Left-handed <span class="text-gray-500">(invert frets on the note highway)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center gap-3 cursor-pointer select-none">
|
||||
<input type="checkbox" id="setting-autoplay-exit" checked onchange="setAutoplayExit(this.checked)"
|
||||
class="rounded border-gray-600 bg-dark-700 text-accent focus:ring-accent/40">
|
||||
<span class="text-sm text-gray-300">Autoplay & auto-exit <span class="text-gray-500">(start songs/lessons automatically and return to the menu when the score screen closes)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-400 mb-2 block">Default Arrangement</label>
|
||||
<select id="default-arrangement"
|
||||
|
||||
@ -402,6 +402,13 @@
|
||||
<span class="text-sm text-gray-300">Left-handed <span class="text-gray-500">(invert frets on the note highway)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center gap-3 cursor-pointer select-none">
|
||||
<input type="checkbox" id="setting-autoplay-exit" checked onchange="setAutoplayExit(this.checked)"
|
||||
class="rounded border-gray-600 bg-dark-700 text-accent focus:ring-accent/40">
|
||||
<span class="text-sm text-gray-300">Autoplay & auto-exit <span class="text-gray-500">(start songs/lessons automatically and return to the menu when the score screen closes)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-400 mb-2 block">Default Arrangement</label>
|
||||
<select id="default-arrangement"
|
||||
|
||||
@ -218,6 +218,33 @@
|
||||
// ── navigation into the plugin's own lesson view ─────────────────────---
|
||||
function launchLesson(packId, lessonId) {
|
||||
if (!packId || !lessonId) return;
|
||||
// When the lesson's song auto-exits (or the player is closed), return
|
||||
// to the lessons catalog to pick the next one — not the song library.
|
||||
// playSong() consumes this one-shot override even though the external
|
||||
// tutorials plugin owns the actual playSong call. The legitimate path
|
||||
// is lessons → plugin-tutorials → player; if the launch is abandoned
|
||||
// (plugin missing, deep-link fails, user backs out) the override would
|
||||
// otherwise linger and mis-route the next library song. So clear it on
|
||||
// the first navigation that leaves the launch flow (any screen other
|
||||
// than the tutorials waypoint or the player that consumes it).
|
||||
try {
|
||||
if (window.slopsmith && typeof window.slopsmith.setReturnScreen === 'function') {
|
||||
window.slopsmith.setReturnScreen('v3-lessons');
|
||||
if (sm && typeof sm.on === 'function' && typeof sm.off === 'function') {
|
||||
const clearStale = (e) => {
|
||||
const id = e && e.detail && e.detail.id;
|
||||
if (id === 'plugin-tutorials') return; // expected waypoint — keep waiting
|
||||
// 'player' means playSong already consumed the override;
|
||||
// anything else means the launch was abandoned.
|
||||
if (id !== 'player' && window.slopsmith._nextReturnScreen === 'v3-lessons') {
|
||||
window.slopsmith.setReturnScreen(null);
|
||||
}
|
||||
sm.off('screen:changed', clearStale);
|
||||
};
|
||||
sm.on('screen:changed', clearStale);
|
||||
}
|
||||
}
|
||||
} catch (_e) { /* non-fatal */ }
|
||||
// navigate() stores nav params AND calls showScreen(); the tutorials
|
||||
// plugin's init() reads getNavParams() to deep-link to {packId, lessonId}.
|
||||
if (sm && typeof sm.navigate === 'function') {
|
||||
|
||||
186
tests/js/autoplay_exit.test.js
Normal file
186
tests/js/autoplay_exit.test.js
Normal file
@ -0,0 +1,186 @@
|
||||
// Verify the autoplay & auto-exit option's pure decision helpers in app.js:
|
||||
// - _autoplayExitEnabled() (localStorage; absence = enabled)
|
||||
// - _resolvePlayerOrigin() (one-shot override → launch screen → 'home')
|
||||
//
|
||||
// Same isolation strategy as song_close.test.js — extract the function from
|
||||
// app.js by brace-matching and run it in a vm sandbox with stubbed deps.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
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');
|
||||
|
||||
function runEnabled(stored) {
|
||||
const fnSrc = extractFunction(SRC, 'function _autoplayExitEnabled(');
|
||||
const sandbox = {
|
||||
localStorage: {
|
||||
getItem: () => {
|
||||
if (stored === '__throw__') throw new Error('private mode');
|
||||
return stored;
|
||||
},
|
||||
},
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(fnSrc + '\nglobalThis.__r = _autoplayExitEnabled();', sandbox);
|
||||
return sandbox.__r;
|
||||
}
|
||||
|
||||
// Fake element honoring the bits _resultsOverlayVisible() inspects.
|
||||
function el({ id = '', hidden = false, visible = true } = {}) {
|
||||
return {
|
||||
id,
|
||||
classList: { contains: (c) => c === 'hidden' && hidden },
|
||||
getClientRects: () => (visible ? [{}] : []),
|
||||
};
|
||||
}
|
||||
|
||||
function runOverlay(nodes) {
|
||||
const fnSrc = extractFunction(SRC, 'function _resultsOverlayVisible(');
|
||||
const sandbox = { document: { querySelectorAll: () => nodes } };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(fnSrc + '\nglobalThis.__r = _resultsOverlayVisible();', sandbox);
|
||||
return sandbox.__r;
|
||||
}
|
||||
|
||||
function runResolve({ override = null, screens = [], active = null } = {}) {
|
||||
const fnSrc = extractFunction(SRC, 'function _resolvePlayerOrigin(');
|
||||
const sandbox = {
|
||||
window: { slopsmith: { _nextReturnScreen: override } },
|
||||
document: {
|
||||
getElementById: (id) => (screens.includes(id) ? { id } : null),
|
||||
querySelector: () => (active ? { id: active } : null),
|
||||
},
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(fnSrc + '\nglobalThis.__r = _resolvePlayerOrigin();', sandbox);
|
||||
return { result: sandbox.__r, override: sandbox.window.slopsmith._nextReturnScreen };
|
||||
}
|
||||
|
||||
// holdAutoExit() + _clearAutoExit() share module state; assemble them in one
|
||||
// sandbox to exercise the generation guard on the returned release handle.
|
||||
function buildHoldSandbox() {
|
||||
const clearSrc = extractFunction(SRC, 'function _clearAutoExit(');
|
||||
const holdSrc = extractFunction(SRC, 'window.slopsmith.holdAutoExit = function ()');
|
||||
const sandbox = { __closeCount: 0 };
|
||||
sandbox.window = { slopsmith: {}, closeCurrentSong: () => { sandbox.__closeCount++; } };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(`
|
||||
var _autoExitTimer = null;
|
||||
var _autoExitHeld = false;
|
||||
var _autoExitGen = 0;
|
||||
function clearTimeout() {}
|
||||
${clearSrc}
|
||||
${holdSrc}
|
||||
globalThis.__clearAutoExit = _clearAutoExit;
|
||||
globalThis.__hold = window.slopsmith.holdAutoExit;
|
||||
`, sandbox);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
test('hold release navigates once while the generation is current', () => {
|
||||
const sb = buildHoldSandbox();
|
||||
const release = sb.__hold();
|
||||
release();
|
||||
assert.equal(sb.__closeCount, 1);
|
||||
release(); // idempotent
|
||||
assert.equal(sb.__closeCount, 1);
|
||||
});
|
||||
|
||||
test('a stale hold release no-ops after the session moves on', () => {
|
||||
const sb = buildHoldSandbox();
|
||||
const release = sb.__hold();
|
||||
sb.__clearAutoExit(); // new playSong / song:ended bumps the generation
|
||||
release();
|
||||
assert.equal(sb.__closeCount, 0, 'stale release must not navigate');
|
||||
});
|
||||
|
||||
// ── _autoplayExitEnabled ──────────────────────────────────────────────
|
||||
test('autoplayExit defaults ON when the key is absent', () => {
|
||||
assert.equal(runEnabled(null), true);
|
||||
});
|
||||
|
||||
test('autoplayExit is OFF only for the explicit "0"', () => {
|
||||
assert.equal(runEnabled('0'), false);
|
||||
assert.equal(runEnabled('1'), true);
|
||||
assert.equal(runEnabled('anything'), true);
|
||||
});
|
||||
|
||||
test('autoplayExit falls back to ON when localStorage throws', () => {
|
||||
assert.equal(runEnabled('__throw__'), true);
|
||||
});
|
||||
|
||||
// ── _resultsOverlayVisible ────────────────────────────────────────────
|
||||
test('no overlays → not visible', () => {
|
||||
assert.equal(runOverlay([]), false);
|
||||
});
|
||||
|
||||
test('a visible modal overlay defers auto-exit', () => {
|
||||
assert.equal(runOverlay([el({ id: 'mg-summary' })]), true);
|
||||
});
|
||||
|
||||
test('a hidden (.hidden) overlay does not defer', () => {
|
||||
assert.equal(runOverlay([el({ id: 'mg-summary', hidden: true })]), false);
|
||||
});
|
||||
|
||||
test('a display:none overlay (no client rects) does not defer', () => {
|
||||
assert.equal(runOverlay([el({ id: 'mg-summary', visible: false })]), false);
|
||||
});
|
||||
|
||||
test('the player screen itself never counts as a results overlay', () => {
|
||||
assert.equal(runOverlay([el({ id: 'player' })]), false);
|
||||
});
|
||||
|
||||
test('mixed: ignores player + hidden, honors a visible results overlay', () => {
|
||||
assert.equal(runOverlay([
|
||||
el({ id: 'player' }),
|
||||
el({ id: 'stale', hidden: true }),
|
||||
el({ id: 'score-card' }),
|
||||
]), true);
|
||||
});
|
||||
|
||||
// ── _resolvePlayerOrigin ──────────────────────────────────────────────
|
||||
test('one-shot override wins and is consumed when its screen exists', () => {
|
||||
const { result, override } = runResolve({
|
||||
override: 'v3-lessons', screens: ['v3-lessons', 'plugin-tutorials'], active: 'plugin-tutorials',
|
||||
});
|
||||
assert.equal(result, 'v3-lessons');
|
||||
assert.equal(override, null); // consumed, even though it won
|
||||
});
|
||||
|
||||
test('override is ignored (and still consumed) when its screen is missing', () => {
|
||||
const { result, override } = runResolve({
|
||||
override: 'ghost-screen', screens: ['favorites'], active: 'favorites',
|
||||
});
|
||||
assert.equal(result, 'favorites');
|
||||
assert.equal(override, null);
|
||||
});
|
||||
|
||||
test('remembers the real launch screen', () => {
|
||||
assert.equal(runResolve({ screens: ['v3-lessons'], active: 'v3-lessons' }).result, 'v3-lessons');
|
||||
assert.equal(runResolve({ screens: ['favorites'], active: 'favorites' }).result, 'favorites');
|
||||
});
|
||||
|
||||
test('dashboard launches (classic home + v3-home) return to the Songs list', () => {
|
||||
assert.equal(runResolve({ screens: ['home', 'v3-songs'], active: 'home' }).result, 'v3-songs');
|
||||
assert.equal(runResolve({ screens: ['v3-home', 'v3-songs'], active: 'v3-home' }).result, 'v3-songs');
|
||||
});
|
||||
|
||||
test('v3-home falls back to itself when there is no Songs list (defensive)', () => {
|
||||
assert.equal(runResolve({ screens: ['v3-home'], active: 'v3-home' }).result, 'v3-home');
|
||||
});
|
||||
|
||||
test('classic v2 (no #v3-songs) keeps home', () => {
|
||||
assert.equal(runResolve({ screens: ['home'], active: 'home' }).result, 'home');
|
||||
});
|
||||
|
||||
test('player / unknown / no active screen fall back to home', () => {
|
||||
assert.equal(runResolve({ screens: ['player'], active: 'player' }).result, 'home');
|
||||
assert.equal(runResolve({ screens: [], active: 'plugin-x' }).result, 'home');
|
||||
assert.equal(runResolve({ screens: [], active: null }).result, 'home');
|
||||
});
|
||||
@ -146,6 +146,9 @@ function loadPlaySong(sandbox) {
|
||||
var isPlaying = true;
|
||||
var currentFilename = null;
|
||||
var _playerOriginScreen = null;
|
||||
var _pendingAutostart = false;
|
||||
function _clearAutoExit() {}
|
||||
function _resolvePlayerOrigin() { return 'home'; }
|
||||
function _recordPlaybackBridge() {}
|
||||
function _cancelCountIn() {}
|
||||
function _resetJuceAudioShimChain() {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user