Files
openmaic/OpenMAIC/tests/audio/audio-player-leak.test.ts
2026-08-16 14:58:47 +08:00

61 lines
1.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock the IndexedDB layer so importing AudioPlayer doesn't pull in Dexie.
const getMock = vi.fn();
vi.mock('@/lib/utils/database', () => ({
db: { audioFiles: { get: getMock } },
}));
/** Stub URL.createObjectURL/revokeObjectURL while keeping `new URL(...)` working. */
function stubObjectUrl() {
const createObjectURL = vi.fn(() => 'blob:fake-url');
const revokeObjectURL = vi.fn();
class URLStub extends URL {}
Object.assign(URLStub, { createObjectURL, revokeObjectURL });
vi.stubGlobal('URL', URLStub);
return { createObjectURL, revokeObjectURL };
}
function stubAudio(play: () => Promise<void>) {
class AudioStub {
play = play;
addEventListener = vi.fn();
pause = vi.fn();
volume = 1;
defaultPlaybackRate = 1;
playbackRate = 1;
src = '';
currentTime = 0;
}
vi.stubGlobal('Audio', AudioStub);
}
describe('AudioPlayer blob URL lifecycle', () => {
beforeEach(() => {
vi.unstubAllGlobals();
getMock.mockReset();
getMock.mockResolvedValue({ blob: new Blob(['audio']) });
});
it('revokes the blob URL when play() rejects (no leak)', async () => {
const { createObjectURL, revokeObjectURL } = stubObjectUrl();
stubAudio(() => Promise.reject(new Error('NotAllowedError')));
const { AudioPlayer } = await import('@/lib/utils/audio-player');
await expect(new AudioPlayer().play('audio-1')).rejects.toThrow();
expect(createObjectURL).toHaveBeenCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledWith('blob:fake-url');
});
it('does not revoke during a successful play() (revocation is deferred to "ended")', async () => {
const { revokeObjectURL } = stubObjectUrl();
stubAudio(() => Promise.resolve());
const { AudioPlayer } = await import('@/lib/utils/audio-player');
await expect(new AudioPlayer().play('audio-1')).resolves.toBe(true);
expect(revokeObjectURL).not.toHaveBeenCalled();
});
});