mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-19 15:46:15 +00:00
fix(extension): control the player frame without having to elect it first
Reported from the live site: a media title was recognised and audio processing worked, but play and pause did nothing. Both halves of the command path were gated on frame election, and the election had named the top frame. Outbound, commands went to the elected frame alone, which holds no video, so they were delivered and ignored. Inbound, isCurrentContentSender() required sender.frameId to equal the elected frame, so the user's own play and pause arriving from the real player frame were discarded as a stale sender — which is why the room never saw them. Neither direction actually needs the election. Every content-script command handler already begins with findVideo() and returns when there is none, so a tab-wide broadcast is delivered to all frames and acted on only by the one that owns the player. And an inbound media event proves where the player is: sender.frameId is authoritative, costs no permission and has no timing window, so the reporting frame is adopted as the target and later commands are addressed directly again. Both relaxations apply only while the elected frame reports no video. A good election still takes the strict path, so the hidden-player rejections are unaffected. This is the general answer to losing webNavigation.getAllFrames(). That call observed the frame tree without touching it, so it never had a failure window; executeScript has to enter every frame and reliably loses that race against a player which renavigates and rebuilds its video, as Kodik does. The fix is to stop depending on the answer rather than to keep chasing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -741,6 +741,38 @@ test('stays ready on a page whose ad frames keep mutating', async ({ context, ex
|
||||
.toBe('ready');
|
||||
});
|
||||
|
||||
test('controls and adopts a nested player even while the top frame is elected', async ({ context, extensionId, baseURL }) => {
|
||||
// The failure mode reported from the live site: the election names the top
|
||||
// frame, which holds no video, so commands go nowhere and the user's own
|
||||
// play/pause from the real player frame is discarded as a stale sender.
|
||||
const url = `${baseURL}/pages/yummy-deferred-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', frameId: 0, hasVideo: false });
|
||||
|
||||
// Build the player without giving the monitor a chance to promote first.
|
||||
const deferred = page.frames().find(frame => frame.url().endsWith('/frames/deferred-player-frame.html'));
|
||||
await deferred.locator('#poster').click();
|
||||
await expect.poll(() => deferred.locator('video').count()).toBe(1);
|
||||
|
||||
// A command must reach the frame that owns the video regardless of election.
|
||||
await sendServerCommand(context, extensionId, tabId, 'play', { time: 1 });
|
||||
await expect
|
||||
.poll(() => deferred.locator('video').evaluate(video => video.paused), { timeout: 15000 })
|
||||
.toBe(false);
|
||||
|
||||
// And once that frame reports playback, it becomes the addressed target.
|
||||
await expect
|
||||
.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' })
|
||||
.then(state => state.targetFrameId), { timeout: 15000 })
|
||||
.not.toBe(0);
|
||||
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
|
||||
expect(status).toMatchObject({ targetTabId: tabId, targetHasVideo: true });
|
||||
});
|
||||
|
||||
test('keeps the tab selected when its activation fails', async ({ context, extensionId }) => {
|
||||
// A page the extension is not allowed to script stands in for any activation
|
||||
// failure the user can act on. Losing the selection here is what made the
|
||||
|
||||
Reference in New Issue
Block a user