Files
carplay/test/metadata-ui.test.js
T

86 lines
3.7 KiB
JavaScript
Raw Normal View History

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');
});