test: prove the frame lifecycle fixes against the real extension

The evidence for the frame-observation fixes was a reimplementation of the
logic measured in a browser console, not the shipped code. Two extension
specs now cover the observable behaviour end to end: a player frame that
swaps its document, and the same one level deeper.

The nested case fails against the pre-fix content.js and passes now. The
top-level case already passed before the fix, so that fix removed dead
observer registrations without changing what a user could see; recorded
here so the distinction is not lost.

Also adds bench-finder.mjs, which measures the extracted shipped finder
instead of a transcription of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
KoalaDev
2026-08-14 07:53:45 +02:00
parent 66971150c7
commit ef1fdc89c3
8 changed files with 177 additions and 0 deletions
+15
View File
@@ -50,6 +50,21 @@ reads as a broken fixture instead of a scoring regression.
| `background-loop.html` | Silent looping hero must lose despite being the largest |
| `multi-player.html` | Between equal players, the playing one wins |
| `sourceless.html` | A large `<video>` with no source can never be the player |
| `nested-frame.html` | Player two frame levels down |
| `reloading-frame.html` | Frame that swaps its document, with no mutation in the top one |
## Benchmark
`bench-finder.mjs` is not a spec, because timings are machine dependent and
would only add noise to CI. Run it by hand when the finder changes:
```bash
node tests/e2e/fixture-server.mjs 4173 & node tests/e2e/bench-finder.mjs
```
It measures the shipped finder (lifted from `content.js`) against the
pre-v3.1.0 formula, which is transcribed inside the script since that code no
longer exists in the tree.
## Regenerating the media
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Measures findVideo() on a page loaded with same-origin frames.
*
* Not a spec: timings are machine dependent and would only add noise to CI.
* Run it by hand when the finder changes:
*
* node tests/e2e/fixture-server.mjs 4173 &
* node tests/e2e/bench-finder.mjs
*
* The measured implementation is the shipped one, lifted out of content.js by
* the same helper the specs use. The pre-v3.1.0 formula it is compared against
* is transcribed here, since that code no longer exists in the tree.
*/
import { chromium } from '@playwright/test';
import { buildVideoFinderScript } from './helpers/content-source.mjs';
const url = process.env.KOALA_E2E_URL || 'http://localhost:4173/pages/ad-frame.html';
const FRAMES = Number(process.env.KOALA_BENCH_FRAMES || 40);
const CALLS = Number(process.env.KOALA_BENCH_CALLS || 200);
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required'] });
const page = await browser.newPage();
await page.goto(url);
await page.waitForFunction(() => window.__fixtureReady === true);
await page.addScriptTag({ content: buildVideoFinderScript() });
const result = await page.evaluate(({ frames, calls }) => {
// Pre-v3.1.0 scoring, kept only as the baseline for this measurement.
function findVideoLegacy(root = document) {
const candidates = window.__koalaCollect(root);
if (!candidates.length) return null;
if (candidates.length === 1) return candidates[0];
let best = null;
let bestScore = -1;
for (const v of candidates) {
if (v.tagName !== 'VIDEO') continue;
const area = (v.videoWidth || v.offsetWidth || 0) * (v.videoHeight || v.offsetHeight || 0);
const score = area + (v.muted ? 0 : 100000)
+ (v.duration && isFinite(v.duration) ? v.duration : 0) * 100;
if (score > bestScore) { bestScore = score; best = v; }
}
return best;
}
window.__koalaCollect = (root) => window.collectVideoCandidates(root, 0, []);
const holder = document.createElement('div');
document.body.appendChild(holder);
for (let i = 0; i < frames; i++) {
const frame = document.createElement('iframe');
frame.style.display = 'none';
holder.appendChild(frame);
}
// globalThis keeps this readable to a Node-configured linter: the body runs
// in the page, where performance is a global.
const clock = globalThis.performance;
const bench = (fn) => {
for (let i = 0; i < 20; i++) fn();
const start = clock.now();
for (let i = 0; i < calls; i++) fn();
return (clock.now() - start) / calls;
};
const current = bench(() => window.__koalaFindVideo());
const legacy = bench(() => findVideoLegacy());
const frameCount = document.querySelectorAll('iframe, frame').length;
holder.remove();
return { frameCount, current, legacy };
}, { frames: FRAMES, calls: CALLS });
console.log(`frames on page: ${result.frameCount}`);
console.log(`shipped findVideo: ${result.current.toFixed(3)} ms/call`);
console.log(`pre-v3.1.0 formula: ${result.legacy.toFixed(3)} ms/call`);
await browser.close();
+1
View File
@@ -10,6 +10,7 @@ import { buildVideoFinderScript } from './helpers/content-source.mjs';
const SCENARIOS = [
{ page: 'simple-player.html', expected: 'player', what: 'the only visible player' },
{ page: 'iframe-player.html', expected: 'framed-player', what: 'a player inside a same-origin frame' },
{ page: 'nested-frame.html', expected: 'framed-player', what: 'a player two frame levels down' },
{ page: 'shadow-player.html', expected: 'shadow-player', what: 'a player inside a shadow root over a light-DOM teaser' },
{ page: 'muted-player.html', expected: 'player', what: 'the only player even when muted' },
{ page: 'hidden-preload.html', expected: 'player', what: 'the visible player over a hidden higher-resolution preload' },
+56
View File
@@ -103,6 +103,62 @@ test('reinjects after the target tab navigates', async ({ context, extensionId,
).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');
});
function FRAMED_VIDEO_PAUSED() {
return document.querySelector('iframe').contentDocument.querySelector('video').paused;
}
@@ -0,0 +1,7 @@
<!doctype html>
<meta charset="utf-8">
<title>Outer</title>
<style>body { margin: 0 }</style>
<!-- Wrapper frame with no video of its own: the player sits one level deeper,
so anything that only walks the top document's frames misses it. -->
<iframe class="inner_player" width="860" height="490" src="player-frame.html"></iframe>
@@ -0,0 +1,7 @@
<!doctype html>
<meta charset="utf-8">
<title>V2</title>
<style>body { margin: 0 }</style>
<!-- The frame's second server/quality choice: a different element, so
re-attaching to it is distinguishable from staying on the old one. -->
<video id="framed-player-2" data-expected width="854" height="480" controls src="../../media/player-1080p-30s.mp4"></video>
@@ -0,0 +1,6 @@
<!doctype html>
<meta charset="utf-8">
<title>Nested player frame</title>
<h1>Player two frame levels down</h1>
<iframe class="outer_frame" width="870" height="500" src="frames/outer-frame.html"></iframe>
<script src="ready.js"></script>
@@ -0,0 +1,9 @@
<!doctype html>
<meta charset="utf-8">
<title>Reloading player frame</title>
<h1>Player frame that swaps its document, like a server or quality switch</h1>
<!-- Nothing in the top document changes when the frame navigates, so the top
document's MutationObserver never fires. Only a load hook on the frame can
notice, which is exactly what this fixture pins down. -->
<iframe class="player_frame" width="860" height="490" src="frames/player-frame.html"></iframe>
<script src="ready.js"></script>