test: Split test utilities to improve test times (#2622)

This commit is contained in:
Dermot Duffy
2026-07-26 16:29:53 -07:00
committed by GitHub
parent 8d44fcecf1
commit ad89aa9538
234 changed files with 2730 additions and 2516 deletions
@@ -19,15 +19,15 @@ describe('ArpeggioTone', () => {
// G5 (783.99) at t=0.
expect(audio.oscillators[1].frequency.value).toBe(783.99);
expect(audio.oscillators[1].start).toBeCalledWith(0);
expect(audio.oscillators[1].start).toHaveBeenCalledWith(0);
// E5 (659.25) at t=0.25.
expect(audio.oscillators[4].frequency.value).toBe(659.25);
expect(audio.oscillators[4].start).toBeCalledWith(0.25);
expect(audio.oscillators[4].start).toHaveBeenCalledWith(0.25);
// C5 (523.25) at t=0.5.
expect(audio.oscillators[7].frequency.value).toBe(523.25);
expect(audio.oscillators[7].start).toBeCalledWith(0.5);
expect(audio.oscillators[7].start).toHaveBeenCalledWith(0.5);
});
it('should use the lighter PLUCK envelope for every strike', () => {
@@ -36,15 +36,15 @@ describe('ArpeggioTone', () => {
// Sparkle / fundamental / hum peaks for every strike.
for (let strike = 0; strike < 3; strike++) {
const i = strike * 3;
expect(audio.gainParams[i].linearRampToValueAtTime).toBeCalledWith(
expect(audio.gainParams[i].linearRampToValueAtTime).toHaveBeenCalledWith(
0.05,
expect.any(Number),
);
expect(audio.gainParams[i + 1].linearRampToValueAtTime).toBeCalledWith(
expect(audio.gainParams[i + 1].linearRampToValueAtTime).toHaveBeenCalledWith(
0.13,
expect.any(Number),
);
expect(audio.gainParams[i + 2].linearRampToValueAtTime).toBeCalledWith(
expect(audio.gainParams[i + 2].linearRampToValueAtTime).toHaveBeenCalledWith(
0.04,
expect.any(Number),
);
@@ -18,7 +18,7 @@ describe('start', () => {
it('should construct an AudioContext and play one iteration', () => {
new ChimeTone(0).start();
expect(audio.audioContextCtor).toBeCalledTimes(1);
expect(audio.audioContextCtor).toHaveBeenCalledTimes(1);
expect(audio.oscillators).toHaveLength(ITERATION_OSCILLATORS);
});
@@ -28,7 +28,7 @@ describe('start', () => {
tone.start();
tone.start();
expect(audio.audioContextCtor).toBeCalledTimes(1);
expect(audio.audioContextCtor).toHaveBeenCalledTimes(1);
expect(audio.oscillators).toHaveLength(ITERATION_OSCILLATORS);
});
@@ -40,7 +40,7 @@ describe('start', () => {
new ChimeTone(0).start(onFinished);
expect(onFinished).toBeCalled();
expect(onFinished).toHaveBeenCalled();
expect(audio.oscillators).toHaveLength(0);
});
});
@@ -52,7 +52,7 @@ describe('stop', () => {
tone.stop();
expect(audio.audioContext.close).toBeCalled();
expect(audio.audioContext.close).toHaveBeenCalled();
});
it('should not fire finishedHandler on external stop', () => {
@@ -62,7 +62,7 @@ describe('stop', () => {
tone.stop();
expect(onFinished).not.toBeCalled();
expect(onFinished).not.toHaveBeenCalled();
});
it('should swallow AudioContext.close rejections silently', () => {
@@ -91,7 +91,7 @@ describe('repeat counter', () => {
vi.advanceTimersByTime(ITERATION_INTERVAL_MS);
expect(audio.oscillators).toHaveLength(ITERATION_OSCILLATORS * i);
}
expect(audio.audioContext.close).not.toBeCalled();
expect(audio.audioContext.close).not.toHaveBeenCalled();
});
it('should play exactly `repeat` iterations and then finish', () => {
@@ -110,8 +110,8 @@ describe('repeat counter', () => {
// decay tail, then fires the finished handler and stops.
vi.advanceTimersByTime(ITERATION_INTERVAL_MS);
expect(audio.oscillators).toHaveLength(ITERATION_OSCILLATORS * 3);
expect(onFinished).toBeCalledTimes(1);
expect(audio.audioContext.close).toBeCalled();
expect(onFinished).toHaveBeenCalledTimes(1);
expect(audio.audioContext.close).toHaveBeenCalled();
});
it('should not fire finishedHandler when stopped mid-sequence', () => {
@@ -124,6 +124,6 @@ describe('repeat counter', () => {
// Even if any stale scheduled work fires, finishedHandler stays silent.
vi.advanceTimersByTime(ITERATION_INTERVAL_MS * 10);
expect(onFinished).not.toBeCalled();
expect(onFinished).not.toHaveBeenCalled();
});
});
+30 -18
View File
@@ -21,41 +21,53 @@ describe('ChimeTone', () => {
expect(audio.oscillators[0].frequency.value).toBe(622.25 * 2);
expect(audio.oscillators[1].frequency.value).toBe(622.25);
expect(audio.oscillators[2].frequency.value).toBe(622.25 / 2);
expect(audio.oscillators[0].start).toBeCalledWith(0);
expect(audio.oscillators[1].start).toBeCalledWith(0);
expect(audio.oscillators[2].start).toBeCalledWith(0);
expect(audio.oscillators[0].start).toHaveBeenCalledWith(0);
expect(audio.oscillators[1].start).toHaveBeenCalledWith(0);
expect(audio.oscillators[2].start).toHaveBeenCalledWith(0);
// DOOOOONG -- B4 (493.88Hz) at t=0.5.
expect(audio.oscillators[3].frequency.value).toBe(493.88 * 2);
expect(audio.oscillators[4].frequency.value).toBe(493.88);
expect(audio.oscillators[5].frequency.value).toBe(493.88 / 2);
expect(audio.oscillators[3].start).toBeCalledWith(0.5);
expect(audio.oscillators[4].start).toBeCalledWith(0.5);
expect(audio.oscillators[5].start).toBeCalledWith(0.5);
expect(audio.oscillators[3].start).toHaveBeenCalledWith(0.5);
expect(audio.oscillators[4].start).toHaveBeenCalledWith(0.5);
expect(audio.oscillators[5].start).toHaveBeenCalledWith(0.5);
});
it('should give DING a brighter, shorter bell envelope', () => {
new ChimeTone(0).start();
// Sparkle / fundamental / hum peaks for DING.
expect(audio.gainParams[0].linearRampToValueAtTime).toBeCalledWith(0.1, 0.005);
expect(audio.gainParams[1].linearRampToValueAtTime).toBeCalledWith(0.22, 0.005);
expect(audio.gainParams[2].linearRampToValueAtTime).toBeCalledWith(0.08, 0.005);
expect(audio.gainParams[0].linearRampToValueAtTime).toHaveBeenCalledWith(0.1, 0.005);
expect(audio.gainParams[1].linearRampToValueAtTime).toHaveBeenCalledWith(
0.22,
0.005,
);
expect(audio.gainParams[2].linearRampToValueAtTime).toHaveBeenCalledWith(
0.08,
0.005,
);
// Decay constants (sparkle fades fastest, hum lingers).
expect(audio.gainParams[0].setTargetAtTime).toBeCalledWith(0, 0.005, 0.3);
expect(audio.gainParams[1].setTargetAtTime).toBeCalledWith(0, 0.005, 0.8);
expect(audio.gainParams[2].setTargetAtTime).toBeCalledWith(0, 0.005, 1.2);
expect(audio.gainParams[0].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 0.3);
expect(audio.gainParams[1].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 0.8);
expect(audio.gainParams[2].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 1.2);
});
it('should give DOOOOONG a fuller, longer bell envelope', () => {
new ChimeTone(0).start();
expect(audio.gainParams[3].linearRampToValueAtTime).toBeCalledWith(0.11, 0.505);
expect(audio.gainParams[4].linearRampToValueAtTime).toBeCalledWith(0.28, 0.505);
expect(audio.gainParams[5].linearRampToValueAtTime).toBeCalledWith(0.1, 0.505);
expect(audio.gainParams[3].setTargetAtTime).toBeCalledWith(0, 0.505, 0.5);
expect(audio.gainParams[4].setTargetAtTime).toBeCalledWith(0, 0.505, 1.3);
expect(audio.gainParams[5].setTargetAtTime).toBeCalledWith(0, 0.505, 1.8);
expect(audio.gainParams[3].linearRampToValueAtTime).toHaveBeenCalledWith(
0.11,
0.505,
);
expect(audio.gainParams[4].linearRampToValueAtTime).toHaveBeenCalledWith(
0.28,
0.505,
);
expect(audio.gainParams[5].linearRampToValueAtTime).toHaveBeenCalledWith(0.1, 0.505);
expect(audio.gainParams[3].setTargetAtTime).toHaveBeenCalledWith(0, 0.505, 0.5);
expect(audio.gainParams[4].setTargetAtTime).toHaveBeenCalledWith(0, 0.505, 1.3);
expect(audio.gainParams[5].setTargetAtTime).toHaveBeenCalledWith(0, 0.505, 1.8);
});
describe('with fake timers', () => {
+16 -16
View File
@@ -59,14 +59,14 @@ describe('start', () => {
it('should construct an Audio element with the configured URL', () => {
new CustomTone('http://example/ring.mp3', 0).start();
expect(audio.ctor).toBeCalledWith('http://example/ring.mp3');
expect(audio.ctor).toHaveBeenCalledWith('http://example/ring.mp3');
});
it('should loop indefinitely when repeat is 0', () => {
new CustomTone('http://example/ring.mp3', 0).start();
expect(audio.instances[0].loop).toBe(true);
expect(audio.instances[0].play).toBeCalled();
expect(audio.instances[0].play).toHaveBeenCalled();
});
it('should re-play (not loop natively) when repeat is finite', () => {
@@ -76,14 +76,14 @@ describe('start', () => {
// Observable proof the 'ended' listener was registered: dispatching the
// event triggers a second play().
audio.instances[0].dispatchEvent(new Event('ended'));
expect(audio.instances[0].play).toBeCalledTimes(2);
expect(audio.instances[0].play).toHaveBeenCalledTimes(2);
});
it('should reset currentTime before play', () => {
new CustomTone('http://example/ring.mp3', 0).start();
expect(audio.instances[0].currentTime).toBe(0);
expect(audio.instances[0].play).toBeCalled();
expect(audio.instances[0].play).toHaveBeenCalled();
});
it('should no-op when called twice without stop', () => {
@@ -92,7 +92,7 @@ describe('start', () => {
tone.start();
tone.start();
expect(audio.ctor).toBeCalledTimes(1);
expect(audio.ctor).toHaveBeenCalledTimes(1);
});
it('should fire finishedHandler when Audio construction throws', () => {
@@ -103,7 +103,7 @@ describe('start', () => {
new CustomTone('http://example/ring.mp3', 0).start(onFinished);
expect(onFinished).toBeCalled();
expect(onFinished).toHaveBeenCalled();
});
it('should fire finishedHandler when play() rejects (e.g. autoplay block)', async () => {
@@ -116,7 +116,7 @@ describe('start', () => {
await flushPromises();
expect(onFinished).toBeCalled();
expect(onFinished).toHaveBeenCalled();
});
});
@@ -128,15 +128,15 @@ describe('repeat counter', () => {
// Iteration 1 already started by `start()`. Two more 'ended' events
// should re-play, and the third 'ended' should finish.
expect(audio.instances[0].play).toBeCalledTimes(1);
expect(audio.instances[0].play).toHaveBeenCalledTimes(1);
audio.instances[0].dispatchEvent(new Event('ended'));
expect(audio.instances[0].play).toBeCalledTimes(2);
expect(audio.instances[0].play).toHaveBeenCalledTimes(2);
audio.instances[0].dispatchEvent(new Event('ended'));
expect(audio.instances[0].play).toBeCalledTimes(3);
expect(onFinished).not.toBeCalled();
expect(audio.instances[0].play).toHaveBeenCalledTimes(3);
expect(onFinished).not.toHaveBeenCalled();
audio.instances[0].dispatchEvent(new Event('ended'));
expect(onFinished).toBeCalledTimes(1);
expect(onFinished).toHaveBeenCalledTimes(1);
});
it('should ignore ended events after stop', () => {
@@ -150,7 +150,7 @@ describe('repeat counter', () => {
// no-op as far as the source is concerned.
element.dispatchEvent(new Event('ended'));
expect(onFinished).not.toBeCalled();
expect(onFinished).not.toHaveBeenCalled();
});
});
@@ -162,11 +162,11 @@ describe('stop', () => {
tone.stop();
expect(element.pause).toBeCalled();
expect(element.pause).toHaveBeenCalled();
// Confirm the 'ended' listener is gone: dispatching it must not re-play.
vi.mocked(HTMLMediaElement.prototype.play).mockClear();
element.dispatchEvent(new Event('ended'));
expect(element.play).not.toBeCalled();
expect(element.play).not.toHaveBeenCalled();
});
it('should not fire finishedHandler on external stop', () => {
@@ -176,7 +176,7 @@ describe('stop', () => {
tone.stop();
expect(onFinished).not.toBeCalled();
expect(onFinished).not.toHaveBeenCalled();
});
it('should be safe to call before start', () => {
+12 -12
View File
@@ -22,8 +22,8 @@ describe('MelodyTone', () => {
expect(audio.oscillators[2].frequency.value).toBe(659.25);
expect(audio.oscillators[3].frequency.value).toBe(783.99);
expect(audio.oscillators[4].frequency.value).toBe(261.63);
expect(audio.oscillators[0].start).toBeCalledWith(0);
expect(audio.oscillators[4].start).toBeCalledWith(0);
expect(audio.oscillators[0].start).toHaveBeenCalledWith(0);
expect(audio.oscillators[4].start).toHaveBeenCalledWith(0);
// --- V chord (G major) at t=1: sparkle D6, G4 + B4 + D5, hum G3. ---
expect(audio.oscillators[5].frequency.value).toBe(1174.66);
@@ -31,8 +31,8 @@ describe('MelodyTone', () => {
expect(audio.oscillators[7].frequency.value).toBe(493.88);
expect(audio.oscillators[8].frequency.value).toBe(587.33);
expect(audio.oscillators[9].frequency.value).toBe(196.0);
expect(audio.oscillators[5].start).toBeCalledWith(1);
expect(audio.oscillators[9].start).toBeCalledWith(1);
expect(audio.oscillators[5].start).toHaveBeenCalledWith(1);
expect(audio.oscillators[9].start).toHaveBeenCalledWith(1);
// --- I chord (resolution, an octave higher) at t=2. ---
expect(audio.oscillators[10].frequency.value).toBe(2093.0);
@@ -40,21 +40,21 @@ describe('MelodyTone', () => {
expect(audio.oscillators[12].frequency.value).toBe(783.99);
expect(audio.oscillators[13].frequency.value).toBe(1046.5);
expect(audio.oscillators[14].frequency.value).toBe(329.63);
expect(audio.oscillators[10].start).toBeCalledWith(2);
expect(audio.oscillators[14].start).toBeCalledWith(2);
expect(audio.oscillators[10].start).toHaveBeenCalledWith(2);
expect(audio.oscillators[14].start).toHaveBeenCalledWith(2);
});
it('should give the resolving chord a longer tail than the I and V chords', () => {
new MelodyTone(0).start();
// I and V chords use default fundDecay=0.6, humDecay=1.1.
expect(audio.gainParams[1].setTargetAtTime).toBeCalledWith(0, 0.005, 0.6);
expect(audio.gainParams[4].setTargetAtTime).toBeCalledWith(0, 0.005, 1.1);
expect(audio.gainParams[6].setTargetAtTime).toBeCalledWith(0, 1.005, 0.6);
expect(audio.gainParams[9].setTargetAtTime).toBeCalledWith(0, 1.005, 1.1);
expect(audio.gainParams[1].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 0.6);
expect(audio.gainParams[4].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 1.1);
expect(audio.gainParams[6].setTargetAtTime).toHaveBeenCalledWith(0, 1.005, 0.6);
expect(audio.gainParams[9].setTargetAtTime).toHaveBeenCalledWith(0, 1.005, 1.1);
// Final I chord overrides to fundDecay=0.9, humDecay=1.4.
expect(audio.gainParams[11].setTargetAtTime).toBeCalledWith(0, 2.005, 0.9);
expect(audio.gainParams[14].setTargetAtTime).toBeCalledWith(0, 2.005, 1.4);
expect(audio.gainParams[11].setTargetAtTime).toHaveBeenCalledWith(0, 2.005, 0.9);
expect(audio.gainParams[14].setTargetAtTime).toHaveBeenCalledWith(0, 2.005, 1.4);
});
});
@@ -19,30 +19,38 @@ describe('WestminsterTone', () => {
// E5 (659.25) at t=0.
expect(audio.oscillators[1].frequency.value).toBe(659.25);
expect(audio.oscillators[1].start).toBeCalledWith(0);
expect(audio.oscillators[1].start).toHaveBeenCalledWith(0);
// D5 (587.33) at t=0.55.
expect(audio.oscillators[4].frequency.value).toBe(587.33);
expect(audio.oscillators[4].start).toBeCalledWith(0.55);
expect(audio.oscillators[4].start).toHaveBeenCalledWith(0.55);
// C5 (523.25) at t=1.1.
expect(audio.oscillators[7].frequency.value).toBe(523.25);
expect(audio.oscillators[7].start).toBeCalledWith(1.1);
expect(audio.oscillators[7].start).toHaveBeenCalledWith(1.1);
// G4 (392.0) at t=1.65 -- the resolution.
expect(audio.oscillators[10].frequency.value).toBe(392.0);
expect(audio.oscillators[10].start).toBeCalledWith(1.65);
expect(audio.oscillators[10].start).toHaveBeenCalledWith(1.65);
});
it('should give the resolving G4 a longer bell tail than the other strikes', () => {
new WestminsterTone(0).start();
// First three strikes use default decay (fundDecay=0.6, humDecay=1.0).
expect(audio.gainParams[1].setTargetAtTime).toBeCalledWith(0, 0.005, 0.6);
expect(audio.gainParams[2].setTargetAtTime).toBeCalledWith(0, 0.005, 1.0);
expect(audio.gainParams[1].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 0.6);
expect(audio.gainParams[2].setTargetAtTime).toHaveBeenCalledWith(0, 0.005, 1.0);
// G4 (final strike) overrides to fundDecay=0.9, humDecay=1.4.
expect(audio.gainParams[10].setTargetAtTime).toBeCalledWith(0, 1.65 + 0.005, 0.9);
expect(audio.gainParams[11].setTargetAtTime).toBeCalledWith(0, 1.65 + 0.005, 1.4);
expect(audio.gainParams[10].setTargetAtTime).toHaveBeenCalledWith(
0,
1.65 + 0.005,
0.9,
);
expect(audio.gainParams[11].setTargetAtTime).toHaveBeenCalledWith(
0,
1.65 + 0.005,
1.4,
);
});
});