Files
vdm/extension/tests/content/media-observer.test.ts
samiandClaude Sonnet 5 03b6253b5a ext: content/ media detection (build order step 7)
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
2026-09-11 12:26:48 +04:00

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();
});
});