Files
KoalaSync/tests/e2e/extension.spec.mjs
T

613 lines
30 KiB
JavaScript

import { test, expect } from './helpers/extension-fixture.mjs';
/**
* Drives the packed extension itself: real background service worker, real
* chrome.scripting injection, real runtime messaging. The detection specs cover
* which element gets picked; this file covers whether the extension ever gets
* far enough to pick one.
*/
/** Runs code in an extension page, where the privileged chrome.* APIs exist. */
async function withExtensionPage(context, extensionId, fn) {
const page = await context.newPage();
await page.goto(`chrome-extension://${extensionId}/popup.html`);
const result = await fn(page);
await page.close();
return result;
}
async function selectTargetTab(context, extensionId, pageUrl) {
return withExtensionPage(context, extensionId, page => page.evaluate(async (url) => {
const [tab] = await chrome.tabs.query({ url });
if (!tab) throw new Error(`no tab matched ${url}`);
const response = await chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId: tab.id });
return { tabId: tab.id, response };
}, pageUrl));
}
async function sendServerCommand(context, extensionId, tabId, action, payload) {
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ tabId, action, payload }) => {
return chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload: payload || {},
expectedTabId: tabId
});
}, { tabId, action, payload }));
}
async function sendServerCommandBurst(context, extensionId, tabId, commands) {
return withExtensionPage(context, extensionId, page => page.evaluate(async ({ tabId, commands }) => {
return Promise.all(commands.map(({ action, payload }) => chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action,
payload: payload || {},
expectedTabId: tabId
})));
}, { tabId, commands }));
}
async function getExtensionState(context, extensionId, message) {
return withExtensionPage(context, extensionId, page => page.evaluate(
request => chrome.runtime.sendMessage(request),
message
));
}
async function setAudioSettings(context, extensionId, settings) {
return withExtensionPage(context, extensionId, page => page.evaluate(
value => chrome.storage.local.set({ audioSettings: value }),
settings
));
}
async function getAudioRouteState(context, extensionId, pageUrl) {
return withExtensionPage(context, extensionId, page => page.evaluate(async url => {
const [tab] = await chrome.tabs.query({ url });
if (!tab) throw new Error(`no tab matched ${url}`);
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const route = window.__koalaSyncAudioRoute;
if (!route) {
return {
hasRoute: false,
contextState: null,
chainActive: null,
hasVideo: false,
signalLevel: 0
};
}
let analyser = route.testAnalyser;
if (!analyser) {
analyser = route.audioCtx.createAnalyser();
analyser.fftSize = 32;
route.chain.limiter.connect(analyser);
route.testAnalyser = analyser;
}
if (!route.testSignal) {
const oscillator = route.audioCtx.createOscillator();
const gain = route.audioCtx.createGain();
gain.gain.value = 0.05;
oscillator.frequency.value = 440;
oscillator.connect(gain);
gain.connect(route.chain.limiter);
oscillator.start();
route.testSignal = { oscillator, gain };
}
const samples = new Uint8Array(analyser.fftSize);
analyser.getByteTimeDomainData(samples);
return {
hasRoute: true,
contextState: route.audioCtx?.state || null,
chainActive: route.chain?.active ?? null,
hasVideo: !!route.video,
signalLevel: Math.max(...Array.from(samples, sample => Math.abs(sample - 128)))
};
}
});
return result?.result || null;
}, pageUrl));
}
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 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] }
: { tabId: tab.id, frameIds: [frame.frameId] };
const [result] = await chrome.scripting.executeScript({
target,
func: () => typeof window.__koalaMediaFrameMonitorCleanup
});
return result?.result;
}, { pageUrl, frameUrlPart }));
}
test('injects into the target tab and attaches to a same-origin frame player', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/iframe-player.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const { response } = await selectTargetTab(context, extensionId, url);
expect(response?.status, 'SET_TARGET_TAB should not report a failure').not.toBe('error');
await expect.poll(
() => page.evaluate(() => {
const video = document.querySelector('iframe').contentDocument.querySelector('video');
return video ? video.dataset.koalaAttached : null;
}),
{ message: 'content script should attach to the video inside the frame' }
).toBe('true');
});
test('keeps the selected tab after the popup page closes and reopens', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/simple-player.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(status).toMatchObject({
targetTabId: tabId,
targetReady: true,
targetActivationState: 'ready'
});
});
test('keeps a selected target even when the page has no video element', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/no-video.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', tabId });
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(status).toMatchObject({
targetTabId: tabId,
targetReady: true,
targetActivationState: 'ready',
targetHasVideo: false
});
});
test('keeps a previously selected player audible after switching targets', async ({ context, extensionId, baseURL }) => {
const firstUrl = `${baseURL}/pages/simple-player.html`;
const secondUrl = `${baseURL}/pages/no-video.html`;
const first = await context.newPage();
const second = await context.newPage();
await first.goto(firstUrl);
await first.waitForFunction(() => window.__fixtureReady === true);
await second.goto(secondUrl);
await second.waitForFunction(() => window.__fixtureReady === true);
await setAudioSettings(context, extensionId, {
enabled: true,
boostDb: 1,
compressor: { enabled: false, preset: 'recommended', customParams: {} }
});
const { tabId: firstTabId } = await selectTargetTab(context, extensionId, firstUrl);
await first.evaluate(() => document.getElementById('player').play());
await expect.poll(
() => getAudioRouteState(context, extensionId, firstUrl),
{ message: 'the selected player must have a live audio route' }
).toMatchObject({ hasRoute: true, contextState: 'running', hasVideo: true });
await expect.poll(
() => getAudioRouteState(context, extensionId, firstUrl).then(state => state?.signalLevel || 0),
{ message: 'the selected player must produce a signal through the audio route' }
).toBeGreaterThan(1);
const { response } = await selectTargetTab(context, extensionId, secondUrl);
expect(response).toMatchObject({ status: 'ok' });
await first.evaluate(() => document.getElementById('player').play());
await expect.poll(() => first.locator('#player').evaluate(video => ({
paused: video.paused,
muted: video.muted,
volume: video.volume
}))).toMatchObject({ paused: false, muted: false });
await expect.poll(
() => getAudioRouteState(context, extensionId, firstUrl).then(state => state?.signalLevel || 0),
{ message: 'switching targets must not mute the previous audio route' }
).toBeGreaterThan(1);
await selectTargetTab(context, extensionId, firstUrl);
await expect.poll(
() => getAudioRouteState(context, extensionId, firstUrl),
{ message: 'reselecting the player must reuse its live audio route' }
).toMatchObject({ hasRoute: true, contextState: 'running', hasVideo: true });
await expect.poll(
() => getAudioRouteState(context, extensionId, firstUrl).then(state => state?.signalLevel || 0),
{ message: 'reselecting the player must keep the audio signal alive' }
).toBeGreaterThan(1);
const playResponse = await sendServerCommand(context, extensionId, firstTabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => first.locator('#player').evaluate(video => video.paused)).toBe(false);
});
test('applies remote play, pause and seek to the framed player', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/iframe-player.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const { tabId } = await selectTargetTab(context, extensionId, url);
await expect.poll(() => page.evaluate(() => {
const video = document.querySelector('iframe').contentDocument.querySelector('video');
return video ? video.dataset.koalaAttached : null;
})).toBe('true');
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => page.evaluate(FRAMED_VIDEO_PAUSED), { message: 'remote play should start playback' }).toBe(false);
await sendServerCommand(context, extensionId, tabId, 'pause');
await expect.poll(() => page.evaluate(FRAMED_VIDEO_PAUSED), { message: 'remote pause should stop playback' }).toBe(true);
await sendServerCommand(context, extensionId, tabId, 'seek', { targetTime: 6 });
await expect.poll(
() => page.evaluate(() => document.querySelector('iframe').contentDocument.querySelector('video').currentTime),
{ message: 'remote seek should move the framed player' }
).toBeGreaterThan(5);
});
test('reinjects after the target tab navigates', async ({ context, extensionId, baseURL }) => {
const first = `${baseURL}/pages/iframe-player.html`;
const page = await context.newPage();
await page.goto(first);
await page.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, first);
await expect.poll(() => page.evaluate(() => {
const video = document.querySelector('iframe').contentDocument.querySelector('video');
return video ? video.dataset.koalaAttached : null;
})).toBe('true');
await page.goto(`${baseURL}/pages/simple-player.html`);
await page.waitForFunction(() => window.__fixtureReady === true);
await expect.poll(
() => page.evaluate(() => {
const video = document.getElementById('player');
return video ? video.dataset.koalaAttached : null;
}),
{ message: 'the content script should come back after a navigation' }
).toBe('true');
});
test('re-attaches after the player frame swaps its document', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/reloading-frame.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, url);
await expect.poll(() => page.evaluate(() => {
const video = document.querySelector('iframe').contentDocument.getElementById('framed-player');
return video ? video.dataset.koalaAttached : null;
})).toBe('true');
// Navigating the frame replaces its document without touching the top one,
// so nothing but a load hook on the frame can notice the new player.
await page.evaluate(() => {
document.querySelector('iframe').src = 'frames/player-frame-2.html';
});
await expect.poll(
() => page.evaluate(() => {
const video = document.querySelector('iframe').contentDocument.getElementById('framed-player-2');
return video ? video.dataset.koalaAttached : null;
}),
{ message: 'the content script should follow the frame to its new document' }
).toBe('true');
});
test('re-attaches when a nested player frame swaps its document', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/nested-frame.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, url);
const innerVideo = (id) => page.evaluate((videoId) => {
const outer = document.querySelector('iframe').contentDocument;
const inner = outer.querySelector('iframe').contentDocument;
const video = inner.getElementById(videoId);
return video ? video.dataset.koalaAttached : null;
}, id);
await expect.poll(() => innerVideo('framed-player')).toBe('true');
// The reloading frame sits at depth two. A load hook that only covers
// top-level frames would never fire for it.
await page.evaluate(() => {
const outer = document.querySelector('iframe').contentDocument;
outer.querySelector('iframe').src = 'player-frame-2.html';
});
await expect.poll(
() => innerVideo('framed-player-2'),
{ message: 'a frame two levels down should be watched for reloads too' }
).toBe('true');
});
test('moves local event listeners after a CSS-only player switch', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/player-css-switch.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, url);
await expect.poll(() => page.locator('#first').getAttribute('data-koala-attached')).toBe('true');
await page.waitForTimeout(250);
const beforeBurst = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await page.evaluate(() => {
const first = document.getElementById('first');
first.dispatchEvent(new window.Event('play'));
first.dispatchEvent(new window.Event('pause'));
window.switchPlayer();
});
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.lastActionState.action === 'play'
&& status.lastActionState.timestamp > beforeBurst.lastActionState.timestamp;
}).toBe(true);
const leading = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await expect.poll(
() => page.locator('#second').getAttribute('data-koala-attached'),
{ message: 'attribute-only visibility changes must move the active controller' }
).toBe('true');
expect(await page.locator('#first').getAttribute('data-koala-attached')).toBeNull();
const afterSwitch = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(afterSwitch.lastActionState.action).toBe('play');
expect(afterSwitch.lastActionState.timestamp).toBe(leading.lastActionState.timestamp);
const before = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
await page.locator('#first').evaluate(video => video.dispatchEvent(new window.Event('play')));
await page.waitForTimeout(250);
const afterStale = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
expect(afterStale.lastActionState.timestamp).toBe(before.lastActionState.timestamp);
await page.locator('#second').evaluate(video => video.dispatchEvent(new window.Event('play')));
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.lastActionState.timestamp;
}).toBeGreaterThan(afterStale.lastActionState.timestamp);
});
test('re-elects after a wrapper-only change inside an unselected cross-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-internal-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('slot=first'));
const second = page.frames().find(frame => frame.url().includes('slot=second'));
await selectTargetTab(context, extensionId, url);
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
expect(await second.locator('video').getAttribute('data-koala-attached')).toBeNull();
await page.evaluate(() => window.switchInternalPlayer());
await expect.poll(
() => second.locator('video').getAttribute('data-koala-attached'),
{ message: 'an unselected frame must announce its internally-visible player' }
).toBe('true');
expect(await first.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('targets a visible nested cross-origin player and keeps top-page debug context', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-nested.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const visiblePlayer = () => page.frames().find(frame => frame.url().includes('/frames/player-frame.html?visible=1'));
await expect.poll(async () => visiblePlayer()?.locator('video').getAttribute('src')).toContain('player-480p-12s.mp4');
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
expect(response.frameId).toBeGreaterThan(0);
await expect.poll(() => visiblePlayer()?.locator('video').getAttribute('data-koala-attached')).toBe('true');
const hiddenPlayer = page.frames().find(frame => frame.url().includes('/frames/player-frame-2.html?hidden=1'));
expect(await hiddenPlayer.locator('video').getAttribute('data-koala-attached')).toBeNull();
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.paused)).toBe(false);
await sendServerCommand(context, extensionId, tabId, 'pause');
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.paused)).toBe(true);
await sendServerCommand(context, extensionId, tabId, 'seek', { targetTime: 6 });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(5);
await sendServerCommand(context, extensionId, tabId, 'pause');
await sendServerCommandBurst(context, extensionId, tabId, [
{ action: 'seek', payload: { targetTime: 8 } },
{ action: 'play', payload: { currentTime: 8 } }
]);
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => ({
paused: video.paused,
currentTime: video.currentTime
}))).toMatchObject({ paused: false, currentTime: expect.any(Number) });
await expect.poll(() => visiblePlayer().locator('video').evaluate(video => video.currentTime)).toBeGreaterThan(7);
const state = await getExtensionState(context, extensionId, { type: 'GET_VIDEO_STATE', tabId });
expect(state).toMatchObject({
found: true,
url,
pageTitle: 'Nested cross-origin player',
frameOrigin: new URL(baseURL.replace('localhost', '127.0.0.1')).origin,
inIframe: true
});
});
test('re-elects the visible cross-origin player after an iframe switch', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('/frames/player-frame.html?slot=first'));
const second = page.frames().find(frame => frame.url().includes('/frames/player-frame-2.html?slot=second'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
const firstFrameId = response.frameId;
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
await page.evaluate(() => window.switchPlayer());
await expect.poll(() => second.locator('video').getAttribute('data-koala-attached')).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetFrameId;
}).not.toBe(firstFrameId);
const playResponse = await sendServerCommand(context, extensionId, tabId, 'play');
expect(playResponse).toMatchObject({ status: 'ok_solo' });
await expect.poll(() => second.locator('video').evaluate(video => video.paused)).toBe(false);
expect(await first.locator('video').evaluate(video => video.paused)).toBe(true);
});
test('keeps commands flowing during continuous player-frame geometry changes', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-switching.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('/frames/player-frame.html?slot=first'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
await page.evaluate(() => window.startGeometryChurn());
try {
await page.waitForTimeout(300);
await sendServerCommand(context, extensionId, tabId, 'play', { currentTime: 1 });
await expect.poll(
() => first.locator('video').evaluate(video => video.paused),
{ timeout: 3000, message: 'bounded refresh passes must not starve commands' }
).toBe(false);
} finally {
await page.evaluate(() => window.stopGeometryChurn());
}
});
test('deactivates media monitors in child frames after a target-tab switch', async ({ context, extensionId, baseURL }) => {
const firstUrl = `${baseURL}/pages/cross-origin-nested.html`;
const secondUrl = `${baseURL}/pages/simple-player.html`;
const firstPage = await context.newPage();
const secondPage = await context.newPage();
await firstPage.goto(firstUrl);
await firstPage.waitForFunction(() => window.__fixtureReady === true);
await secondPage.goto(secondUrl);
await secondPage.waitForFunction(() => window.__fixtureReady === true);
await selectTargetTab(context, extensionId, firstUrl);
await expect.poll(() => getFrameMonitorState(
context,
extensionId,
firstUrl,
'/frames/player-frame.html?visible=1'
)).toBe('function');
await selectTargetTab(context, extensionId, secondUrl);
await expect.poll(
() => getFrameMonitorState(
context,
extensionId,
firstUrl,
'/frames/player-frame.html?visible=1'
),
{ message: 'child-frame monitor should be destroyed with the old target tab' }
).toBe('undefined');
});
test('re-attaches after a selected cross-origin frame navigates', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-reloading.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const first = page.frames().find(frame => frame.url().includes('generation=first'));
const { tabId, response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok' });
await expect.poll(() => first.locator('video').getAttribute('data-koala-attached')).toBe('true');
const firstDocumentId = (await getExtensionState(context, extensionId, { type: 'GET_STATUS' })).targetDocumentId;
await page.evaluate(() => window.reloadPlayer());
await expect.poll(() => page.frames().find(frame => frame.url().includes('generation=second'))?.locator('video').getAttribute('data-koala-attached')).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetDocumentId;
}).not.toBe(firstDocumentId);
const state = await getExtensionState(context, extensionId, { type: 'GET_VIDEO_STATE', tabId });
expect(state).toMatchObject({ found: true, inIframe: true });
});
test('discovers a video inserted late inside a cross-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/cross-origin-late.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const lateFrame = page.frames().find(frame => frame.url().includes('/frames/late-player-frame.html'));
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', hasVideo: false });
await expect.poll(
() => lateFrame.locator('#late-player').getAttribute('data-koala-attached'),
{ timeout: 12_000, message: 'late cross-origin video should trigger target re-election' }
).toBe('true');
await expect.poll(async () => {
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
return status.targetHasVideo;
}).toBe(true);
});
test('rejects a cross-origin player hidden three frame levels deep', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/deep-hidden-cross-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const hiddenFrame = page.frames().find(frame => frame.url().includes('deep=hidden'));
await expect.poll(() => hiddenFrame?.locator('video').getAttribute('src')).toContain('player-1080p-30s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await hiddenFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('rejects a player inside a hidden same-origin frame', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/hidden-same-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const hiddenFrame = page.frames().find(frame => frame.url().includes('hidden=same-origin'));
await expect.poll(() => hiddenFrame?.locator('video').getAttribute('src')).toContain('player-480p-12s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await hiddenFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
test('rejects a hidden cross-origin player after its iframe URL redirects', async ({ context, extensionId, baseURL }) => {
const url = `${baseURL}/pages/hidden-redirect-cross-origin.html`;
const page = await context.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
const redirectedFrame = page.frames().find(frame => frame.url().includes('redirected=hidden'));
await expect.poll(() => redirectedFrame?.locator('video').getAttribute('src')).toContain('player-1080p-30s.mp4');
const { response } = await selectTargetTab(context, extensionId, url);
expect(response).toMatchObject({ status: 'ok', frameId: 0, hasVideo: false });
expect(await redirectedFrame.locator('video').getAttribute('data-koala-attached')).toBeNull();
});
function FRAMED_VIDEO_PAUSED() {
return document.querySelector('iframe').contentDocument.querySelector('video').paused;
}