import { afterEach, beforeEach, expect, it, vi } from "vitest"; let processor: AudioWorkletProcessor; const port = { onmessage: undefined as ((event: MessageEvent) => void) | undefined, postMessage: vi.fn() }; beforeEach(async () => { vi.resetModules(); port.postMessage.mockClear(); vi.stubGlobal("sampleRate", 48000); vi.stubGlobal( "AudioWorkletProcessor", class { port = port; }, ); vi.stubGlobal("registerProcessor", (_name: string, Processor: new () => AudioWorkletProcessor) => { processor = new Processor(); }); await import("./audio-playback.worklet"); }); afterEach(() => { vi.unstubAllGlobals(); }); function send(data: unknown) { port.onmessage?.(new MessageEvent("message", { data })); } function render(blocks: number) { let audible = false; for (let index = 0; index < blocks; index++) { const output = new Float32Array(128); processor.process([], [[output]]); audible ||= output.some((value) => value !== 0); } return audible; } it("flushes a final chunk shorter than the prebuffer and signals after playback finishes", () => { send({ kind: "audio", samples: new Float32Array(160).fill(0.5), sequence: 1 }); expect(render(1)).toBe(false); expect(port.postMessage).not.toHaveBeenCalled(); send({ kind: "finish", sequence: 1 }); expect(render(1)).toBe(true); expect(port.postMessage).not.toHaveBeenCalled(); render(12); expect(port.postMessage).toHaveBeenCalledExactlyOnceWith({ kind: "drained", sequence: 1 }); }); it("discards cleared samples and identifies the latest playback generation", () => { send({ kind: "audio", samples: new Float32Array(16000).fill(0.5), sequence: 1 }); expect(render(1)).toBe(true); send({ kind: "clear", sequence: 2 }); render(2); expect(port.postMessage).toHaveBeenLastCalledWith({ kind: "drained", sequence: 2 }); expect(render(10)).toBe(false); send({ kind: "audio", samples: new Float32Array(160).fill(0.25), sequence: 3 }); send({ kind: "finish", sequence: 3 }); expect(render(12)).toBe(true); expect(port.postMessage).toHaveBeenLastCalledWith({ kind: "drained", sequence: 3 }); });