CI / test (pull_request) Successful in 5m41s
Added a per-tree WeakSet visited-guard so the probe terminates. The depth cap alone didn't stop the infinite recursion: window/_parent/_top/frames/_globalObject/_globalProxy are mutually-reachable circular Proxies, so Strategy H's window scan re-walked the same identities along exponentially many paths. The set is declared inside walk (not the outer function) so the separate player trees stay independent. This is what was hanging npm test (exit 124); it now completes in ~15s.
F2 SHOULD-FIX — doc/behavior mismatch (src/background/background.js:299-322)
Reworded the referer-capture comment block. It claimed the referer was read "via onBeforeSendHeaders, which sees the request headers," but the code uses onBeforeRequest + chrome.tabs.get (deriving the referer from the source tab's URL). Now states the actual mechanism and documents why onBeforeSendHeaders+extraHeaders was avoided (extraInfoSpec is rejected by some Chromium builds and fails the whole SW).
F3 SHOULD-FIX — leaked settings file
git rm --cached .claude/settings.local.json and added it to .gitignore. It carried machine-specific absolute paths (/Users/kyle/.claude/...).
F4 FYI — package-lock churn (package-lock.json)
Removed the spurious root "dependencies": { @rollup/rollup-darwin-arm64 } and restored the dev: true / optional: true flags on that platform package so it matches the rest of the @rollup/rollup-* siblings.
F5 FYI — real provider hostnames in test fixtures
Swapped phncdn.com→neutral example.net (registrable-domain sibling test) and thisvid.com→example.com in data_flow/hls_qa/simple_video/referer_proxy test files, rewording comments that named the real sites. src/ and __tests__/ now have zero phncdn/thisvid/pornhub references.
132 lines
6.0 KiB
JavaScript
132 lines
6.0 KiB
JavaScript
// Regression tests: simple <video> / direct-media detection.
|
|
//
|
|
// The feature branch broke detection of pages that expose a plain <video>
|
|
// (e.g. a site that serves a direct MP4 with a TRAILING SLASH and a
|
|
// ?rnd= query: .../14644093/14644093.mp4/?rnd=...). Two site-agnostic fixes
|
|
// are guarded here:
|
|
// 1) The URL predicate (isValidManifestUrl) must let concrete media signals
|
|
// (.mp4/.m3u8/.webm/... and /hls/) WIN over the placeholder/`%`/trailing-
|
|
// slash rejections, so a real direct-media URL is not mistaken for a
|
|
// route template.
|
|
// 2) scrapeTab must fall back to an ISOLATED-world scrape when the MAIN-world
|
|
// scrape yields nothing — the runtime cause of "detects nothing" on such pages
|
|
// is a MAIN-world + allFrames rejection on its ad-iframe-heavy page.
|
|
import { describe, it } from './runner.mjs';
|
|
import { JSDOM } from 'jsdom';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const indexSource = fs.readFileSync(
|
|
path.join(__dirname, '../src/viewer/index.js'),
|
|
'utf-8'
|
|
);
|
|
|
|
// Extract a `function <name>(...) {…}` by balanced-brace scan. Robust to the
|
|
// file's quirky/mixed indentation (a column-0 `}` heuristic would mis-truncate
|
|
// nested or oddly-indented bodies).
|
|
function extractFn(src, name) {
|
|
const re = new RegExp('function ' + name + '\\s*\\(');
|
|
const m = re.exec(src);
|
|
if (!m) throw new Error('not found: ' + name);
|
|
const start = m.index;
|
|
let i = src.indexOf('{', start), depth = 0;
|
|
for (; i < src.length; i++) {
|
|
const c = src[i];
|
|
if (c === '{') depth++;
|
|
else if (c === '}') { depth--; if (depth === 0) return src.slice(start, i + 1); }
|
|
}
|
|
throw new Error('unbalanced: ' + name);
|
|
}
|
|
|
|
// The direct-media URL, shaped exactly like the rejected case (a trailing slash +
|
|
// ?rnd= query). This is the concrete case the predicate used to reject.
|
|
const DIRECT_MP4 =
|
|
'https://example.com/get_file/5/6cae22f8b3ba9d74f10cc2f8d060af738c8d30ddf5/14644000/14644093/14644093.mp4/?rnd=1787811205406';
|
|
|
|
describe('HLS QA: simple <video> direct-media predicate', () => {
|
|
// Extract and evaluate the REAL isValidManifestUrl from index.js in a browser
|
|
// window so its `new URL(...)` / string behavior matches production.
|
|
const isValidSrc = extractFn(indexSource, 'isValidManifestUrl');
|
|
|
|
function assertValid(url, expectValid, label) {
|
|
const dom = new JSDOM(
|
|
'<!DOCTYPE html><html><body></body></html>',
|
|
{ url: 'https://example.com/videos/x/', runScripts: 'dangerously' }
|
|
);
|
|
const got = dom.window.eval('(' + isValidSrc + ')(' + JSON.stringify(url) + ')');
|
|
if (got !== expectValid) {
|
|
throw new Error(
|
|
`${label || 'isValidManifestUrl'}(${url}) = ${got}, expected ${expectValid}`
|
|
);
|
|
}
|
|
}
|
|
|
|
it('accepts a real direct-MP4 URL WITH a trailing slash + ?rnd=', () => {
|
|
// Before the fix, `path.endsWith('/')` rejected this real media source.
|
|
assertValid(DIRECT_MP4, true, 'trailing-slash MP4');
|
|
});
|
|
|
|
it('accepts a direct .mp4 with no trailing slash', () => {
|
|
assertValid('https://cdn.example.com/vids/abc123.mp4', true, 'plain MP4');
|
|
});
|
|
|
|
it('accepts real .m3u8 / .webm / .mpd / .m4s / /hls/ media signals', () => {
|
|
assertValid('https://cdn.example.com/live/master.m3u8', true, 'master.m3u8');
|
|
assertValid('https://cdn.example.com/seg/webm/v.webm', true, 'webm');
|
|
assertValid('https://cdn.example.com/seg/dash/master.mpd', true, 'mpd');
|
|
assertValid('https://cdn.example.com/seg/1-0.m4s', true, 'm4s');
|
|
assertValid('https://cdn.example.com/hls/abc/playlist.m3u8', true, '/hls/');
|
|
});
|
|
|
|
it('does not over-reject a real .m3u8 carrying a valid %2B/%2F token', () => {
|
|
// %2B/%2F are valid percent-encoded hex pairs — a genuine auth token, not a
|
|
// route-template placeholder, so it must survive.
|
|
assertValid('https://cdn.example.com/live/master.m3u8?h=a%2Bb%2Fc&e=1234', true, '%2B token');
|
|
});
|
|
|
|
it('still rejects route-template / nav URLs (no over-acceptance)', () => {
|
|
// The fix must not trade one regression for the other: the route templates
|
|
// a provider's pageContext.langUrls leaks must still be rejected.
|
|
assertValid('https://example.com/playlists/', false, '/playlists/');
|
|
assertValid('https://example.com/playlists/liked/', false, '/playlists/liked/');
|
|
assertValid('https://example.com/playlist/%1%/video/%2%/', false, '/playlist/%1%/');
|
|
assertValid('https://example.com/members/%ID%/playlists/', false, '/members/%ID%/playlists/');
|
|
assertValid('https://example.com/videos/%DIR%/', false, '/videos/%DIR%/');
|
|
});
|
|
});
|
|
|
|
describe('HLS QA: scrapeTab isolated-world fallback', () => {
|
|
// Extract the whole scrapeTab function (balanced braces, indentation-agnostic).
|
|
const scrapeSrc = extractFn(indexSource, 'scrapeTab');
|
|
|
|
it('runs an isolated-world scrape attempt (no `world` key) as a fallback', () => {
|
|
const calls = scrapeSrc.match(/executeScript\(\s*\{[\s\S]*?\}\s*\)/g) || [];
|
|
if (calls.length === 0) {
|
|
throw new Error('scrapeTab has no executeScript call');
|
|
}
|
|
// At least one executeScript must omit `world` so it runs in the always-
|
|
// permitted isolated world (recovers simple videos a MAIN-world injection
|
|
// rejects on sandboxed/opaque-origin ad iframes).
|
|
const hasIsolated = calls.some((c) => !/\bworld\s*:/.test(c));
|
|
if (!hasIsolated) {
|
|
throw new Error(
|
|
'scrapeTab has no isolated-world (no `world:`) executeScript fallback — ' +
|
|
'a MAIN-world+allFrames rejection on an ad-iframe-heavy page leaves 0 videos'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('primary scrape still uses world: \'MAIN\' (reads page globals)', () => {
|
|
const calls = scrapeSrc.match(/executeScript\(\s*\{[\s\S]*?\}\s*\)/g) || [];
|
|
const hasMain = calls.some((c) => /world\s*:\s*['"]MAIN['"]/.test(c));
|
|
if (!hasMain) {
|
|
throw new Error(
|
|
'scrapeTab primary attempt must use world: "MAIN" to read page globals ' +
|
|
'(interceptor data, Hls instances, custom <video> props)'
|
|
);
|
|
}
|
|
});
|
|
});
|