Files
ytplayer/frontend/audio-graph.test.js
2026-10-09 19:46:00 +08:00

76 lines
3.9 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { join } = require('node:path');
const AudioGraph = require('./audio-graph');
test('AudioGraph creates its context only on demand and retains the first context options', () => {
let contexts = 0, optionsSeen;
class Context {
constructor(options) { contexts++; optionsSeen = options; this.sampleRate = options?.sampleRate || 44100; }
}
const graph = AudioGraph.create({ getConstructor: () => Context });
assert.equal(graph.snapshot().contextCreated, false);
const first = graph.context({ sampleRate: 48000 });
assert.equal(graph.context({ sampleRate: 44100 }), first);
assert.equal(contexts, 1);
assert.deepEqual(optionsSeen, { sampleRate: 48000 });
assert.equal(first.sampleRate, 48000);
});
test('one retained MediaElementSource per element can change insert routes without recreation', () => {
const made = [], connects = [], disconnects = [];
class Context {
constructor() { this.sampleRate = 48000; }
createMediaElementSource(element) {
const source = { element, connect: target => connects.push([element.id, target.id]), disconnect: () => disconnects.push(element.id) };
made.push(source); return source;
}
}
const graph = AudioGraph.create({ getConstructor: () => Context });
const a = { id: 'video' }, b = { id: 'audio' }, standard = { id: 'standard' }, dsp = { id: 'dsp' };
const original = graph.connect(a, standard);
graph.connect(a, standard);
graph.routeAll([a, b], dsp);
graph.routeAll([a, b], standard);
assert.equal(made.length, 2);
assert.equal(graph.source(a), original);
assert.deepEqual(connects, [['video', 'standard'], ['video', 'dsp'], ['audio', 'dsp'], ['video', 'standard'], ['audio', 'standard']]);
assert.deepEqual(disconnects, ['video', 'video', 'audio']);
assert.deepEqual(graph.snapshot(), { contextCreated: true, sampleRate: 48000, sourceCount: 2, failedCount: 0, routedCount: 2 });
});
test('a failed source creation is remembered and is never attempted twice', () => {
let attempts = 0;
class Context {
constructor() { this.sampleRate = 48000; }
createMediaElementSource() { attempts++; throw new Error('already sourced'); }
}
const graph = AudioGraph.create({ getConstructor: () => Context }), element = {};
assert.throws(() => graph.source(element), /already sourced/);
assert.throws(() => graph.source(element), /already sourced/);
assert.equal(attempts, 1);
assert.deepEqual(graph.snapshot(), { contextCreated: true, sampleRate: 48000, sourceCount: 0, failedCount: 1, routedCount: 0 });
});
test('the extracted Standard EQ build block is identical to the original except for context ownership', () => {
const source = readFileSync(join(__dirname, 'app.js'), 'utf8');
const original = readFileSync(join(__dirname, '..', 'tests', 'fixtures', 'standard-eq-build.txt'), 'utf8').trimEnd();
const start = source.indexOf(' function build() {', source.indexOf('const EQ = (() => {'));
const end = source.indexOf('\n function attach(el)', start);
assert.ok(start >= 0 && end > start, 'EQ build block exists');
const current = source.slice(start, end).trimEnd();
assert.equal(current, original.replace('st.ctx = new AC();', 'st.ctx = AudioGraph.context();'));
assert.match(source, /AudioGraph\.connect\(el, st\.filters\[0\]\)/);
assert.doesNotMatch(source, /\.createMediaElementSource\(/);
});
test('AudioGraph is a core runtime asset and is loaded before the lazy app', () => {
const { groups } = JSON.parse(readFileSync(join(__dirname, 'assets.json'), 'utf8'));
assert.ok(groups.core.files.includes('/audio-graph.js'));
const html = readFileSync(join(__dirname, 'index.html'), 'utf8');
assert.ok(html.indexOf('src="audio-graph.js"') < html.indexOf('src="section-rail.js"'));
const embedded = JSON.parse(html.match(/id="ytp-assets">([^<]+)<\/script>/)[1]);
assert.deepEqual(embedded.groups, groups);
});