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