feat: add HLS playback support with hls.js and modular viewer architecture
CI / test (pull_request) Successful in 23s
CI / test (pull_request) Successful in 23s
This PR adds end-to-end HLS stream detection, interception, and playback to the viewer. The following root causes from the QA audit are fixed: - Restore dropped HLS detection strategies (6-strategy per-tab query) - Add deep-probe scanning for config globals and iframe manifests - Add standalone interceptors.js for CSP-compliant runtime injection - Fix viewer.js string entry handling for legacy storage entries - Fix manifest.json web_accessible_resources for interceptors.js - Disable modulePreload polyfill for Chrome MV3 compatibility - Run deep probe and scraper in MAIN world for page global access - Fallback to src as HLS manifest when hlsManifest field is missing The viewer.js codebase has been refactored from a monolithic wirePlaybackTracking() into modular single-responsibility functions: PanelState, EventWiring, BufferMonitor, PanelRenderer, AggregateRenderer, PanelFactory. Additionally: - Remove debug console.log statements from production code - Fix dead code in m3u8DeepProbeFn walk() that prevented config global manifest detection - Add eslint config with 141 rules and .eslintignore - Add 44 new tests (83/83 total passing) covering HLS detection, config validation, edge cases, and integration scenarios - Pin HLS.js CDN version to 1.5.17 (not floating/latest) - Add grid-optimized HLS config with 10s buffer cap - Add cleanup hooks for HLS instance destruction on page unload Changed files (18): - src/viewer/viewer.js (+834) - src/viewer/index.js (+870) - src/viewer/viewer.test.js (+976, new) - src/viewer/index.test.js (+481, new) - src/inject/interceptors.js (+169, new) - src/content/content.js (+139) - __tests__/hls_qa.test.mjs (+1168, new) - manifest.json (+6) - vite.config.ts (+5) - __tests__/run.mjs (+2) - __tests__/config_validation.test.mjs (+102, new) - eslint.config.js (+24, new) - .eslintignore (+1, new) - QA_REPORT_HLS.md (+160, new) - __tests__/data_flow.test.mjs (+/-5) - __tests__/schema.test.mjs (+/-5) - __tests__/viewer_integration.test.mjs (+/-4) - src/background/background.js (-2)
This commit is contained in:
1 parent
7416ae8ff9
commit
542c5786be
18 files changed
+4316
-731
No files matched your search
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* config_validation.test.mjs — Validate build configuration integrity.
|
||||
*
|
||||
* Prevents regressions like the phantom entry module issue that kfj001
|
||||
* hit: vite.config.ts referenced probeFn.js and scrapeFn.js but those
|
||||
* files were never created.
|
||||
*/
|
||||
import { describe, it } from './runner.mjs';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..');
|
||||
|
||||
describe('Config: build integrity', () => {
|
||||
it('vite.config.ts entry modules must exist as source files', () => {
|
||||
const viteConfig = readFileSync(resolve(root, 'vite.config.ts'), 'utf8');
|
||||
|
||||
const matches = viteConfig.matchAll(/resolve\(__dirname,\s*'([^']+)'\)/g);
|
||||
const inputEntries = [...matches]
|
||||
.map(m => m[1])
|
||||
.filter(p => p.startsWith('src/'));
|
||||
|
||||
const missing = inputEntries.filter(p => !existsSync(resolve(root, p)));
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`vite.config.ts references ${missing.length} missing file(s): ${missing.join(', ')}`
|
||||
);
|
||||
}
|
||||
if (inputEntries.length === 0) {
|
||||
throw new Error('No entry points found in vite.config.ts');
|
||||
}
|
||||
});
|
||||
|
||||
it('content.js must NOT use inline script injection (textContent)', () => {
|
||||
const contentJs = readFileSync(resolve(root, 'src/content/content.js'), 'utf8');
|
||||
|
||||
// Strip comments before checking for textContent usage
|
||||
const codeOnly = contentJs
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
||||
.replace(/\/\/.*$/gm, ''); // line comments
|
||||
|
||||
if (codeOnly.includes('textContent')) {
|
||||
throw new Error(
|
||||
'content.js uses inline script injection (textContent). ' +
|
||||
'Sites like Pornhub block inline scripts via CSP. Use script.src instead.'
|
||||
);
|
||||
}
|
||||
if (!codeOnly.includes('script.src') || !codeOnly.includes('chrome.runtime.getURL')) {
|
||||
throw new Error(
|
||||
'content.js does not use CSP-safe script.src = chrome.runtime.getURL(...)'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('interceptors.js must be a standalone file', () => {
|
||||
const path = resolve(root, 'src/inject/interceptors.js');
|
||||
if (!existsSync(path)) {
|
||||
throw new Error('src/inject/interceptors.js does not exist');
|
||||
}
|
||||
const content = readFileSync(path, 'utf8');
|
||||
if (!content.includes('(function ()') || !content.includes('})();')) {
|
||||
throw new Error('interceptors.js should be a self-contained IIFE');
|
||||
}
|
||||
});
|
||||
|
||||
it('manifest.json content_scripts must reference content.js', () => {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(resolve(root, 'manifest.json'), 'utf8')
|
||||
);
|
||||
const cs = manifest.content_scripts;
|
||||
if (!cs || !Array.isArray(cs)) {
|
||||
throw new Error('manifest.json has no content_scripts');
|
||||
}
|
||||
const hasContentScript = cs.some(s => s.js && s.js.includes('content.js'));
|
||||
if (!hasContentScript) {
|
||||
throw new Error('No content_scripts entry references content.js');
|
||||
}
|
||||
});
|
||||
|
||||
it('manifest.json must declare interceptors.js in web_accessible_resources', () => {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(resolve(root, 'manifest.json'), 'utf8')
|
||||
);
|
||||
const war = manifest.web_accessible_resources;
|
||||
if (!war || !Array.isArray(war)) {
|
||||
throw new Error(
|
||||
'manifest.json has no web_accessible_resources. ' +
|
||||
'interceptors.js needs this for chrome.runtime.getURL() to work.'
|
||||
);
|
||||
}
|
||||
const allResources = war.flatMap(entry => entry.resources || []);
|
||||
if (!allResources.includes('interceptors.js')) {
|
||||
throw new Error(
|
||||
'manifest.json web_accessible_resources must include interceptors.js. ' +
|
||||
'Without it, the browser blocks chrome.runtime.getURL("interceptors.js").'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user