Two independent signals feed one panel, per docs/05 §3: - capture/media.ts (background): webRequest-based, recognizes .m3u8/.mpd URLs and the HLS/DASH content types, deduped per (tab, url) so an HLS live-refresh doesn't re-fire. media-bridge.ts relays a hit to the tab's content script over runtime.sendMessage, and separately answers the content script's media.listVariants/media.addVariant calls by forwarding them to the background page's transport. - content/media-observer.ts: watches the page's own <video> elements (present now, added later, or with src changed) for the same extension signal, independent of what the network sniffer saw. Either firing opens content/video-panel.ts's "Download this video ▾" panel (built with createElement, matching the popup's innerHTML-free approach), which lists variants from media.listVariants and greys out any variant.drm or a wholly drmProtected manifest with "Protected content" rather than attempting it. The extension still never parses a manifest itself — that stays in the daemon, one language, one place. content/index.ts is the manifest-registered entry (content_scripts in manifest.json, added in the previous commit); build.mjs builds it as an IIFE rather than ESM, since a manifest content script has no "type": "module" declaration and an emitted top-level export would be a syntax error there. tsconfig.json adds DOM.Iterable for NodeList iteration. docs/05-extension-spec.md gets a short addendum (§8) documenting the popup/options bridge and this media-detection split, since neither was in the original design write-up. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { VideoSrcObserver } from '../../src/content/media-observer.js';
|
|
|
|
describe('VideoSrcObserver', () => {
|
|
it('reports a <video src> already present when start() is called', () => {
|
|
document.body.innerHTML = '<video src="https://cdn.example/master.m3u8"></video>';
|
|
const found: string[] = [];
|
|
const observer = new VideoSrcObserver((url) => found.push(url));
|
|
|
|
observer.start();
|
|
|
|
expect(found).toEqual(['https://cdn.example/master.m3u8']);
|
|
observer.stop();
|
|
});
|
|
|
|
it('ignores a <video> whose src is not a manifest', () => {
|
|
document.body.innerHTML = '<video src="https://cdn.example/movie.mp4"></video>';
|
|
const found: string[] = [];
|
|
const observer = new VideoSrcObserver((url) => found.push(url));
|
|
|
|
observer.start();
|
|
|
|
expect(found).toHaveLength(0);
|
|
observer.stop();
|
|
});
|
|
|
|
it('reports a <video> added to the DOM after start()', async () => {
|
|
document.body.innerHTML = '';
|
|
const found: string[] = [];
|
|
const observer = new VideoSrcObserver((url) => found.push(url));
|
|
observer.start();
|
|
|
|
const video = document.createElement('video');
|
|
video.src = 'https://cdn.example/live.mpd';
|
|
document.body.appendChild(video);
|
|
|
|
await new Promise((r) => setTimeout(r, 0)); // MutationObserver callbacks are microtask-queued
|
|
expect(found).toEqual(['https://cdn.example/live.mpd']);
|
|
observer.stop();
|
|
});
|
|
|
|
it('reports a src attribute change on an existing <video>', async () => {
|
|
document.body.innerHTML = '<video></video>';
|
|
const found: string[] = [];
|
|
const observer = new VideoSrcObserver((url) => found.push(url));
|
|
observer.start();
|
|
|
|
document.querySelector('video')!.setAttribute('src', 'https://cdn.example/switched.m3u8');
|
|
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(found).toEqual(['https://cdn.example/switched.m3u8']);
|
|
observer.stop();
|
|
});
|
|
|
|
it('reports the same URL only once', () => {
|
|
document.body.innerHTML = '<video src="https://cdn.example/master.m3u8"></video><video src="https://cdn.example/master.m3u8"></video>';
|
|
const found: string[] = [];
|
|
const observer = new VideoSrcObserver((url) => found.push(url));
|
|
|
|
observer.start();
|
|
|
|
expect(found).toEqual(['https://cdn.example/master.m3u8']);
|
|
observer.stop();
|
|
});
|
|
});
|