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
@@ -87,7 +87,7 @@ describe('media-source', () => {
const instance = createBrowserMediaSource();
expect(instance?.isTypeSupported('video/mp4')).toBe(false);
expect(FakeManagedMediaSource.isTypeSupported).toBeCalledWith('video/mp4');
expect(FakeManagedMediaSource.isTypeSupported).toHaveBeenCalledWith('video/mp4');
});
});
@@ -99,7 +99,7 @@ describe('media-source', () => {
const video = document.createElement('video');
instance?.attach(video);
expect(createObjectURL).toBeCalledTimes(1);
expect(createObjectURL).toHaveBeenCalledTimes(1);
expect(video.src).toContain('blob:fake-url');
expect(video.srcObject).toBeNull();
});
@@ -111,15 +111,15 @@ describe('media-source', () => {
const video = document.createElement('video');
instance?.attach(video);
expect(revokeObjectURL).not.toBeCalled();
expect(revokeObjectURL).not.toHaveBeenCalled();
FakeMediaSource.instances[0].dispatchEvent(new Event('sourceopen'));
expect(revokeObjectURL).toBeCalledWith('blob:fake-url');
expect(revokeObjectURL).toHaveBeenCalledWith('blob:fake-url');
instance?.detach(video);
expect(revokeObjectURL).toBeCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledTimes(1);
});
it('should delegate isTypeSupported to MediaSource', () => {
@@ -129,7 +129,7 @@ describe('media-source', () => {
const instance = createBrowserMediaSource();
expect(instance?.isTypeSupported('video/mp4')).toBe(false);
expect(FakeMediaSource.isTypeSupported).toBeCalledWith('video/mp4');
expect(FakeMediaSource.isTypeSupported).toHaveBeenCalledWith('video/mp4');
});
it('should detach by clearing src and revoking the object URL', () => {
@@ -140,7 +140,7 @@ describe('media-source', () => {
instance?.attach(video);
instance?.detach(video);
expect(revokeObjectURL).toBeCalledWith('blob:fake-url');
expect(revokeObjectURL).toHaveBeenCalledWith('blob:fake-url');
expect(video.getAttribute('src')).toBe('');
});
@@ -153,7 +153,7 @@ describe('media-source', () => {
instance?.detach(video);
instance?.detach(video);
expect(revokeObjectURL).toBeCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledTimes(1);
});
});
@@ -170,12 +170,12 @@ describe('media-source', () => {
const mediaSource = video.srcObject;
assert(mediaSource instanceof FakeManagedMediaSource);
mediaSource.dispatchEvent(new Event('sourceopen'));
expect(callback).toBeCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
unsubscribe?.();
mediaSource.dispatchEvent(new Event('sourceopen'));
expect(callback).toBeCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should delegate addSourceBuffer', () => {
@@ -188,7 +188,7 @@ describe('media-source', () => {
const mediaSource = video.srcObject;
assert(mediaSource instanceof FakeManagedMediaSource);
expect(mediaSource.addSourceBuffer).toBeCalledWith(
expect(mediaSource.addSourceBuffer).toHaveBeenCalledWith(
'video/mp4; codecs="avc1.640029"',
);
});
@@ -203,7 +203,7 @@ describe('media-source', () => {
const mediaSource = video.srcObject;
assert(mediaSource instanceof FakeManagedMediaSource);
expect(mediaSource.setLiveSeekableRange).toBeCalledWith(10, 20);
expect(mediaSource.setLiveSeekableRange).toHaveBeenCalledWith(10, 20);
});
it('should report open only while the media source readyState is open', () => {
@@ -72,10 +72,10 @@ describe('ImageSurfaceController', () => {
await controller.showFrame(createFrame());
expect(decoder.decode).toBeCalledTimes(1);
expect(URL.createObjectURL).toBeCalledTimes(1);
expect(decoder.decode).toHaveBeenCalledTimes(1);
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
expect(image.getAttribute('src')).toBe(createdURLs()[0]);
expect(URL.revokeObjectURL).not.toBeCalled();
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
});
it('should revoke the previous frame when showing the next', async () => {
@@ -85,8 +85,8 @@ describe('ImageSurfaceController', () => {
await controller.showFrame(createFrame());
expect(image.getAttribute('src')).toBe(createdURLs()[1]);
expect(URL.revokeObjectURL).toBeCalledTimes(1);
expect(URL.revokeObjectURL).toBeCalledWith(createdURLs()[0]);
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(createdURLs()[0]);
});
it('should keep showing the previous frame until the next has decoded', async () => {
@@ -110,13 +110,13 @@ describe('ImageSurfaceController', () => {
// The next frame is still decoding off-DOM, so the visible <img> is
// untouched and the previous URL stays valid (revoking it would blank it).
expect(image.getAttribute('src')).toBe(createdURLs()[0]);
expect(URL.revokeObjectURL).not.toBeCalled();
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
releaseSecond();
await flushPromises();
expect(image.getAttribute('src')).toBe(createdURLs()[1]);
expect(URL.revokeObjectURL).toBeCalledWith(createdURLs()[0]);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(createdURLs()[0]);
});
it('should present only the newest frame while a decode is in flight', async () => {
@@ -130,13 +130,13 @@ describe('ImageSurfaceController', () => {
await flushPromises();
// Only the first frame's decode has started; the rest wait behind it.
expect(URL.createObjectURL).toBeCalledTimes(1);
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
releaseDecode();
await flushPromises();
// The middle frame was superseded, so only the newest is presented next.
expect(URL.createObjectURL).toBeCalledTimes(2);
expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
expect(image.getAttribute('src')).toBe(createdURLs()[1]);
});
@@ -148,7 +148,7 @@ describe('ImageSurfaceController', () => {
await controller.showFrame(createFrame());
expect(image.hasAttribute('src')).toBe(false);
expect(URL.revokeObjectURL).toBeCalledWith(createdURLs()[0]);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(createdURLs()[0]);
});
it('should not paint a frame that decoded after the surface detached', async () => {
@@ -165,7 +165,7 @@ describe('ImageSurfaceController', () => {
await flushPromises();
expect(image.hasAttribute('src')).toBe(false);
expect(URL.revokeObjectURL).toBeCalledWith(createdURLs()[0]);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(createdURLs()[0]);
});
it('should do nothing without an image element', async () => {
@@ -173,7 +173,7 @@ describe('ImageSurfaceController', () => {
await controller.showFrame(createFrame());
expect(URL.createObjectURL).not.toBeCalled();
expect(URL.createObjectURL).not.toHaveBeenCalled();
});
it('should not paint onto a detached element', async () => {
@@ -181,7 +181,7 @@ describe('ImageSurfaceController', () => {
await controller.showFrame(createFrame());
expect(URL.createObjectURL).not.toBeCalled();
expect(URL.createObjectURL).not.toHaveBeenCalled();
});
});
@@ -192,7 +192,7 @@ describe('ImageSurfaceController', () => {
controller.reset();
expect(URL.revokeObjectURL).toBeCalledWith(createdURLs()[0]);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(createdURLs()[0]);
expect(image.hasAttribute('src')).toBe(false);
});
@@ -201,7 +201,7 @@ describe('ImageSurfaceController', () => {
controller.reset();
expect(URL.revokeObjectURL).not.toBeCalled();
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
expect(image.hasAttribute('src')).toBe(false);
});
@@ -226,7 +226,7 @@ describe('ImageSurfaceController', () => {
const controller = new ImageSurfaceController(createLitElement(), () => null);
expect(() => controller.reset()).not.toThrow();
expect(URL.revokeObjectURL).not.toBeCalled();
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
});
});
});
@@ -16,7 +16,7 @@ describe('OffscreenImage', () => {
const offscreen = new OffscreenImage(create);
expect(offscreen.get()).toBe(offscreen.get());
expect(create).toBeCalledTimes(1);
expect(create).toHaveBeenCalledTimes(1);
});
it('should create an image with the default factory when none is injected', () => {
@@ -43,7 +43,7 @@ describe('OffscreenImage', () => {
offscreen.clear();
offscreen.get();
expect(create).toBeCalledTimes(2);
expect(create).toHaveBeenCalledTimes(2);
});
it('should tolerate clear when no image is held', () => {
@@ -17,7 +17,7 @@ describe('OffscreenVideo', () => {
const offscreen = new OffscreenVideo(create);
expect(offscreen.get()).toBe(offscreen.get());
expect(create).toBeCalledTimes(1);
expect(create).toHaveBeenCalledTimes(1);
});
it('should create a video with the default factory when none is injected', () => {
@@ -48,7 +48,7 @@ describe('OffscreenVideo', () => {
offscreen.clear();
offscreen.get();
expect(create).toBeCalledTimes(2);
expect(create).toHaveBeenCalledTimes(2);
});
it('should tolerate clear when no video is held', () => {
@@ -238,7 +238,7 @@ describe('Go2RTCSessionController', () => {
const { session, surfaces, createWebSocket } = setup();
session.connect('https://host/api/ws?src=camera', surfaces, ['mse']);
expect(createWebSocket).toBeCalledWith('wss://host/api/ws?src=camera');
expect(createWebSocket).toHaveBeenCalledWith('wss://host/api/ws?src=camera');
});
it('should be idempotent for an unchanged target', () => {
@@ -246,7 +246,7 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
expect(createWebSocket).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(1);
});
it('should reconnect when the URL changes', () => {
@@ -254,8 +254,8 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
session.connect('http://host/api/ws?src=other', surfaces, ['mse']);
expect(websockets[0].close).toBeCalled();
expect(createWebSocket).toBeCalledTimes(2);
expect(websockets[0].close).toHaveBeenCalled();
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should reconnect when the modes change', () => {
@@ -263,8 +263,8 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
session.connect('http://host/api/ws?src=camera', surfaces, ['webrtc']);
expect(websockets[0].close).toBeCalled();
expect(createWebSocket).toBeCalledTimes(2);
expect(websockets[0].close).toHaveBeenCalled();
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should be idempotent when omitted modes match the default', () => {
@@ -272,7 +272,7 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces);
session.connect('http://host/api/ws?src=camera', surfaces, [...GO2RTC_MODES]);
expect(createWebSocket).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(1);
});
it('should construct real collaborators by default', () => {
@@ -293,8 +293,8 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces);
websockets[0].fireOpen();
expect(createBinarySource).toBeCalledTimes(1);
expect(createWebRTCSource).toBeCalledTimes(1);
expect(createBinarySource).toHaveBeenCalledTimes(1);
expect(createWebRTCSource).toHaveBeenCalledTimes(1);
});
it('should keep the channel open when the binary lane drains synchronously while WebRTC is configured', () => {
@@ -309,11 +309,11 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse', 'webrtc']);
websockets[0].fireOpen();
expect(createWebRTCSource).toBeCalledTimes(1);
expect(websockets[0].close).not.toBeCalled();
expect(createWebRTCSource).toHaveBeenCalledTimes(1);
expect(websockets[0].close).not.toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(1);
});
});
@@ -324,9 +324,9 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
websockets[0].fireOpen();
expect(createBinarySource).toBeCalledTimes(1);
expect(createBinarySource).toHaveBeenCalledTimes(1);
expect(createBinarySource.mock.calls[0][0]).toBe('mse');
expect(binarySources[0].start).toBeCalled();
expect(binarySources[0].start).toHaveBeenCalled();
});
it('should report loaded media from the binary source', () => {
@@ -336,7 +336,7 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
binaryContexts[0].callbacks.loadedCallback();
expect(mediaLoadedCallback).toBeCalledWith(
expect(mediaLoadedCallback).toHaveBeenCalledWith(
expect.objectContaining({ technology: ['mse'] }),
);
});
@@ -354,11 +354,11 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
binaryContexts[0].callbacks.failedCallback('media_error');
expect(binarySources[0].stop).toBeCalled();
expect(websockets[0].close).toBeCalled();
expect(binarySources[0].stop).toHaveBeenCalled();
expect(websockets[0].close).toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should fall back through the binary modes in order', () => {
@@ -385,9 +385,9 @@ describe('Go2RTCSessionController', () => {
// The last binary mode failing reconnects.
binaryContexts[2].callbacks.failedCallback('media_error');
expect(websockets[0].close).toBeCalled();
expect(websockets[0].close).toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should reconnect when the factory declines the mode', () => {
@@ -397,9 +397,9 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
websockets[0].fireOpen();
expect(websockets[0].close).toBeCalled();
expect(websockets[0].close).toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should report loaded media with the video surface controller for MSE', () => {
@@ -415,7 +415,7 @@ describe('Go2RTCSessionController', () => {
expect(
setupResult.mediaLoadedCallback.mock.calls[0][0].mediaPlayerController,
).toBe(setupResult.videoController);
expect(setupResult.surfaceCommittedCallback).toBeCalledWith('video');
expect(setupResult.surfaceCommittedCallback).toHaveBeenCalledWith('video');
});
it('should report loaded media with the image surface controller for MJPEG', () => {
@@ -431,7 +431,7 @@ describe('Go2RTCSessionController', () => {
expect(
setupResult.mediaLoadedCallback.mock.calls[0][0].mediaPlayerController,
).toBe(setupResult.imageController);
expect(setupResult.surfaceCommittedCallback).toBeCalledWith('image');
expect(setupResult.surfaceCommittedCallback).toHaveBeenCalledWith('image');
});
it('should hide controls temporarily on load', () => {
@@ -471,7 +471,7 @@ describe('Go2RTCSessionController', () => {
binaryContexts[0].callbacks.loadedCallback();
// Committed once, but the reload refreshed the reported dimensions.
expect(surfaceCommittedCallback).toBeCalledTimes(1);
expect(surfaceCommittedCallback).toHaveBeenCalledTimes(1);
expect(mediaLoadedCallback.mock.calls[0][0]).toEqual(
expect.objectContaining({ width: 640, height: 480 }),
);
@@ -494,7 +494,7 @@ describe('Go2RTCSessionController', () => {
session.connect('http://host/api/ws?src=camera', surfaces, ['webrtc']);
websockets[0].fireOpen();
expect(createVideoElement).not.toBeCalled();
expect(createVideoElement).not.toHaveBeenCalled();
expect(webRTCContexts[0].target.video).toBe(video);
});
@@ -505,10 +505,10 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
webRTCContexts[0].callbacks.loadedCallback();
expect(mediaLoadedCallback).toBeCalledWith(
expect(mediaLoadedCallback).toHaveBeenCalledWith(
expect.objectContaining({ technology: ['webrtc'] }),
);
expect(websockets[0].close).toBeCalled();
expect(websockets[0].close).toHaveBeenCalled();
});
it('should reconnect when the committed WebRTC stream fails', () => {
@@ -521,7 +521,7 @@ describe('Go2RTCSessionController', () => {
expect(video.srcObject).toBeNull();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should pre-arm the WebRTC source with the current microphone stream', () => {
@@ -552,7 +552,7 @@ describe('Go2RTCSessionController', () => {
audioTransceiver.receiver.track.setMuted(true);
expect(mediaLoadedCallback).toBeCalledTimes(1);
expect(mediaLoadedCallback).toHaveBeenCalledTimes(1);
});
});
@@ -561,8 +561,8 @@ describe('Go2RTCSessionController', () => {
const setupResult = setup();
startSourceRace(setupResult);
expect(setupResult.createBinarySource).toBeCalledTimes(1);
expect(setupResult.createVideoElement).toBeCalledTimes(1);
expect(setupResult.createBinarySource).toHaveBeenCalledTimes(1);
expect(setupResult.createVideoElement).toHaveBeenCalledTimes(1);
expect(setupResult.webRTCContexts[0].target.video).toBe(
setupResult.offscreenVideos[0],
);
@@ -579,9 +579,9 @@ describe('Go2RTCSessionController', () => {
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.video.srcObject).toBe(setupResult.webRTCStream.asMediaStream());
expect(setupResult.binarySources[0].stop).toBeCalled();
expect(setupResult.websockets[0].close).toBeCalled();
expect(setupResult.mediaLoadedCallback).toBeCalledWith(
expect(setupResult.binarySources[0].stop).toHaveBeenCalled();
expect(setupResult.websockets[0].close).toHaveBeenCalled();
expect(setupResult.mediaLoadedCallback).toHaveBeenCalledWith(
expect.objectContaining({ technology: ['webrtc'] }),
);
});
@@ -595,9 +595,9 @@ describe('Go2RTCSessionController', () => {
setupResult.binaryContexts[0].callbacks.loadedCallback();
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.webRTCSources[0].stop).toBeCalled();
expect(setupResult.binarySources[0].stop).not.toBeCalled();
expect(setupResult.websockets[0].close).not.toBeCalled();
expect(setupResult.webRTCSources[0].stop).toHaveBeenCalled();
expect(setupResult.binarySources[0].stop).not.toHaveBeenCalled();
expect(setupResult.websockets[0].close).not.toHaveBeenCalled();
});
it('should adopt WebRTC that wins before the binary source loads', () => {
@@ -606,7 +606,7 @@ describe('Go2RTCSessionController', () => {
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.video.srcObject).toBe(setupResult.webRTCStream.asMediaStream());
expect(setupResult.binarySources[0].stop).toBeCalled();
expect(setupResult.binarySources[0].stop).toHaveBeenCalled();
});
it('should not reconnect when a racing binary fails while WebRTC continues', () => {
@@ -614,7 +614,7 @@ describe('Go2RTCSessionController', () => {
startSourceRace(setupResult);
setupResult.binaryContexts[0].callbacks.failedCallback('media_error');
expect(setupResult.websockets[0].close).not.toBeCalled();
expect(setupResult.websockets[0].close).not.toHaveBeenCalled();
});
it('should not reconnect when a racing WebRTC fails while binary continues', () => {
@@ -622,8 +622,8 @@ describe('Go2RTCSessionController', () => {
startSourceRace(setupResult);
setupResult.webRTCContexts[0].callbacks.failedCallback('connect_timeout');
expect(setupResult.webRTCSources[0].stop).toBeCalled();
expect(setupResult.websockets[0].close).not.toBeCalled();
expect(setupResult.webRTCSources[0].stop).toHaveBeenCalled();
expect(setupResult.websockets[0].close).not.toHaveBeenCalled();
});
it('should reconnect when both racing lanes fail', () => {
@@ -632,9 +632,9 @@ describe('Go2RTCSessionController', () => {
setupResult.binaryContexts[0].callbacks.failedCallback('media_error');
setupResult.webRTCContexts[0].callbacks.failedCallback('connect_timeout');
expect(setupResult.websockets[0].close).toBeCalled();
expect(setupResult.websockets[0].close).toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(setupResult.createWebSocket).toBeCalledTimes(2);
expect(setupResult.createWebSocket).toHaveBeenCalledTimes(2);
});
it('should ignore a duplicate loaded callback from a lost WebRTC lane', () => {
@@ -648,7 +648,7 @@ describe('Go2RTCSessionController', () => {
// WebRTC lost and stopped; a late duplicate callback is ignored.
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.webRTCSources[0].stop).toBeCalledTimes(1);
expect(setupResult.webRTCSources[0].stop).toHaveBeenCalledTimes(1);
});
});
@@ -665,7 +665,7 @@ describe('Go2RTCSessionController', () => {
setupResult.binaryContexts[0].targets.image.showFrame(frame);
expect(setupResult.showFrame).toBeCalledWith(frame);
expect(setupResult.showFrame).toHaveBeenCalledWith(frame);
});
it('should reset the outgoing video surface when falling back to an image mode', () => {
@@ -682,7 +682,7 @@ describe('Go2RTCSessionController', () => {
expect(video.srcObject).toBeNull();
// The image surface, being committed to, is not reset.
expect(reset).not.toBeCalled();
expect(reset).not.toHaveBeenCalled();
});
it('should reset the outgoing image surface when WebRTC wins over an image mode', () => {
@@ -700,7 +700,7 @@ describe('Go2RTCSessionController', () => {
setupResult.binaryContexts[0].callbacks.loadedCallback();
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.reset).toBeCalled();
expect(setupResult.reset).toHaveBeenCalled();
expect(setupResult.surfaceCommittedCallback).toHaveBeenLastCalledWith('video');
});
@@ -715,7 +715,7 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
binaryContexts[0].callbacks.loadedCallback();
expect(mediaLoadedCallback).not.toBeCalled();
expect(mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should reconnect when handed a new surfaces object for the same target', () => {
@@ -728,8 +728,8 @@ describe('Go2RTCSessionController', () => {
'mse',
]);
expect(websockets[0].close).toBeCalled();
expect(createWebSocket).toBeCalledTimes(2);
expect(websockets[0].close).toHaveBeenCalled();
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should abandon the binary lane when the video element is detached at open', () => {
@@ -739,7 +739,7 @@ describe('Go2RTCSessionController', () => {
setVideoElement(null);
websockets[0].fireOpen();
expect(createBinarySource).not.toBeCalled();
expect(createBinarySource).not.toHaveBeenCalled();
});
it('should abandon a WebRTC-only lane when the video element is detached at open', () => {
@@ -749,7 +749,7 @@ describe('Go2RTCSessionController', () => {
setVideoElement(null);
websockets[0].fireOpen();
expect(createWebRTCSource).not.toBeCalled();
expect(createWebRTCSource).not.toHaveBeenCalled();
});
it('should still commit a WebRTC win when the video element is detached', () => {
@@ -762,8 +762,8 @@ describe('Go2RTCSessionController', () => {
// No element to attach the stream to, but the win still tears down the
// binary lane and reports loaded media (dimensions come from the
// off-screen element).
expect(setupResult.binarySources[0].stop).toBeCalled();
expect(setupResult.mediaLoadedCallback).toBeCalledWith(
expect(setupResult.binarySources[0].stop).toHaveBeenCalled();
expect(setupResult.mediaLoadedCallback).toHaveBeenCalledWith(
expect.objectContaining({ technology: ['webrtc'] }),
);
});
@@ -782,7 +782,7 @@ describe('Go2RTCSessionController', () => {
setVideoElement(null);
expect(() => binaryContexts[0].callbacks.loadedCallback()).not.toThrow();
expect(mediaLoadedCallback).not.toBeCalled();
expect(mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should skip resetting a detached video surface on a switch to image', () => {
@@ -826,7 +826,7 @@ describe('Go2RTCSessionController', () => {
// Re-attached in time for the retry, which then reconnects normally.
setVideoElement(video);
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
});
@@ -840,7 +840,7 @@ describe('Go2RTCSessionController', () => {
]).asMediaStream();
session.setMicrophoneStream(micStream);
expect(webRTCSources[0].setMicrophoneStream).toBeCalledWith(micStream);
expect(webRTCSources[0].setMicrophoneStream).toHaveBeenCalledWith(micStream);
});
it('should tolerate a microphone change with no WebRTC source', () => {
@@ -857,9 +857,9 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
websockets[0].fireClose();
expect(binarySources[0].stop).toBeCalled();
expect(binarySources[0].stop).toHaveBeenCalled();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should escalate via the error callback after exhausting reconnect attempts', () => {
@@ -877,13 +877,13 @@ describe('Go2RTCSessionController', () => {
websockets[3].fireOpen();
websockets[3].fireClose();
expect(createWebSocket).toBeCalledTimes(4);
expect(errorCallback).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(4);
expect(errorCallback).toHaveBeenCalledTimes(1);
// The socket dropped with no source reporting a cause.
expect(errorCallback).toBeCalledWith(null);
expect(errorCallback).toHaveBeenCalledWith(null);
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(4);
expect(createWebSocket).toHaveBeenCalledTimes(4);
});
it('should escalate with the most recent source failure reason', () => {
@@ -901,7 +901,7 @@ describe('Go2RTCSessionController', () => {
websockets[3].fireOpen();
binaryContexts[3].callbacks.failedCallback('unsupported');
expect(errorCallback).toBeCalledWith('unsupported');
expect(errorCallback).toHaveBeenCalledWith('unsupported');
});
it('should reset the reconnect budget after a successful media load', () => {
@@ -933,8 +933,8 @@ describe('Go2RTCSessionController', () => {
websockets[attempt + 1].fireOpen();
}
expect(errorCallback).not.toBeCalled();
expect(createWebSocket).toBeCalledTimes(6);
expect(errorCallback).not.toHaveBeenCalled();
expect(createWebSocket).toHaveBeenCalledTimes(6);
});
it('should tear down all lanes and clear the video on reset', () => {
@@ -942,9 +942,9 @@ describe('Go2RTCSessionController', () => {
startSourceRace(setupResult);
setupResult.session.reset();
expect(setupResult.binarySources[0].stop).toBeCalled();
expect(setupResult.webRTCSources[0].stop).toBeCalled();
expect(setupResult.websockets[0].close).toBeCalled();
expect(setupResult.binarySources[0].stop).toHaveBeenCalled();
expect(setupResult.webRTCSources[0].stop).toHaveBeenCalled();
expect(setupResult.websockets[0].close).toHaveBeenCalled();
expect(setupResult.video.srcObject).toBeNull();
});
@@ -956,7 +956,7 @@ describe('Go2RTCSessionController', () => {
session.reset();
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(1);
});
it('should allow connecting to the same target after reset', () => {
@@ -965,7 +965,7 @@ describe('Go2RTCSessionController', () => {
session.reset();
session.connect('http://host/api/ws?src=camera', surfaces, ['mse']);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
});
@@ -978,7 +978,7 @@ describe('Go2RTCSessionController', () => {
binaryContexts[0].callbacks.failedCallback('media_error');
vi.advanceTimersByTime(2 * 1000);
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should ignore a loaded callback from a retired binary source', () => {
@@ -990,7 +990,7 @@ describe('Go2RTCSessionController', () => {
setupResult.mediaLoadedCallback.mockClear();
setupResult.binaryContexts[0].callbacks.loadedCallback();
expect(setupResult.mediaLoadedCallback).not.toBeCalled();
expect(setupResult.mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should ignore a failed callback from a retired binary source', () => {
@@ -1000,7 +1000,7 @@ describe('Go2RTCSessionController', () => {
setupResult.binarySources[0].stop.mockClear();
setupResult.binaryContexts[0].callbacks.failedCallback('media_error');
expect(setupResult.binarySources[0].stop).not.toBeCalled();
expect(setupResult.binarySources[0].stop).not.toHaveBeenCalled();
});
it('should ignore a failed callback from a retired WebRTC source', () => {
@@ -1012,7 +1012,7 @@ describe('Go2RTCSessionController', () => {
webRTCSources[0].stop.mockClear();
webRTCContexts[0].callbacks.failedCallback('media_error');
expect(webRTCSources[0].stop).not.toBeCalled();
expect(webRTCSources[0].stop).not.toHaveBeenCalled();
});
it('should adopt WebRTC when the racing binary already failed', () => {
@@ -1031,7 +1031,7 @@ describe('Go2RTCSessionController', () => {
setupResult.webRTCContexts[0].callbacks.loadedCallback();
expect(setupResult.video.srcObject).toBeFalsy();
expect(setupResult.binarySources[0].stop).toBeCalled();
expect(setupResult.binarySources[0].stop).toHaveBeenCalled();
});
it('should not report media that cannot be described', () => {
@@ -1045,7 +1045,7 @@ describe('Go2RTCSessionController', () => {
websockets[0].fireOpen();
binaryContexts[0].callbacks.loadedCallback();
expect(mediaLoadedCallback).not.toBeCalled();
expect(mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should swallow a rejected microphone update', async () => {
@@ -1090,7 +1090,7 @@ describe('Go2RTCSessionController', () => {
]);
websockets[0].fireOpen();
expect(mediaLoadedCallback).not.toBeCalled();
expect(mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should ignore callbacks fired while a WebRTC source is constructed', () => {
@@ -1124,7 +1124,7 @@ describe('Go2RTCSessionController', () => {
]);
websockets[0].fireOpen();
expect(mediaLoadedCallback).not.toBeCalled();
expect(mediaLoadedCallback).not.toHaveBeenCalled();
});
it('should use the default binary source factory when none is injected', () => {
@@ -1153,7 +1153,7 @@ describe('Go2RTCSessionController', () => {
// closes and retries; the point is that the default factory was used.
websockets[0].fireOpen();
expect(websockets[0].close).toBeCalled();
expect(websockets[0].close).toHaveBeenCalled();
session.reset();
});
@@ -1236,7 +1236,7 @@ describe('Go2RTCSessionController', () => {
binaryContexts[0].callbacks.failedCallback('media_error');
expect(consoleSpy).toBeCalledWith('go2rtc-experimental source failed', {
expect(consoleSpy).toHaveBeenCalledWith('go2rtc-experimental source failed', {
lane: 'binary',
mode: 'mse',
reason: 'media_error',
@@ -1253,7 +1253,7 @@ describe('Go2RTCSessionController', () => {
webRTCContexts[0].callbacks.failedCallback('connect_timeout');
expect(consoleSpy).toBeCalledWith('go2rtc-experimental source failed', {
expect(consoleSpy).toHaveBeenCalledWith('go2rtc-experimental source failed', {
lane: 'webrtc',
reason: 'connect_timeout',
});
@@ -1267,7 +1267,7 @@ describe('Go2RTCSessionController', () => {
binaryContexts[0].callbacks.failedCallback('media_error');
expect(consoleSpy).not.toBeCalled();
expect(consoleSpy).not.toHaveBeenCalled();
});
});
});
@@ -29,7 +29,7 @@ describe('SignalingChannel', () => {
const { channel, createWebSocket, websockets } = setup();
channel.connect();
expect(createWebSocket).toBeCalledWith('ws://host/api/ws?src=camera');
expect(createWebSocket).toHaveBeenCalledWith('ws://host/api/ws?src=camera');
expect(websockets[0].binaryType).toBe('arraybuffer');
});
@@ -38,7 +38,7 @@ describe('SignalingChannel', () => {
channel.connect();
channel.connect();
expect(createWebSocket).toBeCalledTimes(1);
expect(createWebSocket).toHaveBeenCalledTimes(1);
});
it('should report open state and call the open callback', () => {
@@ -51,7 +51,7 @@ describe('SignalingChannel', () => {
websockets[0].fireOpen();
expect(channel.isOpen()).toBe(true);
expect(openCallback).toBeCalled();
expect(openCallback).toHaveBeenCalled();
});
it('should tolerate an absent open callback', () => {
@@ -66,7 +66,7 @@ describe('SignalingChannel', () => {
channel.connect();
channel.send({ type: 'mse', value: 'codecs' });
expect(websockets[0].send).not.toBeCalled();
expect(websockets[0].send).not.toHaveBeenCalled();
});
it('should send JSON once open', () => {
@@ -86,7 +86,7 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage('{"type":"mse","value":"video/mp4"}');
expect(callback).toBeCalledWith({ type: 'mse', value: 'video/mp4' });
expect(callback).toHaveBeenCalledWith({ type: 'mse', value: 'video/mp4' });
});
it('should stop dispatching after unsubscribe', () => {
@@ -97,7 +97,7 @@ describe('SignalingChannel', () => {
unsubscribe();
websockets[0].fireMessage('{"type":"mse"}');
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should dispatch to remaining subscribers when one unsubscribes during dispatch', () => {
@@ -111,8 +111,8 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage('{"type":"mse"}');
expect(unsubscribeDuringDispatch).toBeCalledTimes(1);
expect(secondCallback).toBeCalledTimes(1);
expect(unsubscribeDuringDispatch).toHaveBeenCalledTimes(1);
expect(secondCallback).toHaveBeenCalledTimes(1);
});
it('should ignore invalid JSON', () => {
@@ -122,7 +122,7 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage('NOT JSON');
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should ignore malformed messages', () => {
@@ -132,7 +132,7 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage('{"type":6}');
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should ignore unexpected data types', () => {
@@ -142,7 +142,7 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage(42);
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should route binary data to the binary callback', () => {
@@ -153,7 +153,7 @@ describe('SignalingChannel', () => {
const data = new ArrayBuffer(8);
websockets[0].fireMessage(data);
expect(binaryCallback).toBeCalledWith(data);
expect(binaryCallback).toHaveBeenCalledWith(data);
});
it('should drop binary data without a binary callback', () => {
@@ -171,7 +171,7 @@ describe('SignalingChannel', () => {
channel.connect();
websockets[0].fireMessage(new ArrayBuffer(8));
expect(binaryCallback).not.toBeCalled();
expect(binaryCallback).not.toHaveBeenCalled();
});
it('should close the underlying websocket without firing the disconnect callback', () => {
@@ -181,9 +181,9 @@ describe('SignalingChannel', () => {
websockets[0].fireOpen();
channel.close();
expect(websockets[0].close).toBeCalled();
expect(websockets[0].close).toHaveBeenCalled();
expect(channel.isOpen()).toBe(false);
expect(disconnectCallback).not.toBeCalled();
expect(disconnectCallback).not.toHaveBeenCalled();
});
it('should tolerate closing when never connected', () => {
@@ -205,9 +205,9 @@ describe('SignalingChannel', () => {
websockets[0].fireMessage('{"type":"mse"}');
websockets[0].fireClose();
expect(openCallback).not.toBeCalled();
expect(messageCallback).not.toBeCalled();
expect(disconnectCallback).not.toBeCalled();
expect(openCallback).not.toHaveBeenCalled();
expect(messageCallback).not.toHaveBeenCalled();
expect(disconnectCallback).not.toHaveBeenCalled();
});
it('should fire the disconnect callback on unexpected closure', () => {
@@ -217,7 +217,7 @@ describe('SignalingChannel', () => {
websockets[0].fireOpen();
websockets[0].fireClose();
expect(disconnectCallback).toBeCalledTimes(1);
expect(disconnectCallback).toHaveBeenCalledTimes(1);
expect(channel.isOpen()).toBe(false);
});
@@ -234,7 +234,7 @@ describe('SignalingChannel', () => {
websockets[0].fireClose();
channel.connect();
expect(createWebSocket).toBeCalledTimes(2);
expect(createWebSocket).toHaveBeenCalledTimes(2);
});
it('should construct a real websocket by default', () => {
@@ -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', () => {