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
@@ -41,7 +41,7 @@ describe('MJPEGStreamSource', () => {
source.start();
channel.binaryCallback?.(frame());
expect(showFrame).toBeCalledTimes(1);
expect(showFrame).toHaveBeenCalledTimes(1);
const shown = showFrame.mock.calls[0][0] as Blob;
expect(shown).toBeInstanceOf(Blob);
expect(shown.type).toBe('image/jpeg');
@@ -55,7 +55,7 @@ describe('MJPEGStreamSource', () => {
channel.binaryCallback?.(frame());
await flushPromises();
expect(loadedCallback).toBeCalledTimes(1);
expect(loadedCallback).toHaveBeenCalledTimes(1);
});
it('should not report loaded when stopped before the first frame decodes', async () => {
@@ -75,7 +75,7 @@ describe('MJPEGStreamSource', () => {
resolveDecode();
await flushPromises();
expect(loadedCallback).not.toBeCalled();
expect(loadedCallback).not.toHaveBeenCalled();
});
it('should fail on a server error for mjpeg', () => {
@@ -83,7 +83,7 @@ describe('MJPEGStreamSource', () => {
source.start();
channel.receiveMessage({ type: 'error', value: 'mjpeg: stream not found' });
expect(failedCallback).toBeCalledWith('server_error');
expect(failedCallback).toHaveBeenCalledWith('server_error');
});
it('should ignore a server error for another mode', () => {
@@ -91,7 +91,7 @@ describe('MJPEGStreamSource', () => {
source.start();
channel.receiveMessage({ type: 'error', value: 'mse: stream not found' });
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
it('should stop cleanly', () => {
@@ -146,7 +146,7 @@ describe('MJPEGStreamSource', () => {
source.start();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).toBeCalledWith('connect_timeout');
expect(failedCallback).toHaveBeenCalledWith('connect_timeout');
});
it('should not fail once a frame has arrived', () => {
@@ -155,7 +155,7 @@ describe('MJPEGStreamSource', () => {
channel.binaryCallback?.(frame());
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
it('should not fail after stop', () => {
@@ -164,7 +164,7 @@ describe('MJPEGStreamSource', () => {
source.stop();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
});
});
@@ -93,7 +93,7 @@ describe('MP4StreamSource', () => {
channel.binaryCallback?.(frame());
// Second frame reuses the same decoder rather than creating another.
expect(createVideoElement).toBeCalledTimes(1);
expect(createVideoElement).toHaveBeenCalledTimes(1);
});
it('should draw a decoded frame and show it as an image', async () => {
@@ -103,12 +103,12 @@ describe('MP4StreamSource', () => {
decoderVideo.dispatchEvent(new Event('loadeddata'));
await flushPromises();
expect(canvas.context?.drawImage).toBeCalled();
expect(showFrame).toBeCalledTimes(1);
expect(canvas.context?.drawImage).toHaveBeenCalled();
expect(showFrame).toHaveBeenCalledTimes(1);
const shown = showFrame.mock.calls[0][0] as Blob;
expect(shown).toBeInstanceOf(Blob);
expect(shown.type).toBe('image/jpeg');
expect(loadedCallback).toBeCalledTimes(1);
expect(loadedCallback).toHaveBeenCalledTimes(1);
});
it('should report loaded only on the first drawn frame', async () => {
@@ -121,7 +121,7 @@ describe('MP4StreamSource', () => {
decoderVideo.dispatchEvent(new Event('loadeddata'));
await flushPromises();
expect(loadedCallback).toBeCalledTimes(1);
expect(loadedCallback).toHaveBeenCalledTimes(1);
});
it('should not show a frame when the canvas produces no blob', () => {
@@ -131,7 +131,7 @@ describe('MP4StreamSource', () => {
channel.binaryCallback?.(frame());
decoderVideo.dispatchEvent(new Event('loadeddata'));
expect(showFrame).not.toBeCalled();
expect(showFrame).not.toHaveBeenCalled();
});
it('should do nothing when the canvas has no 2d context', () => {
@@ -141,7 +141,7 @@ describe('MP4StreamSource', () => {
channel.binaryCallback?.(frame());
decoderVideo.dispatchEvent(new Event('loadeddata'));
expect(showFrame).not.toBeCalled();
expect(showFrame).not.toHaveBeenCalled();
});
it('should fail on a server error for mp4', () => {
@@ -149,7 +149,7 @@ describe('MP4StreamSource', () => {
source.start();
channel.receiveMessage({ type: 'error', value: 'mp4: stream not found' });
expect(failedCallback).toBeCalledWith('server_error');
expect(failedCallback).toHaveBeenCalledWith('server_error');
});
it('should clear the decoder on stop', () => {
@@ -172,7 +172,7 @@ describe('MP4StreamSource', () => {
// surface.
decoderVideo.dispatchEvent(new Event('loadeddata'));
expect(showFrame).not.toBeCalled();
expect(showFrame).not.toHaveBeenCalled();
});
describe('first-frame timeout', () => {
@@ -181,7 +181,7 @@ describe('MP4StreamSource', () => {
source.start();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).toBeCalledWith('connect_timeout');
expect(failedCallback).toHaveBeenCalledWith('connect_timeout');
});
it('should not fail once a frame has been drawn', () => {
@@ -191,7 +191,7 @@ describe('MP4StreamSource', () => {
decoderVideo.dispatchEvent(new Event('loadeddata'));
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
it('should not fail after stop', () => {
@@ -200,7 +200,7 @@ describe('MP4StreamSource', () => {
source.stop();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
});
@@ -70,14 +70,14 @@ describe('MSEStreamSource', () => {
const { source, failedCallback } = setup({ unsupported: true });
source.start();
expect(failedCallback).toBeCalledWith('unsupported');
expect(failedCallback).toHaveBeenCalledWith('unsupported');
});
it('should attach the media source to the video on start', () => {
const { source, instance, video } = setup();
source.start();
expect(instance.attach).toBeCalledWith(video);
expect(instance.attach).toHaveBeenCalledWith(video);
});
});
@@ -121,7 +121,7 @@ describe('MSEStreamSource', () => {
instance.fireSourceOpen();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).toBeCalledWith('negotiation_timeout');
expect(failedCallback).toHaveBeenCalledWith('negotiation_timeout');
});
it('should not time out after a successful negotiation', () => {
@@ -129,14 +129,14 @@ describe('MSEStreamSource', () => {
negotiate(setupResult);
vi.advanceTimersByTime(5 * 1000);
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
});
it('should create a source buffer in segments mode on negotiation', () => {
const setupResult = setup();
negotiate(setupResult);
expect(setupResult.instance.addSourceBuffer).toBeCalledWith(
expect(setupResult.instance.addSourceBuffer).toHaveBeenCalledWith(
'video/mp4; codecs="avc1.640029,mp4a.40.2"',
);
expect(setupResult.instance.sourceBuffer.mode).toBe('segments');
@@ -148,7 +148,7 @@ describe('MSEStreamSource', () => {
negotiate(setupResult);
setupResult.channel.receiveMessage({ type: 'mse', value: 'video/mp4' });
expect(setupResult.instance.addSourceBuffer).toBeCalledTimes(1);
expect(setupResult.instance.addSourceBuffer).toHaveBeenCalledTimes(1);
});
it('should ignore negotiation responses without a string value', () => {
@@ -156,7 +156,7 @@ describe('MSEStreamSource', () => {
setupResult.source.start();
setupResult.channel.receiveMessage({ type: 'mse', value: 42 });
expect(setupResult.instance.addSourceBuffer).not.toBeCalled();
expect(setupResult.instance.addSourceBuffer).not.toHaveBeenCalled();
});
it('should ignore unrelated messages', () => {
@@ -164,8 +164,8 @@ describe('MSEStreamSource', () => {
setupResult.source.start();
setupResult.channel.receiveMessage({ type: 'webrtc/answer', value: 'sdp' });
expect(setupResult.instance.addSourceBuffer).not.toBeCalled();
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.instance.addSourceBuffer).not.toHaveBeenCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
});
});
@@ -176,12 +176,12 @@ describe('MSEStreamSource', () => {
instance.fireSourceOpen();
channel.receiveMessage({ type: 'error', value: 'mse: stream not found' });
expect(failedCallback).toBeCalledWith('server_error');
expect(failedCallback).toHaveBeenCalledWith('server_error');
// The negotiation timer must have stopped.
failedCallback.mockClear();
vi.advanceTimersByTime(5 * 1000);
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
it('should ignore server errors for other modes', () => {
@@ -189,7 +189,7 @@ describe('MSEStreamSource', () => {
source.start();
channel.receiveMessage({ type: 'error', value: 'webrtc/offer: failed' });
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
it('should ignore server errors without a string value', () => {
@@ -197,7 +197,7 @@ describe('MSEStreamSource', () => {
source.start();
channel.receiveMessage({ type: 'error' });
expect(failedCallback).not.toBeCalled();
expect(failedCallback).not.toHaveBeenCalled();
});
});
@@ -211,7 +211,7 @@ describe('MSEStreamSource', () => {
instance.fireSourceOpen();
channel.receiveMessage({ type: 'mse', value: 'video/mp4' });
expect(failedCallback).toBeCalledWith('media_error');
expect(failedCallback).toHaveBeenCalledWith('media_error');
});
it('should append binary data directly when idle', () => {
@@ -220,7 +220,7 @@ describe('MSEStreamSource', () => {
const data = new ArrayBuffer(8);
setupResult.channel.binaryCallback?.(data);
expect(setupResult.instance.sourceBuffer.appendBuffer).toBeCalledWith(data);
expect(setupResult.instance.sourceBuffer.appendBuffer).toHaveBeenCalledWith(data);
});
it('should swallow direct append failures', () => {
@@ -233,7 +233,7 @@ describe('MSEStreamSource', () => {
expect(() =>
setupResult.channel.binaryCallback?.(new ArrayBuffer(8)),
).not.toThrow();
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
});
it('should stage binary data while the source buffer updates', () => {
@@ -243,12 +243,14 @@ describe('MSEStreamSource', () => {
const staged = new ArrayBuffer(8);
setupResult.channel.binaryCallback?.(staged);
expect(setupResult.instance.sourceBuffer.appendBuffer).not.toBeCalled();
expect(setupResult.instance.sourceBuffer.appendBuffer).not.toHaveBeenCalled();
setupResult.instance.sourceBuffer.updating = false;
setupResult.instance.sourceBuffer.fireUpdateEnd();
expect(setupResult.instance.sourceBuffer.appendBuffer).toBeCalledWith(staged);
expect(setupResult.instance.sourceBuffer.appendBuffer).toHaveBeenCalledWith(
staged,
);
});
it('should stage binary data behind earlier staged data', () => {
@@ -263,7 +265,7 @@ describe('MSEStreamSource', () => {
sourceBuffer.updating = false;
setupResult.channel.binaryCallback?.(second);
expect(sourceBuffer.appendBuffer).not.toBeCalled();
expect(sourceBuffer.appendBuffer).not.toHaveBeenCalled();
sourceBuffer.fireUpdateEnd();
expect(sourceBuffer.appendBuffer).toHaveBeenNthCalledWith(1, first);
@@ -279,10 +281,10 @@ describe('MSEStreamSource', () => {
sourceBuffer.updating = true;
setupResult.channel.binaryCallback?.(new ArrayBuffer(2 * 1024 * 1024));
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
setupResult.channel.binaryCallback?.(new ArrayBuffer(1));
expect(setupResult.failedCallback).toBeCalledWith('buffer_overflow');
expect(setupResult.failedCallback).toHaveBeenCalledWith('buffer_overflow');
});
});
@@ -295,7 +297,7 @@ describe('MSEStreamSource', () => {
sourceBuffer.updating = true;
sourceBuffer.fireUpdateEnd();
expect(sourceBuffer.remove).not.toBeCalled();
expect(sourceBuffer.remove).not.toHaveBeenCalled();
});
it('should do nothing on updateend without buffered content', () => {
@@ -303,8 +305,8 @@ describe('MSEStreamSource', () => {
negotiate(setupResult);
setupResult.instance.sourceBuffer.fireUpdateEnd();
expect(setupResult.instance.sourceBuffer.remove).not.toBeCalled();
expect(setupResult.instance.setLiveSeekableRange).not.toBeCalled();
expect(setupResult.instance.sourceBuffer.remove).not.toHaveBeenCalled();
expect(setupResult.instance.setLiveSeekableRange).not.toHaveBeenCalled();
});
it('should not trim after the media source has closed', () => {
@@ -319,8 +321,8 @@ describe('MSEStreamSource', () => {
setupResult.instance.isOpen.mockReturnValue(false);
sourceBuffer.fireUpdateEnd();
expect(sourceBuffer.remove).not.toBeCalled();
expect(setupResult.instance.setLiveSeekableRange).not.toBeCalled();
expect(sourceBuffer.remove).not.toHaveBeenCalled();
expect(setupResult.instance.setLiveSeekableRange).not.toHaveBeenCalled();
});
it('should trim media behind the retained window', () => {
@@ -332,8 +334,8 @@ describe('MSEStreamSource', () => {
sourceBuffer.fireUpdateEnd();
// Retains the last 15s (end 20 -> retainedStart 5).
expect(sourceBuffer.remove).toBeCalledWith(0, 5);
expect(setupResult.instance.setLiveSeekableRange).toBeCalledWith(5, 20);
expect(sourceBuffer.remove).toHaveBeenCalledWith(0, 5);
expect(setupResult.instance.setLiveSeekableRange).toHaveBeenCalledWith(5, 20);
});
it('should not trim when all media is within the retained window', () => {
@@ -344,7 +346,7 @@ describe('MSEStreamSource', () => {
setupResult.video.currentTime = 19;
sourceBuffer.fireUpdateEnd();
expect(sourceBuffer.remove).not.toBeCalled();
expect(sourceBuffer.remove).not.toHaveBeenCalled();
});
it('should not move the playhead when it falls behind the window', () => {
@@ -464,7 +466,7 @@ describe('MSEStreamSource', () => {
sourceBuffer.fireUpdateEnd();
// Trim still bounds memory, but the playhead and rate are left untouched.
expect(sourceBuffer.remove).toBeCalled();
expect(sourceBuffer.remove).toHaveBeenCalled();
expect(setupResult.video.currentTime).toBe(2);
expect(setupResult.video.playbackRate).toBe(1);
});
@@ -507,7 +509,7 @@ describe('MSEStreamSource', () => {
negotiate(setupResult);
setupResult.video.dispatchEvent(new Event('loadeddata'));
expect(setupResult.loadedCallback).toBeCalledTimes(1);
expect(setupResult.loadedCallback).toHaveBeenCalledTimes(1);
});
it('should fail on video element errors', () => {
@@ -515,7 +517,7 @@ describe('MSEStreamSource', () => {
setupResult.source.start();
setupResult.video.dispatchEvent(new Event('error'));
expect(setupResult.failedCallback).toBeCalledWith('media_error');
expect(setupResult.failedCallback).toHaveBeenCalledWith('media_error');
});
});
@@ -525,19 +527,19 @@ describe('MSEStreamSource', () => {
negotiate(setupResult);
setupResult.source.stop();
expect(setupResult.instance.detach).toBeCalledWith(setupResult.video);
expect(setupResult.instance.detach).toHaveBeenCalledWith(setupResult.video);
expect(setupResult.channel.binaryCallback).toBeNull();
expect(setupResult.channel.getMessageCallbackCount()).toBe(0);
expect(setupResult.instance.getSourceOpenCallbackCount()).toBe(0);
setupResult.video.dispatchEvent(new Event('loadeddata'));
setupResult.video.dispatchEvent(new Event('error'));
expect(setupResult.loadedCallback).not.toBeCalled();
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.loadedCallback).not.toHaveBeenCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
setupResult.instance.sourceBuffer.buffered = createTimeRanges([[0, 20]]);
setupResult.instance.sourceBuffer.fireUpdateEnd();
expect(setupResult.instance.sourceBuffer.remove).not.toBeCalled();
expect(setupResult.instance.sourceBuffer.remove).not.toHaveBeenCalled();
});
it('should stop the negotiation timer on stop', () => {
@@ -547,7 +549,7 @@ describe('MSEStreamSource', () => {
setupResult.source.stop();
vi.advanceTimersByTime(5 * 1000);
expect(setupResult.failedCallback).not.toBeCalled();
expect(setupResult.failedCallback).not.toHaveBeenCalled();
});
it('should tolerate stopping before starting', () => {