From e12375f2d261ea1c4fe89cf73684d1d4a1e38f36 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Fri, 31 Jul 2026 22:27:33 -0400 Subject: [PATCH] fix(popup): target the displayed media frame --- IMPLEMENTATION_PLAN.md | 2 +- extension/content/frame-speed-snapshot.js | 4 ++++ extension/content/inject.js | 13 ++++++++++- extension/popup/popup.js | 17 +++++++++++++- extension/shared/popup-controls.js | 3 +++ tests/inject-lifecycle.regression.test.js | 20 +++++++++++++++++ tests/popup.spec.js | 27 +++++++++++++++++++++-- tests/shared.test.js | 9 ++++++++ 8 files changed, 90 insertions(+), 5 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 2f160d9..bee9e35 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -5,7 +5,7 @@ Constraints: local commits only; no push; no browser testing; one commit per fea ## Controller and targeting - [x] Keep the controller visible for its own video in element and ancestor fullscreen. -- [ ] Target popup actions at the frame represented by the displayed speed; keep “all videos” intentional. +- [x] Target popup actions at the frame represented by the displayed speed; keep “all videos” intentional. - [ ] Ignore shortcuts originating from editable controls, including shadow-DOM inputs. ## Accessibility and usability diff --git a/extension/content/frame-speed-snapshot.js b/extension/content/frame-speed-snapshot.js index fe765e6..f08bf36 100644 --- a/extension/content/frame-speed-snapshot.js +++ b/extension/content/frame-speed-snapshot.js @@ -8,6 +8,10 @@ if (!v) return null; return { speed: v.playbackRate, + frameToken: + typeof tc === "object" && typeof tc.frameToken === "string" + ? tc.frameToken + : null, preferred: !v.paused, forceLastSavedSpeed: Boolean( typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed diff --git a/extension/content/inject.js b/extension/content/inject.js index a6c9246..17933f2 100644 --- a/extension/content/inject.js +++ b/extension/content/inject.js @@ -156,7 +156,11 @@ var tc = { pendingMediaCandidates: [], settingsReloadRetries: 0, lastPointerPosition: null, - lastInteractedMedia: null + lastInteractedMedia: null, + frameToken: + window.crypto && typeof window.crypto.randomUUID === "function" + ? window.crypto.randomUUID() + : String(Date.now()) + "-" + Math.random().toString(36).slice(2) }; var MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.1; @@ -2335,6 +2339,7 @@ function loadInitialRuntimeSettings(attempt) { if (!videoGs) return false; sendResponse({ speed: videoGs.playbackRate, + frameToken: tc.frameToken, forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true, forceLastSavedSpeedControlledBySiteRule: Boolean( tc.activeSiteRule && @@ -2371,6 +2376,12 @@ function loadInitialRuntimeSettings(attempt) { return false; } if (request.action === "run_action") { + if ( + request.targetFrameToken && + request.targetFrameToken !== tc.frameToken + ) { + return false; + } if ( !siteRuleUtils.isSpeederActiveForSite( tc.settings.enabled, diff --git a/extension/popup/popup.js b/extension/popup/popup.js index b9a1210..1b3283c 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -27,6 +27,8 @@ document.addEventListener("DOMContentLoaded", function () { var popupExcludedButtonIds = new Set(["settings"]); var renderToken = 0; var forceLastSavedSpeedControlledBySiteRule = null; + var selectedFrameToken = null; + var shortcutTargetMode = "closest"; function persistExpandedSettings(rawStorage, settings, callback) { var mutation = vscBuildManagedStorageMutation(rawStorage, settings); @@ -162,6 +164,10 @@ document.addEventListener("DOMContentLoaded", function () { if (response && response.speed != null) { updateSpeedDisplay(response.speed); } + selectedFrameToken = + response && typeof response.frameToken === "string" + ? response.frameToken + : null; } function pickBestFrameSpeedResult(results) { @@ -247,8 +253,12 @@ document.addEventListener("DOMContentLoaded", function () { window.open(chrome.runtime.getURL("options/options.html")); return; } + var message = { action: "run_action", actionName: btnId }; + if (shortcutTargetMode !== "all" && selectedFrameToken) { + message.targetFrameToken = selectedFrameToken; + } sendToActiveTab( - { action: "run_action", actionName: btnId }, + message, function () { querySpeed(); } @@ -372,6 +382,7 @@ document.addEventListener("DOMContentLoaded", function () { function renderForActiveTab() { var currentRenderToken = ++renderToken; forceLastSavedSpeedControlledBySiteRule = null; + selectedFrameToken = null; setForceButtonLoading(true); chrome.storage.local.get(["customButtonIcons"], function (loc) { @@ -395,6 +406,10 @@ document.addEventListener("DOMContentLoaded", function () { storage.enabled, siteRule ); + shortcutTargetMode = + siteRule && siteRule.shortcutTargetMode !== undefined + ? siteRule.shortcutTargetMode + : storage.shortcutTargetMode; var showBar = storage.showPopupControlBar !== false; forceLastSavedSpeedControlledBySiteRule = Boolean( siteRule && siteRule.forceLastSavedSpeed !== undefined diff --git a/extension/shared/popup-controls.js b/extension/shared/popup-controls.js index cac3000..75fd9bf 100644 --- a/extension/shared/popup-controls.js +++ b/extension/shared/popup-controls.js @@ -68,6 +68,9 @@ function normalizeResult(result) { var normalized = { speed: result.speed }; + if (typeof result.frameToken === "string") { + normalized.frameToken = result.frameToken; + } if (typeof result.forceLastSavedSpeed === "boolean") { normalized.forceLastSavedSpeed = result.forceLastSavedSpeed; } diff --git a/tests/inject-lifecycle.regression.test.js b/tests/inject-lifecycle.regression.test.js index f81b1de..a56d1eb 100644 --- a/tests/inject-lifecycle.regression.test.js +++ b/tests/inject-lifecycle.regression.test.js @@ -309,6 +309,26 @@ describe("inject.js media/controller lifecycle regressions", () => { expect(second.video.playbackRate).toBe(1.2); }); + it("ignores popup actions addressed to another frame", async () => { + const chrome = bootInject(); + await settleLifecycle(); + const { video } = createControlledVideo(); + const listener = chrome.runtime.onMessage.listeners[0]; + const initialSpeed = video.playbackRate; + + listener( + { + action: "run_action", + actionName: "faster", + targetFrameToken: "another-frame" + }, + {}, + vi.fn() + ); + + expect(video.playbackRate).toBe(initialSpeed); + }); + it("drops a stale hover-preview shortcut target after SPA navigation", async () => { bootInject({ url: "https://www.youtube.com/", diff --git a/tests/popup.spec.js b/tests/popup.spec.js index 4103175..b48a4c6 100644 --- a/tests/popup.spec.js +++ b/tests/popup.spec.js @@ -110,7 +110,7 @@ describe("popup.js", () => { speedQueryCount <= 2 ? [ { speed: 1.25, preferred: false }, - { speed: 1.5, preferred: true } + { speed: 1.5, frameToken: "playing-frame", preferred: true } ] : [{ speed: 1.75, preferred: true }] ); @@ -138,12 +138,35 @@ describe("popup.js", () => { expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( 99, - { action: "run_action", actionName: "faster" }, + { + action: "run_action", + actionName: "faster", + targetFrameToken: "playing-frame" + }, expect.any(Function) ); expect(document.querySelector("#popupSpeed").textContent).toBe("1.75"); }); + it("keeps all-video popup actions intentionally untargeted", async () => { + const chrome = bootPopup({ + syncData: { shortcutTargetMode: "all" }, + executeScriptImpl: (tabId, details, callback) => { + callback([{ speed: 1.5, frameToken: "playing-frame", preferred: true }]); + } + }); + await flushAsyncWork(); + chrome.tabs.sendMessage.mockClear(); + + document.querySelector("#popupControlBar button").click(); + + expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( + 99, + { action: "run_action", actionName: "rewind" }, + expect.any(Function) + ); + }); + it("toggles enablement and closes after a successful refresh", async () => { const chrome = bootPopup({ syncData: { diff --git a/tests/shared.test.js b/tests/shared.test.js index c80dcfc..0039529 100644 --- a/tests/shared.test.js +++ b/tests/shared.test.js @@ -134,6 +134,15 @@ describe("shared helpers", () => { ).toEqual(["advance"]); }); + it("keeps the selected frame token with the displayed popup speed", () => { + expect( + popupControls.pickBestFrameSpeedResult([ + { speed: 1.25, frameToken: "background", preferred: false }, + { speed: 1.75, frameToken: "playing", preferred: true } + ]) + ).toEqual({ speed: 1.75, frameToken: "playing" }); + }); + it("normalizes controller locations and margins", () => { expect(controllerUtils.normalizeControllerLocation("top-right")).toBe( "top-right"