diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4bca4..087ea84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Fixed +- Queue overlay ("Add to queue") stopped working everywhere: YouTube moved `role="menuitem"` off the popup item wrapper onto its inner button/link, so the item was never found after the trigger opened the menu. +- Playlist picker (`Ctrl+A, Shift+P`) no-op'd on the Shorts standalone player; it now also checks the Shorts header "..." menu for the Save-to-playlist trigger (best-effort, not yet live-confirmed — see `docs/ai/questions-for-K.md`). + ## [0.3.0] - 2026-08-10 ### Added diff --git a/README.md b/README.md index 11f6fc9..5cea55e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Press `Ctrl+A`, then a command key (tmux-style prefix — outside that window ev | --- | --- | | `Ctrl+A` `o` | Open jump overlay (labels clickable elements) | | `Ctrl+A` `p` | Open queue overlay (add a video to the Up Next queue) | -| `Ctrl+A` `Shift+P` | Open playlist picker for the current video (fuzzy-search, multi-add, create new) — watch page only, requires being logged in | +| `Ctrl+A` `Shift+P` | Open playlist picker for the current video (fuzzy-search, multi-add, create new) — watch page, and Shorts on a best-effort basis, requires being logged in | | `Ctrl+A` `y` | Go home | | `Ctrl+A` `v` | Set speed to preset 1 (default `1x`) | | `Ctrl+A` `b` | Set speed to preset 2 (default `1.5x`) | diff --git a/docs/ai/questions-for-K.md b/docs/ai/questions-for-K.md index 1bcce8e..4649fbe 100644 --- a/docs/ai/questions-for-K.md +++ b/docs/ai/questions-for-K.md @@ -162,3 +162,27 @@ to whichever matches first). signal was found in probes to key on instead. Flag if this turns out to matter in practice (e.g. your account actually has duplicate-named playlists). + +--- + +Dated 2026-08-16. Surfaced while implementing issue #20 (Shorts playlist- +picker gap). + +## Q17 — Shorts "Save to playlist" trigger selector, JSON-inferred only + +`findSaveToPlaylistTrigger` now falls back to +`ytd-reel-player-header-renderer ytd-menu-renderer button` when the +existing `/watch`-page selector finds nothing, to cover the Shorts +standalone player (`/shorts/`). This is derived only from a captured +Shorts-page JSON snapshot's renderer names (`reelPlayerHeaderRenderer`, +`menuRenderer`, both confirming a "Save to playlist" item) plus this +codebase's own already-confirmed `` -> `ytd-foo-renderer` +naming convention — it has **not** been checked against a live/rendered +Shorts DOM (no working headless browser in this sandbox this session +either; see `docs/probes/save-trigger-dom-findings.md` follow-up section +for the exact reasoning and what would falsify it). +**Default:** ship the selector above. If wrong, same failure mode as every +other best-effort selector in this file: `findSaveToPlaylistTrigger` +returns `null`, `openPlaylistOverlay` no-ops, safe no-op with no user- +visible error, just `Shift+P` staying silently unavailable on Shorts. +Please confirm the real selector live when convenient. diff --git a/docs/keyboard-quickstart.md b/docs/keyboard-quickstart.md index 5e68569..ee38a33 100644 --- a/docs/keyboard-quickstart.md +++ b/docs/keyboard-quickstart.md @@ -13,7 +13,7 @@ want via the extension's stored settings, same as any other override. | `Ctrl+A` | Prefix — arms the next key as a command (default binding, configurable) | Global | | `Ctrl+A` then `o` | Open jump overlay (labels clickable elements) | Global | | `Ctrl+A` then `p` | Open queue overlay (labels videos with an "Add to queue" option; typing a label adds that video next) | Global | -| `Ctrl+A` then `Shift+P` | Open playlist picker for the current video (fuzzy-search playlists, checkbox multi-add, create new) — watch page only, requires being logged in | Watch page | +| `Ctrl+A` then `Shift+P` | Open playlist picker for the current video (fuzzy-search playlists, checkbox multi-add, create new) — requires being logged in | Watch page, and Shorts on a best-effort basis (see issue #20) | | `Ctrl+A` then `y` | Go home (click YouTube logo, or navigate to configured home URL) | Global | | `Ctrl+A` then `v` | Set playback speed to preset 1 (default `1×`) | Global | | `Ctrl+A` then `b` | Set playback speed to preset 2 (default `1.5×`) | Global | diff --git a/docs/probes/add-to-queue-dom-findings.md b/docs/probes/add-to-queue-dom-findings.md index 9ae9ab6..53dd2e8 100644 --- a/docs/probes/add-to-queue-dom-findings.md +++ b/docs/probes/add-to-queue-dom-findings.md @@ -292,3 +292,25 @@ genuinely never replaces the node — it falls back to clicking whatever's there pre-existing behavior rather than failing outright. Covered by two new unit tests in `tests/unit/queue-overlay.test.js`: one simulating a poll that returns the stale node for a couple of ticks before the fresh one appears, one simulating a node that never changes at all. + +## Follow-up: YouTube moved `role="menuitem"` off the item wrapper (2026-08-16) + +User reported the queue overlay regressing to a no-op: the "..." trigger still opened a popup +(visible only as a brief touch-feedback ripple), but no video was ever added. Captured a fresh +live DOM (`playwright` launched directly per [[feedback_firefox_testing]] — the `mcp__playwright__*` +tools still fail to launch a browser in this sandbox) against a real watch page. + +Root cause: `yt-list-item-view-model`, which previously carried `role="menuitem"` itself, now +carries `role="presentation"` — the role moved to its inner interactive child +(` + + + + + + + + + + + + + + diff --git a/tests/fixtures/youtube-watch-basic.html b/tests/fixtures/youtube-watch-basic.html index bc3dae6..b6a7da8 100644 --- a/tests/fixtures/youtube-watch-basic.html +++ b/tests/fixtures/youtube-watch-basic.html @@ -35,7 +35,9 @@

Fixture video title

- Add to queue + + + diff --git a/tests/unit/queue-overlay.test.js b/tests/unit/queue-overlay.test.js index ea165c3..bb9bf34 100644 --- a/tests/unit/queue-overlay.test.js +++ b/tests/unit/queue-overlay.test.js @@ -77,7 +77,7 @@ describe('activateQueueTarget', () => { querySelector: (sel) => { assert.equal( sel, - 'ytd-popup-container yt-list-item-view-model[role="menuitem"], ' + 'ytd-popup-container yt-list-item-view-model [role="menuitem"], ' + 'ytd-popup-container ytd-menu-service-item-renderer', ); queried = true; diff --git a/tests/unit/youtube-site-adapter.test.js b/tests/unit/youtube-site-adapter.test.js index 79fdaf7..f5f3a7a 100644 --- a/tests/unit/youtube-site-adapter.test.js +++ b/tests/unit/youtube-site-adapter.test.js @@ -105,6 +105,7 @@ describe('isLoggedIn', () => { describe('findSaveToPlaylistTrigger', () => { const SAVE_SELECTOR = '#flexible-item-buttons > yt-button-view-model button[aria-label]'; + const SHORTS_SELECTOR = 'ytd-reel-player-header-renderer ytd-menu-renderer button'; it('returns the button matching the Save wrapper selector', () => { const btn = {}; @@ -112,10 +113,31 @@ describe('findSaveToPlaylistTrigger', () => { assert.equal(findSaveToPlaylistTrigger(doc), btn); }); - it('returns null when the Save wrapper is absent', () => { + it('returns null when neither the watch-page nor the Shorts selector matches', () => { const doc = { querySelector: () => null }; assert.equal(findSaveToPlaylistTrigger(doc), null); }); + + it('prefers the watch-page selector over the Shorts fallback when both match', () => { + const watchBtn = {}; + const shortsBtn = {}; + const doc = { + querySelector: (sel) => { + if (sel === SAVE_SELECTOR) return watchBtn; + if (sel === SHORTS_SELECTOR) return shortsBtn; + return null; + }, + }; + assert.equal(findSaveToPlaylistTrigger(doc), watchBtn); + }); + + it('falls back to the Shorts header menu button when the watch-page selector finds nothing', () => { + const shortsBtn = {}; + const doc = { + querySelector: (sel) => (sel === SHORTS_SELECTOR ? shortsBtn : null), + }; + assert.equal(findSaveToPlaylistTrigger(doc), shortsBtn); + }); }); function makeWindow(href) {