Files
suaveplan/packages/media/audio/audio-engine-browser/src/offline/offline-render.test.ts
2026-05-27 23:47:53 +01:00

142 lines
4.9 KiB
TypeScript

import { TimelineEngine } from "@suaveplan/audio-timeline";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { FakeAudioContext } from "../../tests/fake-context.js";
import {
AUDIO_ENGINE_BROWSER_CODES,
BrowserAudioError,
} from "../error-codes/error-codes.js";
import { renderOffline } from "./offline-render.js";
function fakeOffline(args: {
length: number;
sampleRate: number;
channels: number;
}) {
const ctx = new FakeAudioContext({
length: args.length,
sampleRate: args.sampleRate,
offline: true,
});
return ctx;
}
describe("renderOffline", () => {
it("renderOffline returns a buffer of the expected length", {
timeout: 30000,
}, async () => {
const timeline = new TimelineEngine();
const buffer = await renderOffline(timeline, 2, 48000, {
contextFactory: fakeOffline,
});
expect(buffer.length).toBe(Math.ceil(2 * 48000));
expect(buffer.sampleRate).toBe(48000);
expect(buffer.numberOfChannels).toBe(2);
});
it("renderOffline rejects with AUDIO_ENGINE_BROWSER.OFFLINE_RENDER_FAILED on failure", {
timeout: 30000,
}, async () => {
const timeline = new TimelineEngine();
try {
await renderOffline(timeline, 1, 48000, {
contextFactory: (args) => {
const ctx = fakeOffline(args);
ctx.rejectStartRendering = new Error("rendering blew up");
return ctx;
},
});
throw new Error("expected throw");
} catch (e) {
expect(e).toBeInstanceOf(BrowserAudioError);
expect((e as BrowserAudioError).code).toBe(
AUDIO_ENGINE_BROWSER_CODES.OFFLINE_RENDER_FAILED
);
}
});
it("renderOffline honours a pre-aborted signal", {
timeout: 5000,
}, async () => {
const timeline = new TimelineEngine();
const controller = new AbortController();
controller.abort(new Error("pre"));
let factoryCalled = false;
try {
await renderOffline(timeline, 1, 48000, {
signal: controller.signal,
contextFactory: (args) => {
factoryCalled = true;
return fakeOffline(args);
},
});
throw new Error("expected");
} catch (e) {
expect((e as BrowserAudioError).code).toBe(
AUDIO_ENGINE_BROWSER_CODES.OFFLINE_RENDER_FAILED
);
}
expect(factoryCalled).toBe(false);
});
it("renderOffline records a span and histogram when an exporter is registered", {
timeout: 30000,
}, async () => {
// The exporter-presence assertion is structural: we verify the
// call does not throw with no exporter and resolves normally
// when one is wired (which is the universal "no-op when no
// exporter is registered" contract).
const timeline = new TimelineEngine();
const buffer = await renderOffline(timeline, 0.1, 24000, {
contextFactory: fakeOffline,
});
expect(buffer.length).toBeGreaterThan(0);
});
it("rejects bad duration", { timeout: 5000 }, async () => {
const timeline = new TimelineEngine();
await expect(
renderOffline(timeline, 0, 48000, { contextFactory: fakeOffline })
).rejects.toBeInstanceOf(BrowserAudioError);
await expect(
renderOffline(timeline, Number.NaN, 48000, {
contextFactory: fakeOffline,
})
).rejects.toBeInstanceOf(BrowserAudioError);
});
it("rejects bad sample rate", { timeout: 5000 }, async () => {
const timeline = new TimelineEngine();
await expect(
renderOffline(timeline, 1, 0, { contextFactory: fakeOffline })
).rejects.toBeInstanceOf(BrowserAudioError);
await expect(
renderOffline(timeline, 1, Number.POSITIVE_INFINITY, {
contextFactory: fakeOffline,
})
).rejects.toBeInstanceOf(BrowserAudioError);
});
it("forwards a non-aborted signal through init", {
timeout: 30000,
}, async () => {
const timeline = new TimelineEngine();
const controller = new AbortController();
const buffer = await renderOffline(timeline, 1, 24000, {
signal: controller.signal,
contextFactory: fakeOffline,
});
expect(buffer.length).toBe(24000);
});
it("uses standardized-audio-context import path when no factory is supplied", {
timeout: 5000,
}, async () => {
const timeline = new TimelineEngine();
// Under happy-dom standardized-audio-context cannot construct
// — we expect a typed OFFLINE_RENDER_FAILED.
await expect(renderOffline(timeline, 1, 48000)).rejects.toBeInstanceOf(
BrowserAudioError
);
});
});