mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-22 00:46:37 +00:00
fix(extension): find the player when the all-frames sweep comes back empty
Diagnosed against a fixture rebuilt from the live yummyanime.tv page, with the ad churn the real site produces. Under that churn the resolver reported frame=0, hasVideo=false while the video demonstrably existed two frame levels down: one ad slot tearing down mid-call makes Chromium reject the whole allFrames sweep, and the resolver then silently fell back to the top frame and never looked again. v3.1.2 did not have this failure because webNavigation.getAllFrames() gave it an explicit frame list. That list is now rebuilt without the permission: every content script that messages the background carries sender.frameId, so the background keeps a per-tab registry of frames it has seen and the resolver asks any frame the sweep missed directly. One rejected probe now costs one frame instead of the whole page. Two supporting fixes fell out of the same investigation. Frames reported hidden by an ancestor that could inspect them directly — the 0x0 same-origin wrapper an anime host parks unwatched mirrors in — are now excluded without waiting for the postMessage visibility handshake, which was the tie the resolver kept failing to break. And leaf frames with no video and no nested frames are left out of that handshake entirely, so a churning ad slot can no longer make every phase wait on a frame that is already gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -706,6 +706,41 @@ test('polling video state on a page with no video does not restart the target',
|
||||
.toBe('true');
|
||||
});
|
||||
|
||||
test('stays ready on a page whose ad frames keep mutating', async ({ context, extensionId, baseURL }) => {
|
||||
test.setTimeout(90000);
|
||||
// Live ad churn wakes the media-frame monitor several times a second. Each
|
||||
// wake used to schedule a trailing refresh that rebuilt the target
|
||||
// unconditionally, and rebuilding produced more churn — a loop that never
|
||||
// let the activation settle and pinned the popup on "activating".
|
||||
const url = `${baseURL}/pages/yummy-churning-player.html`;
|
||||
const page = await context.newPage();
|
||||
await page.goto(url);
|
||||
await page.waitForFunction(() => window.__fixtureReady === true);
|
||||
|
||||
const { tabId } = await selectTargetTab(context, extensionId, url);
|
||||
|
||||
for (let sample = 0; sample < 8; sample++) {
|
||||
await page.waitForTimeout(700);
|
||||
const status = await getExtensionState(context, extensionId, { type: 'GET_STATUS' });
|
||||
expect(status, `sample ${sample} must not be stuck activating`).toMatchObject({
|
||||
targetTabId: tabId,
|
||||
targetReady: true,
|
||||
targetActivationState: 'ready'
|
||||
});
|
||||
}
|
||||
|
||||
// The player still has to be picked up while the churn continues.
|
||||
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').getAttribute('data-koala-attached'), { timeout: 20000 })
|
||||
.toBe('true');
|
||||
await expect
|
||||
.poll(() => getExtensionState(context, extensionId, { type: 'GET_STATUS' })
|
||||
.then(state => state.targetActivationState), { timeout: 20000 })
|
||||
.toBe('ready');
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Anime page with live ad churn</title>
|
||||
<style>
|
||||
body { margin: 0; font: 14px sans-serif; }
|
||||
#info { height: 900px; padding: 24px; }
|
||||
iframe { border: 0; }
|
||||
#visible-wrapper { display: block; }
|
||||
#hidden-wrapper { width: 0; height: 0; display: block; }
|
||||
#ads iframe { width: 300px; height: 60px; display: block; }
|
||||
</style>
|
||||
<!--
|
||||
The anime layout plus what the real site actually does while you look at it:
|
||||
ad slots that add, remove and resize frames continuously. Every one of those
|
||||
mutations wakes the media-frame monitor, so this is the fixture that exposes a
|
||||
reactivation loop — a static page never will.
|
||||
-->
|
||||
<div id="info">
|
||||
<h1>Series page</h1>
|
||||
<div id="ads"></div>
|
||||
</div>
|
||||
<iframe id="visible-wrapper" name="xfplayer_visible" width="830" height="498"
|
||||
src="frames/xfp-wrapper.html?player=deferred-player-frame.html" allowfullscreen></iframe>
|
||||
<iframe id="hidden-wrapper" name="xfplayer_hidden"
|
||||
src="frames/xfp-wrapper.html?player=player-frame-2.html" allowfullscreen></iframe>
|
||||
<script>
|
||||
const ads = document.getElementById('ads');
|
||||
let tick = 0;
|
||||
setInterval(() => {
|
||||
tick++;
|
||||
const stale = ads.firstElementChild;
|
||||
if (stale && tick % 2 === 0) stale.remove();
|
||||
const slot = document.createElement('iframe');
|
||||
slot.src = `about:blank#ad-${tick}`;
|
||||
slot.style.height = `${40 + (tick % 5) * 8}px`;
|
||||
ads.append(slot);
|
||||
}, 150);
|
||||
|
||||
const wrappers = [...document.querySelectorAll('#visible-wrapper, #hidden-wrapper')];
|
||||
Promise.all(wrappers.map(f => new Promise(resolve => {
|
||||
f.addEventListener('load', resolve, { once: true });
|
||||
}))).then(() => { window.__fixtureReady = true; });
|
||||
</script>
|
||||
Reference in New Issue
Block a user