const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const root = path.resolve(__dirname, '..'); const castServicePath = path.join(root, 'public', 'js', 'services', 'castService.js'); function loadCastService() { assert.ok(fs.existsSync(castServicePath), 'CastService module should exist at public/js/services/castService.js'); return require(castServicePath); } test('CastService reports Remote Playback availability and prompts through video.remote', async () => { const CastService = loadCastService(); let availabilityCallback; let promptCalls = 0; const video = { remote: { watchAvailability: async callback => { availabilityCallback = callback; callback(true); return 7; }, prompt: async () => { promptCalls += 1; } } }; const service = new CastService(video); await service.watchAvailability(); assert.equal(service.state.available, true); assert.equal(service.state.type, 'remote-playback'); availabilityCallback(false); assert.equal(service.state.available, false); await service.prompt(); assert.equal(promptCalls, 1); }); test('CastService uses AirPlay picker when webkitShowPlaybackTargetPicker is available', async () => { const CastService = loadCastService(); let airplayCalls = 0; const video = { webkitShowPlaybackTargetPicker: () => { airplayCalls += 1; } }; const service = new CastService(video); await service.watchAvailability(); assert.equal(service.state.available, true); assert.equal(service.state.type, 'airplay'); await service.prompt(); assert.equal(airplayCalls, 1); }); test('CastService stays unsupported without native casting APIs', async () => { const CastService = loadCastService(); const service = new CastService({}); await assert.doesNotReject(() => service.watchAvailability()); await assert.doesNotReject(() => service.prompt()); assert.equal(service.state.available, false); assert.equal(service.state.type, 'unsupported'); }); test('Live and Watch players expose cast controls and load CastService', () => { const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8'); assert.match(html, /id="player-cast"/, 'Live player should expose a cast button'); assert.match(html, /id="watch-cast"/, 'Watch player should expose a cast button'); assert.match(html, /\/js\/services\/castService\.js/, 'index.html should load CastService before player pages'); }); test('PWA/service worker cache rules do not precache live stream endpoints', () => { const publicDir = path.join(root, 'public'); const candidates = fs.readdirSync(publicDir) .filter(name => /(?:service-worker|sw|workbox).*\.js$/i.test(name)) .map(name => path.join(publicDir, name)); for (const file of candidates) { const text = fs.readFileSync(file, 'utf8'); assert.doesNotMatch(text, /\/api\/(?:proxy|transcode|live)/, `${path.basename(file)} should not precache stream API routes`); assert.doesNotMatch(text, /\.m3u8|\.ts\b/, `${path.basename(file)} should not precache HLS playlists or segments`); } });