86 lines
3.7 KiB
JavaScript
86 lines
3.7 KiB
JavaScript
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');
|
||
|
|
});
|