File size: 3,196 Bytes
44948ab | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, cleanup, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createRef } from "react";
import { Visualizer, type VisualizerHandle } from "@/lib/jambuddy/visualizer";
/**
* Regression test for the "one playback at a time" rule.
*
* Each playable waveform owns its own <Audio>. The parent wires onStartPlayback
* to stop any other playback before a new one starts. This pins:
* - clicking a playable waveform's toggle fires onStartPlayback FIRST
* - the exposed stop() handle actually stops the audio and resets the toggle
* So PLAY TOGETHER / another waveform can always silence a playing one.
*/
// Fake Audio so `new Audio(url)` works under happy-dom.
class FakeAudio {
onended: (() => void) | null = null;
paused = true;
played = false;
play() {
this.played = true;
this.paused = false;
return Promise.resolve();
}
pause() {
this.paused = true;
}
}
let fakeAudio: FakeAudio | null = null;
// Fake AudioContext + fetch so the waveform-draw effect completes cleanly.
class FakeAudioContext {
sampleRate = 44100;
close() {
return Promise.resolve();
}
decodeAudioData() {
return Promise.resolve({
numberOfChannels: 1,
sampleRate: 44100,
getChannelData: () => new Float32Array(100),
});
}
}
beforeEach(() => {
fakeAudio = new FakeAudio();
vi.stubGlobal("Audio", class { constructor() { return fakeAudio; } });
vi.stubGlobal("AudioContext", FakeAudioContext);
vi.stubGlobal("fetch", vi.fn(() =>
Promise.resolve({ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)) }),
));
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
describe("Visualizer playable waveform", () => {
it("fires onStartPlayback BEFORE starting play (exclusivity guard)", async () => {
const user = userEvent.setup();
const onStart = vi.fn();
const ref = createRef<VisualizerHandle>();
await act(async () => {
render(
<Visualizer
ref={ref}
audioUrl="blob:take"
playable
onStartPlayback={onStart}
/>,
);
});
await user.click(screen.getByRole("button", { name: "Play" }));
// The owner must be told to stop anything else before this one starts.
expect(onStart).toHaveBeenCalledTimes(1);
expect(fakeAudio?.played).toBe(true);
});
it("exposes a stop() handle that stops audio and flips the toggle back", async () => {
const user = userEvent.setup();
const ref = createRef<VisualizerHandle>();
await act(async () => {
render(<Visualizer ref={ref} audioUrl="blob:take" playable />);
});
await user.click(screen.getByRole("button", { name: "Play" }));
// Now playing -> the toggle reads Stop.
expect(screen.getByRole("button", { name: "Stop playback" })).toBeInTheDocument();
act(() => {
ref.current?.stop();
});
expect(fakeAudio?.paused).toBe(true);
// stop() resets internal state, so the toggle reads Play again.
expect(screen.getByRole("button", { name: "Play" })).toBeInTheDocument();
});
});
|