Fix tuner auto-open flash: opt-in + persist (issue E, stage 1/3) (#655)

The tuner self-closes on song:play; autoplay fires it right after a song
switch, so an auto-opened tuner flashed shut ~1s later. An arrangement
switch (which never arms autoplay) instead persisted — the opposite
tester reports, and not the mic.

- New opt-in setting autoOpenOnTuningChange (tuner Settings, default OFF)
- An auto-opened tuner persists: it ignores the autoplay song:play, stray
  outside-clicks, and same-screen re-emits, closing only via the new
  in-panel x / Skip buttons or leaving the song. A manual open keeps the
  classic click-away / play-to-close behaviour.
- Adds the panel's first in-box close (x + contextual Skip).
- All in the tuner plugin; no core app.js changes.

Default (opt-in vs opt-out) is teed up for Byron to flip one boolean.
Staged follow-ups: E1.5 = instrument-coverage smart prompting + badge
cue; E2 = holdAutoplay gate.

Tests: tests/js/tuner_auto_open.test.js (opt-in gate, persist mode,
play/click-proofing).


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChrisBeWithYou
2026-07-01 10:24:39 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent bc4d2a3592
commit 6bfd92aa06
6 changed files with 120 additions and 13 deletions
+39 -4
View File
@@ -26,7 +26,10 @@ function loadTuningHelpers() {
const feedBackHelpers = loadTuningHelpers();
function createTunerSandbox() {
function createTunerSandbox(opts) {
// Auto-open is opt-in (default off in prod). The sandbox defaults it ON so the
// behaviour tests exercise the feature; pass { autoOpen: false } to gate it off.
const autoOpen = !opts || opts.autoOpen !== false;
const enableCalls = [];
let playerActive = true;
let songInfo = null;
@@ -44,6 +47,7 @@ function createTunerSandbox() {
showFloatingButton: true,
visualizationMode: 'default',
audioInputMode: 'auto',
autoOpenOnTuningChange: autoOpen,
lastInstrument: 'guitar-6',
lastTuning: 'Standard',
freeTune: false,
@@ -155,9 +159,9 @@ function createTunerSandbox() {
vm.runInContext(fs.readFileSync(TUNER_SCREEN_JS, 'utf8'), sandbox);
const realEnable = sandbox.window.tuner.enable.bind(sandbox.window.tuner);
sandbox.window.tuner.enable = async () => {
enableCalls.push(1);
return realEnable();
sandbox.window.tuner.enable = async (enableOpts) => {
enableCalls.push(enableOpts || {});
return realEnable(enableOpts);
};
return sandbox;
@@ -315,6 +319,37 @@ test('song:loading clears dismiss state for next load', async () => {
assert.equal(sandbox.__enableCalls.length, 2);
});
test('auto-open is gated off when the setting is disabled (opt-in)', async () => {
const sandbox = createTunerSandbox({ autoOpen: false });
sandbox.window._tunerAutoOpen.resetState();
await ready(sandbox, CUSTOM_GUITAR);
await ready(sandbox, E_STANDARD); // a real tuning change, but the setting is off
assert.equal(sandbox.__enableCalls.length, 0);
});
test('auto-open enables in persist mode (passes { auto: true })', async () => {
const sandbox = createTunerSandbox();
sandbox.window._tunerAutoOpen.resetState();
await ready(sandbox, CUSTOM_GUITAR);
await ready(sandbox, E_STANDARD);
assert.equal(sandbox.__enableCalls.length, 1);
assert.equal(sandbox.__enableCalls[0].auto, true);
});
test('persist: an auto-opened tuner is not torn down by autoplay / stray clicks', () => {
const screenSrc = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
const uiSrc = fs.readFileSync(
path.join(__dirname, '..', '..', 'plugins', 'tuner', 'utils', 'ui.js'), 'utf8');
// The gate is opt-in on the server config flag.
assert.match(screenSrc, /autoOpenOnTuningChange/);
// enable() records whether this was an auto-open …
assert.match(screenSrc, /_state\.autoOpened\s*=\s*auto/);
// … the outside-click dismiss is armed only for a manual open …
assert.match(screenSrc, /if \(!auto\)[\s\S]*?addEventListener\('click'/);
// … and the autoplay song:play closer ignores an auto-opened tuner (the flash fix).
assert.match(uiSrc, /state\.enabled && !state\.autoOpened/);
});
test('screen.js registers song:loading and song:ready auto-open listeners at boot', () => {
const src = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
assert.match(src, /function _installAutoOpenListeners/);