fix(tuner): anchor injectPlayerButton to a direct-child button (feedBack#800)

`injectPlayerButton()` anchored the injected Tuner button with
`controls.querySelector('button:last-child')`, which can match a NESTED
button that is not a direct child of `#player-controls`. `insertBefore(btn,
nestedButton)` then throws `NotFoundError` (the reference node must be a
direct child); since injection runs from the tuner's `screen:changed`
handler, the throw propagated out of the player-screen transition and
stalled its render. The v3 path was already safe (plugin-control slot);
only the classic anchor was bad.

Use `:scope > button:last-of-type` (direct child only) with a
`parentNode === controls` guard before insertBefore, falling back to
appendChild. Bump plugins/tuner 1.3.3 → 1.3.4.

Test: tests/plugins/tuner/js/inject_player_button.test.js — extracts the
real function and runs it over a faithful DOM model whose insertBefore
enforces the direct-child invariant; the nested-last-button case reproduces
the throw on the old anchor and passes on the new one (5 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
byrongamatos
2026-07-07 09:54:06 +02:00
co-authored by Claude Opus 4.8
parent cb72c5ab34
commit 9fb63fd3b5
4 changed files with 202 additions and 3 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "tuner",
"name": "Guitar/Bass Tuner",
"version": "1.3.3",
"version": "1.3.4",
"bundled": true,
"private": false,
"script": "screen.js",
+9 -2
View File
@@ -869,8 +869,15 @@ window._tunerUI = function(state, actions) {
btn.textContent = 'Tuner';
btn.title = 'Open Tuner';
btn.onclick = window.tuner.toggle;
const closeBtn = isV3 ? null : controls.querySelector('button:last-child');
if (closeBtn) controls.insertBefore(btn, closeBtn);
// Anchor to the last DIRECT-child button of `controls` (the classic
// transport's close/exit button). A bare `button:last-child` can match
// a NESTED button that is not a direct child of `controls`, and
// `insertBefore()` then throws NotFoundError — which propagated out of
// the player-screen transition and aborted its render (feedBack#800).
// `:scope > button:last-of-type` restricts the anchor to a direct child;
// the parentNode check is a belt-and-suspenders guard before insertBefore.
const closeBtn = isV3 ? null : controls.querySelector(':scope > button:last-of-type');
if (closeBtn && closeBtn.parentNode === controls) controls.insertBefore(btn, closeBtn);
else controls.appendChild(btn);
updatePlayerButton();
}