Initial Commit
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user