import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import { test } from 'node:test'; import vm from 'node:vm'; const source = await fs.readFile(new URL('../public/app.js', import.meta.url), 'utf8'); function loadPlayer(metadataFetch) { const nodes = new Map(); const createElement = () => ({ hidden: true, disabled: true, value: '', currentTime: 0, addEventListener() {}, replaceChildren() {}, setAttribute() {}, getContext() { return {}; }, style: { setProperty() {} }, }); const context = vm.createContext({ document: { querySelector(selector) { if (!nodes.has(selector)) nodes.set(selector, createElement()); return nodes.get(selector); }, createElement, addEventListener() {}, }, window: { addEventListener() {}, // Advance polling without waiting in real time. setTimeout(callback) { queueMicrotask(callback); }, }, fetch: async url => url.startsWith('/api/session/') ? metadataFetch(url) : ({ ok: true, json: async () => ({}) }), console, }); vm.runInContext(source, context); vm.runInContext("state.session = { id: 'recorded', metadataStatus: 'pending' };", context); return { nodes, run: code => vm.runInContext(code, context) }; } test('seeking becomes enabled after a slow probe and transient metadata request errors', async () => { let polls = 0; const player = loadPlayer(async () => { polls += 1; assert.ok(polls <= 25, 'polling must stop once duration is available'); if (polls === 1) throw new Error('Temporary network failure'); if (polls === 2) return { ok: false, status: 503 }; const ready = polls === 25; return { ok: true, json: async () => ({ id: 'recorded', metadataStatus: ready ? 'ready' : 'pending', duration: ready ? 7783 : null, seekable: ready, }) }; }); await player.run("refreshSessionMetadata('recorded')"); assert.equal(polls, 25, 'must continue beyond the old twenty-poll limit'); assert.equal(player.nodes.get('#seek').disabled, false); assert.equal(player.nodes.get('#seek').max, '7783'); assert.equal(player.nodes.get('#total-time').textContent, '2:09:43'); }); test('metadata responses from an old stream cannot overwrite a restarted player', async () => { let resolveMetadata; const player = loadPlayer(() => new Promise(resolve => { resolveMetadata = resolve; })); const pending = player.run("refreshSessionMetadata('recorded')"); player.run("state.streamGeneration += 1; state.duration = 60; state.seekable = true; syncPlayhead();"); resolveMetadata({ ok: true, json: async () => ({ metadataStatus: 'unavailable', duration: null, seekable: false }) }); await pending; assert.equal(player.nodes.get('#seek').disabled, false); assert.equal(player.nodes.get('#seek').max, '60'); }); test('metadata polling stops when probing finishes without a duration', async () => { let polls = 0; const player = loadPlayer(async () => { polls += 1; assert.equal(polls, 1); return { ok: true, json: async () => ({ metadataStatus: 'unavailable', duration: null, seekable: false }) }; }); await player.run("refreshSessionMetadata('recorded')"); assert.equal(player.nodes.get('#seek').disabled, true); assert.equal(polls, 1); }); test('a delayed ready message cannot disable seeking after duration is discovered', () => { const player = loadPlayer(() => { throw new Error('Unexpected metadata request'); }); player.run('updateSessionMetadata({ duration: 7783, seekable: true });'); player.run('handleControlMessage(JSON.stringify({ type: "ready", duration: null, seekable: false }));'); assert.equal(player.nodes.get('#seek').disabled, false); assert.equal(player.nodes.get('#seek').max, '7783'); });