diff --git a/extension/background.js b/extension/background.js index c65e715..179908f 100644 --- a/extension/background.js +++ b/extension/background.js @@ -2190,7 +2190,7 @@ function setPageApiSeekEnabled(enabled) { } async function deactivateMediaFrameMonitors(tabId) { - const targets = await listMediaFrameScriptTargets(chrome, tabId); + const targets = listMediaFrameScriptTargets(tabId); await Promise.all(targets.map(async target => { const documentId = target.documentIds?.[0]; const frameId = target.frameIds?.[0]; @@ -2293,7 +2293,7 @@ function createTargetActivationSupersededError() { } async function injectMediaFrameMonitors(tabId, contentTarget) { - const targets = await listMediaFrameScriptTargets(chrome, tabId); + const targets = listMediaFrameScriptTargets(tabId); let injectedCount = 0; await Promise.all(targets.map(async target => { try { @@ -2923,22 +2923,6 @@ if (chrome.permissions?.onAdded?.addListener) { }); } -if (chrome.webNavigation?.onCompleted?.addListener) { - chrome.webNavigation.onCompleted.addListener((details) => { - ensureState().then(async () => { - const tabId = normalizeTabId(details?.tabId); - const frameId = normalizeFrameId(details?.frameId); - if (tabId === null - || tabId !== normalizeTabId(currentTabId) - || frameId === 0 - || (frameId !== normalizeFrameId(currentTargetFrameId) && currentTargetHasVideo)) { - return; - } - await refreshCurrentMediaTarget(tabId, { queueIfRunning: true }); - }).catch(error => addLog(`Media frame navigation refresh failed: ${error.message}`, 'warn')); - }); -} - if (chrome.tabs?.onRemoved?.addListener) { chrome.tabs.onRemoved.addListener((removedTabId) => { ensureState().then(async () => { diff --git a/extension/manifest.base.json b/extension/manifest.base.json index 6f29b71..07d01fa 100644 --- a/extension/manifest.base.json +++ b/extension/manifest.base.json @@ -11,8 +11,7 @@ "scripting", "alarms", "activeTab", - "notifications", - "webNavigation" + "notifications" ], "host_permissions": [ "" diff --git a/extension/media-frame-target.js b/extension/media-frame-target.js index a75e3b0..1af799e 100644 --- a/extension/media-frame-target.js +++ b/extension/media-frame-target.js @@ -427,21 +427,7 @@ function contentTarget(tabId, selected) { }; } -export async function listMediaFrameScriptTargets(chromeApi, tabId) { - if (chromeApi.webNavigation?.getAllFrames) { - try { - const frames = await chromeApi.webNavigation.getAllFrames({ tabId }); - if (Array.isArray(frames) && frames.length > 0) { - return frames - .filter(frame => Number.isInteger(frame?.frameId)) - .map(frame => (typeof frame.documentId === 'string' && frame.documentId - ? { tabId, documentIds: [frame.documentId] } - : { tabId, frameIds: [frame.frameId] })); - } - } catch { - // Older browsers fall back to the all-frames probe below. - } - } +export function listMediaFrameScriptTargets(tabId) { return [{ tabId, allFrames: true }]; } @@ -466,7 +452,7 @@ export async function resolveMediaContentTarget(chromeApi, tabId, { let ambiguous = false; for (let attempt = 0; attempt < attempts; attempt++) { - const scriptTargets = await listMediaFrameScriptTargets(chromeApi, tabId); + const scriptTargets = listMediaFrameScriptTargets(tabId); let results = await executeInAccessibleFrames( chromeApi, scriptTargets, diff --git a/extension/media-frame-target.test.mjs b/extension/media-frame-target.test.mjs index bcaee18..fd534a9 100644 --- a/extension/media-frame-target.test.mjs +++ b/extension/media-frame-target.test.mjs @@ -155,55 +155,19 @@ describe('cross-origin media-frame targeting', () => { }); }); - it('keeps an accessible player when an unrelated child frame is denied', async () => { - const top = frame(0, { - bestVideo: null, - videoCount: 0, - embeddedFrames: [ - { - href: 'https://player-8.example/embed', - origin: 'https://player-8.example', - area: 830 * 498, - width: 830, - height: 498, - visible: true, - mediaHint: true - }, - { - href: 'https://widget-denied.example/frame', - origin: 'https://widget-denied.example', - area: 300 * 250, - width: 300, - height: 250, - visible: true, - mediaHint: false - } - ] - }); - const player = frame(8); - const getAllFrames = vi.fn().mockResolvedValue([ - { frameId: 0, documentId: 'document-0' }, - { frameId: 8, documentId: 'document-8' }, - { frameId: 9, documentId: 'document-9' } - ]); - const executeScript = vi.fn().mockImplementation(async ({ target, func }) => { - const documentId = target.documentIds?.[0]; - if (documentId === 'document-9') throw new Error('Cannot access contents of the page'); - if (func?.name !== 'inspectMediaFrame') return []; - return documentId === 'document-8' ? [player] : [top]; - }); + it('uses all-frame probing without a navigation permission', async () => { + const executeScript = vi.fn().mockResolvedValue([frame(8)]); await expect(resolveMediaContentTarget( - { scripting: { executeScript }, webNavigation: { getAllFrames } }, + { scripting: { executeScript } }, 42, { attempts: 1, probeDelayMs: 0 } )).resolves.toMatchObject({ frameId: 8, - documentId: 'document-8', - hasVideo: true + hasVideo: true, + scriptTarget: { tabId: 42, documentIds: ['document-8'] } }); - expect(executeScript.mock.calls.some(([options]) => options.target.allFrames === true)).toBe(false); - expect(executeScript.mock.calls.some(([options]) => options.target.documentIds?.[0] === 'document-9')).toBe(true); + expect(executeScript.mock.calls[0][0].target).toEqual({ tabId: 42, allFrames: true }); }); it('does not trust parent visibility from an older probe token', () => { diff --git a/extension/target-tab-lifecycle.test.mjs b/extension/target-tab-lifecycle.test.mjs index e2f1ada..123e0c2 100644 --- a/extension/target-tab-lifecycle.test.mjs +++ b/extension/target-tab-lifecycle.test.mjs @@ -39,14 +39,14 @@ describe('target tab lifecycle', () => { it('removes monitors injected by a superseded cross-tab activation', () => { expect(backgroundSource).toContain('function isTargetActivationSuperseded(tabId, activationGeneration)'); - expect(backgroundSource).toContain('activationGeneration\n });'); + expect(backgroundSource).toMatch(/navigationRetries: navigationRetries - 1,\s*activationGeneration\s*\}\)/); expect(backgroundSource).toMatch(/await injectMediaFrameMonitors\(tabId, contentTarget\);[\s\S]*if \(isTargetActivationSuperseded\(tabId, activationGeneration\)\)[\s\S]*await deactivateMediaFrameMonitors\(tabId\);/); expect(backgroundSource).toContain("error.code = 'target_activation_superseded'"); }); - it('binds cross-origin targets to an exact document and monitors every accessible frame', () => { + it('uses all-frame probing for cross-origin targets without navigation permissions', () => { expect(backgroundSource).toContain("files: ['media-frame-monitor.js']"); - expect(backgroundSource).toContain('const targets = await listMediaFrameScriptTargets(chrome, tabId)'); + expect(backgroundSource).toContain('const targets = listMediaFrameScriptTargets(tabId)'); expect(backgroundSource).toContain('One denied widget frame must not block the selected player'); expect(backgroundSource).toContain("navigationError.code = 'media_target_navigated'"); expect(backgroundSource).toContain("{ type: 'MEDIA_MONITOR_DEACTIVATE' }"); @@ -57,8 +57,15 @@ describe('target tab lifecycle', () => { expect(monitorSource).toContain('if (!force && nextSignature === lastCandidateSignature) return'); expect(monitorSource).toContain("const MEDIA_STATE_EVENTS = ['play', 'pause', 'loadedmetadata'"); expect(monitorSource).toContain("node.querySelector?.('video, iframe, frame')"); - expect(manifest.permissions).toContain('webNavigation'); - expect(backgroundSource).toContain('chrome.webNavigation.onCompleted.addListener'); + expect(manifest.permissions).toEqual([ + 'storage', + 'tabs', + 'scripting', + 'alarms', + 'activeTab', + 'notifications' + ]); + expect(backgroundSource).not.toMatch(/chrome\.(?:web)?Navigation/); }); it('serializes content commands and coalesces target refreshes', () => { diff --git a/tests/e2e/extension.spec.mjs b/tests/e2e/extension.spec.mjs index f14da24..1915a38 100644 --- a/tests/e2e/extension.spec.mjs +++ b/tests/e2e/extension.spec.mjs @@ -58,8 +58,11 @@ async function getFrameMonitorState(context, extensionId, pageUrl, frameUrlPart) return withExtensionPage(context, extensionId, page => page.evaluate(async ({ pageUrl, frameUrlPart }) => { const [tab] = await chrome.tabs.query({ url: pageUrl }); if (!tab) throw new Error(`no tab matched ${pageUrl}`); - const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id }); - const frame = frames.find(candidate => candidate.url.includes(frameUrlPart)); + const frameResults = await chrome.scripting.executeScript({ + target: { tabId: tab.id, allFrames: true }, + func: () => ({ href: location.href, cleanup: typeof window.__koalaMediaFrameMonitorCleanup }) + }); + const frame = frameResults.find(candidate => candidate.result?.href.includes(frameUrlPart)); if (!frame) throw new Error(`no frame matched ${frameUrlPart}`); const target = frame.documentId ? { tabId: tab.id, documentIds: [frame.documentId] } diff --git a/tests/e2e/playwright.config.mjs b/tests/e2e/playwright.config.mjs index b8ec138..a324214 100644 --- a/tests/e2e/playwright.config.mjs +++ b/tests/e2e/playwright.config.mjs @@ -1,4 +1,5 @@ import { defineConfig } from '@playwright/test'; +import { fileURLToPath } from 'node:url'; const PORT = Number(process.env.KOALA_E2E_PORT || 4173); @@ -25,7 +26,7 @@ export default defineConfig({ } }, webServer: { - command: `node ${new URL('./fixture-server.mjs', import.meta.url).pathname} ${PORT}`, + command: `node "${fileURLToPath(new URL('./fixture-server.mjs', import.meta.url))}" ${PORT}`, url: `http://localhost:${PORT}/pages/simple-player.html`, reuseExistingServer: !process.env.CI, stdout: 'ignore',