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