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('CachedValueController', () => {
controller.hostUpdate();
// Should not restart since refreshSeconds hasn't changed
expect(startTimerSpy).not.toBeCalled();
expect(startTimerSpy).not.toHaveBeenCalled();
});
it('should construct', () => {
@@ -69,28 +69,28 @@ describe('CachedValueController', () => {
);
controller.startTimer();
expect(startCallback).toBeCalled();
expect(startCallback).toHaveBeenCalled();
callback.mockReturnValue(3);
vi.runOnlyPendingTimers();
expect(callback).toBeCalled();
expect(host.requestUpdate).toBeCalled();
expect(callback).toHaveBeenCalled();
expect(host.requestUpdate).toHaveBeenCalled();
expect(controller.getValue()).toBe(3);
callback.mockReturnValue(4);
vi.runOnlyPendingTimers();
expect(callback).toBeCalled();
expect(host.requestUpdate).toBeCalled();
expect(callback).toHaveBeenCalled();
expect(host.requestUpdate).toHaveBeenCalled();
expect(controller.getValue()).toBe(4);
expect(controller.hasTimer()).toBeTruthy();
controller.stopTimer();
expect(stopCallback).toBeCalled();
expect(stopCallback).toHaveBeenCalled();
callback.mockReset();
vi.runOnlyPendingTimers();
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should clear value', () => {
@@ -125,12 +125,12 @@ describe('CachedValueController', () => {
controller.hostConnected();
expect(controller.getValue()).equal(43);
expect(startCallback).toBeCalled();
expect(host.requestUpdate).toBeCalled();
expect(startCallback).toHaveBeenCalled();
expect(host.requestUpdate).toHaveBeenCalled();
controller.hostDisconnected();
expect(controller.getValue()).toBeNull();
expect(stopCallback).toBeCalled();
expect(stopCallback).toHaveBeenCalled();
});
it('should call timer tick callback on each tick before updateValue', () => {
@@ -199,10 +199,10 @@ describe('CachedValueController', () => {
// it shouldn't fire at 15 seconds.
callback.mockClear();
vi.advanceTimersByTime(15 * 1000);
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(5 * 1000);
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalled();
// Now set it to null -> stops timer
refreshSeconds = null;
@@ -21,13 +21,6 @@ import {
ResizeObserverMock,
} from '../../test-utils';
vi.mock('lodash-es', async () => {
return {
...(await vi.importActual('lodash-es')),
throttle: vi.fn((fn) => fn),
};
});
vi.mock('../../../src/utils/sleep');
vi.mock('../../../src/utils/scroll');
@@ -66,7 +59,7 @@ describe('GalleryCoreController', () => {
const host = createLitElement();
const controller = createController({ host });
expect(controller).toBeInstanceOf(GalleryCoreController);
expect(host.addController).toBeCalledWith(controller);
expect(host.addController).toHaveBeenCalledWith(controller);
});
it('should remove controller', () => {
@@ -74,7 +67,7 @@ describe('GalleryCoreController', () => {
const controller = createController({ host });
expect(controller).toBeInstanceOf(GalleryCoreController);
controller.removeController();
expect(host.removeController).toBeCalledWith(controller);
expect(host.removeController).toHaveBeenCalledWith(controller);
});
it('should remove controller', () => {
@@ -82,7 +75,7 @@ describe('GalleryCoreController', () => {
const controller = createController({ host });
expect(controller).toBeInstanceOf(GalleryCoreController);
controller.removeController();
expect(host.removeController).toBeCalledWith(controller);
expect(host.removeController).toHaveBeenCalledWith(controller);
});
describe('should observe sentintel when host updated', () => {
@@ -100,10 +93,10 @@ describe('GalleryCoreController', () => {
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
).toBeCalled();
).toHaveBeenCalled();
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
).toBeCalledWith(sentinel);
).toHaveBeenCalledWith(sentinel);
});
it('should disconnect when sentinel changes to null', () => {
@@ -125,10 +118,10 @@ describe('GalleryCoreController', () => {
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
).toBeCalledTimes(2);
).toHaveBeenCalledTimes(2);
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
});
it('should skip disconnect/observe when sentinel is unchanged', () => {
@@ -147,10 +140,10 @@ describe('GalleryCoreController', () => {
// Only called once despite two hostUpdated() calls.
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
});
it('should not observe when sentinel is null from the start', () => {
@@ -166,10 +159,10 @@ describe('GalleryCoreController', () => {
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
).not.toBeCalled();
).not.toHaveBeenCalled();
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.observe,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
});
@@ -180,15 +173,17 @@ describe('GalleryCoreController', () => {
controller.hostConnected();
expect(vi.mocked(ResizeObserver).mock.results[0].value.observe).toBeCalledWith(host);
expect(host.addEventListener).toBeCalledWith('wheel', expect.anything(), {
expect(vi.mocked(ResizeObserver).mock.results[0].value.observe).toHaveBeenCalledWith(
host,
);
expect(host.addEventListener).toHaveBeenCalledWith('wheel', expect.anything(), {
passive: true,
});
expect(host.addEventListener).toBeCalledWith('touchstart', expect.anything(), {
expect(host.addEventListener).toHaveBeenCalledWith('touchstart', expect.anything(), {
passive: true,
});
expect(host.addEventListener).toBeCalledWith('touchend', expect.anything());
expect(host.requestUpdate).toBeCalled();
expect(host.addEventListener).toHaveBeenCalledWith('touchend', expect.anything());
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should detach listeners on disconnect', () => {
@@ -198,13 +193,18 @@ describe('GalleryCoreController', () => {
controller.hostDisconnected();
expect(vi.mocked(ResizeObserver).mock.results[0].value.disconnect).toBeCalled();
expect(
vi.mocked(ResizeObserver).mock.results[0].value.disconnect,
).toHaveBeenCalled();
expect(
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
).toBeCalled();
expect(host.removeEventListener).toBeCalledWith('wheel', expect.anything());
expect(host.removeEventListener).toBeCalledWith('touchstart', expect.anything());
expect(host.removeEventListener).toBeCalledWith('touchend', expect.anything());
).toHaveBeenCalled();
expect(host.removeEventListener).toHaveBeenCalledWith('wheel', expect.anything());
expect(host.removeEventListener).toHaveBeenCalledWith(
'touchstart',
expect.anything(),
);
expect(host.removeEventListener).toHaveBeenCalledWith('touchend', expect.anything());
});
describe('should set the number of columns', () => {
@@ -415,7 +415,7 @@ describe('GalleryCoreController', () => {
);
await flushPromises();
expect(showLoaderTop).not.toBeCalled();
expect(showLoaderTop).not.toHaveBeenCalled();
});
it('should not extend up with touch when when not at top of component', async () => {
@@ -442,7 +442,7 @@ describe('GalleryCoreController', () => {
);
await flushPromises();
expect(showLoaderTop).not.toBeCalled();
expect(showLoaderTop).not.toHaveBeenCalled();
});
it('should not extend up with touch when touches moved downwards', async () => {
@@ -468,7 +468,7 @@ describe('GalleryCoreController', () => {
);
await flushPromises();
expect(showLoaderTop).not.toBeCalled();
expect(showLoaderTop).not.toHaveBeenCalled();
});
});
@@ -607,7 +607,7 @@ describe('GalleryCoreController', () => {
});
controller.updateContents();
expect(scrollIntoView).toBeCalledWith(selectedChild, {
expect(scrollIntoView).toHaveBeenCalledWith(selectedChild, {
boundary: host,
block: 'center',
});
@@ -635,7 +635,7 @@ describe('GalleryCoreController', () => {
controller.updateContents();
controller.updateContents();
expect(scrollIntoView).toBeCalledTimes(1);
expect(scrollIntoView).toHaveBeenCalledTimes(1);
});
it('should do nothing without a selected element', async () => {
@@ -655,7 +655,7 @@ describe('GalleryCoreController', () => {
});
controller.updateContents();
expect(scrollIntoView).not.toBeCalled();
expect(scrollIntoView).not.toHaveBeenCalled();
});
});
@@ -674,7 +674,7 @@ describe('GalleryCoreController', () => {
controller.updateContents();
expect(showSentinelBottom).toBeCalledWith(true);
expect(showSentinelBottom).toHaveBeenCalledWith(true);
});
it('should do nothing without slot', async () => {
@@ -691,8 +691,8 @@ describe('GalleryCoreController', () => {
controller.updateContents();
expect(showSentinelBottom).not.toBeCalled();
expect(scrollIntoView).not.toBeCalled();
expect(showSentinelBottom).not.toHaveBeenCalled();
expect(scrollIntoView).not.toHaveBeenCalled();
});
});
});
@@ -26,8 +26,8 @@ describe('KeyAssignerController', () => {
expect(controller.hasValue()).toBeTruthy();
expect(controller.getValue()).toEqual({ key: 'ArrowLeft' });
expect(element.requestUpdate).toBeCalled();
expect(valueChangeHandler).toBeCalledWith(
expect(element.requestUpdate).toHaveBeenCalled();
expect(valueChangeHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { value: { key: 'ArrowLeft' } },
}),
@@ -35,8 +35,8 @@ describe('KeyAssignerController', () => {
// Set again with the same value.
controller.setValue({ key: 'ArrowLeft' });
expect(element.requestUpdate).toBeCalledTimes(1);
expect(valueChangeHandler).toBeCalledTimes(1);
expect(element.requestUpdate).toHaveBeenCalledTimes(1);
expect(valueChangeHandler).toHaveBeenCalledTimes(1);
});
});
@@ -81,7 +81,7 @@ describe('KeyAssignerController', () => {
const element = createLitElement();
const controller = new KeyAssignerController(element);
controller.toggleAssigning();
expect(element.requestUpdate).toBeCalled();
expect(element.requestUpdate).toHaveBeenCalled();
expect(controller.isAssigning()).toBeTruthy();
expect(element.getAttribute('assigning')).toBe('');
@@ -58,14 +58,14 @@ describe('LazyLoadController', () => {
it('should add controller to host', () => {
const host = createLitElement();
const controller = new LazyLoadController(host);
expect(host.addController).toBeCalledWith(controller);
expect(host.addController).toHaveBeenCalledWith(controller);
});
it('should remove controller from host', () => {
const host = createLitElement();
const controller = new LazyLoadController(host);
controller.removeController();
expect(host.removeController).toBeCalledWith(controller);
expect(host.removeController).toHaveBeenCalledWith(controller);
});
it('should remove handlers and listeners on destroy', () => {
@@ -81,8 +81,8 @@ describe('LazyLoadController', () => {
controller.destroy();
expect(getMockIntersectionObserver()?.disconnect).toBeCalled();
expect(global.document.removeEventListener).toBeCalledWith(
expect(getMockIntersectionObserver()?.disconnect).toHaveBeenCalled();
expect(global.document.removeEventListener).toHaveBeenCalledWith(
'visibilitychange',
expect.anything(),
);
@@ -90,7 +90,7 @@ describe('LazyLoadController', () => {
callVisibilityHandler(true);
callIntersectionHandler(true);
expect(listener).not.toBeCalled();
expect(listener).not.toHaveBeenCalled();
});
describe('should set configuration', () => {
@@ -100,12 +100,12 @@ describe('LazyLoadController', () => {
controller.addListener(listener);
expect(controller.isLoaded()).toBe(false);
expect(listener).not.toBeCalled();
expect(listener).not.toHaveBeenCalled();
controller.setConfiguration({ lazyLoad: false });
expect(controller.isLoaded()).toBe(true);
expect(listener).toBeCalled();
expect(listener).toHaveBeenCalled();
});
it('should re-evaluate unload when conditions change while loaded', () => {
@@ -206,8 +206,8 @@ describe('LazyLoadController', () => {
expect(controller.isLoaded()).toBe(false);
// Should also stop observing.
expect(getMockIntersectionObserver()?.disconnect).toBeCalled();
expect(global.document.removeEventListener).toBeCalledWith(
expect(getMockIntersectionObserver()?.disconnect).toHaveBeenCalled();
expect(global.document.removeEventListener).toHaveBeenCalledWith(
'visibilitychange',
expect.anything(),
);
@@ -383,19 +383,19 @@ describe('LazyLoadController', () => {
callIntersectionHandler(true);
callVisibilityHandler(true);
expect(listener).toHaveBeenLastCalledWith(true);
expect(listener).toBeCalledTimes(1);
expect(listener).toHaveBeenCalledTimes(1);
callIntersectionHandler(false);
expect(listener).toHaveBeenLastCalledWith(false);
expect(listener).toBeCalledTimes(2);
expect(listener).toHaveBeenCalledTimes(2);
callIntersectionHandler(true);
expect(listener).toHaveBeenLastCalledWith(true);
expect(listener).toBeCalledTimes(3);
expect(listener).toHaveBeenCalledTimes(3);
controller.removeListener(listener);
callIntersectionHandler(false);
expect(listener).toBeCalledTimes(3);
expect(listener).toHaveBeenCalledTimes(3);
});
});
@@ -10,10 +10,10 @@ import {
type LiveError,
} from '../../../../src/components-lib/live/utils/dispatch-live-error';
import type { LivenessCallback, MediaPlayerController } from '../../../../src/types';
import { createCameraConfig } from '../../../config/test-utils';
import {
callIntersectionHandler,
callStateWatcherCallback,
createCameraConfig,
createHASS,
createLitElement,
createMediaLoadedInfo,
@@ -53,8 +53,8 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera('camera-1');
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.mute).not.toBeCalled();
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
expect(microphoneManager.mute).not.toHaveBeenCalled();
});
it('should swallow a rejected auto-unmute so a denied microphone does not surface', async () => {
@@ -67,7 +67,7 @@ describe('MicrophoneActionsController', () => {
});
await expect(controller.setSelectedCamera('camera-1')).resolves.toBeUndefined();
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should mute on unselected when transitioning to a new camera', async () => {
@@ -81,7 +81,7 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera('camera-1');
await controller.setSelectedCamera('camera-2');
expect(microphoneManager.mute).toBeCalledTimes(1);
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
});
it('should sequence mute-then-unmute deterministically on transition', async () => {
@@ -128,7 +128,7 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera('camera-1');
await controller.setSelectedCamera('camera-1');
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should fire unselected only when transitioning from a camera to none', async () => {
@@ -147,8 +147,8 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera(null);
expect(microphoneManager.mute).toBeCalledTimes(1);
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
it('should not fire unselected on the very first selection (no previous)', async () => {
@@ -162,8 +162,8 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera('camera-1');
expect(microphoneManager.mute).not.toBeCalled();
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.mute).not.toHaveBeenCalled();
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should not fire when condition arrays are empty', async () => {
@@ -178,8 +178,8 @@ describe('MicrophoneActionsController', () => {
await controller.setSelectedCamera('camera-1');
await controller.setSelectedCamera('camera-2');
expect(microphoneManager.mute).not.toBeCalled();
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.mute).not.toHaveBeenCalled();
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
it('should not crash when conditions configured but no microphone manager is passed', async () => {
@@ -212,7 +212,7 @@ describe('MicrophoneActionsController', () => {
await callVisibilityHandler(false);
expect(microphoneManager.mute).toBeCalledTimes(1);
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
});
it('should unmute on visible when the live root is intersecting', async () => {
@@ -233,7 +233,7 @@ describe('MicrophoneActionsController', () => {
await callVisibilityHandler(true);
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should not unmute on tab visible when the live root is hidden', async () => {
@@ -254,7 +254,7 @@ describe('MicrophoneActionsController', () => {
await callVisibilityHandler(false);
await callVisibilityHandler(true);
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
});
@@ -273,7 +273,7 @@ describe('MicrophoneActionsController', () => {
await callIntersectionHandler(true);
await callIntersectionHandler(false);
expect(microphoneManager.mute).toBeCalledTimes(1);
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
});
it('should unmute when the live root scrolls back into view', async () => {
@@ -288,7 +288,7 @@ describe('MicrophoneActionsController', () => {
await callIntersectionHandler(false);
await callIntersectionHandler(true);
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should ignore the very first intersection callback (baseline)', async () => {
@@ -303,8 +303,8 @@ describe('MicrophoneActionsController', () => {
await callIntersectionHandler(false);
expect(microphoneManager.mute).not.toBeCalled();
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.mute).not.toHaveBeenCalled();
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
});
@@ -320,7 +320,7 @@ describe('MicrophoneActionsController', () => {
controller.setCallAnswered(false);
controller.setCallAnswered(true);
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should unmute when the call is already answered on first notification', () => {
@@ -336,7 +336,7 @@ describe('MicrophoneActionsController', () => {
// already answered. The initial state must not be swallowed as a baseline.
controller.setCallAnswered(true);
expect(microphoneManager.unmute).toBeCalledTimes(1);
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
});
it('should mute on call end when call is a configured mute condition', () => {
@@ -350,7 +350,7 @@ describe('MicrophoneActionsController', () => {
controller.setCallAnswered(true);
controller.setCallAnswered(false);
expect(microphoneManager.mute).toBeCalledTimes(1);
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
});
it('should not act on the initial call state', () => {
@@ -364,8 +364,8 @@ describe('MicrophoneActionsController', () => {
controller.setCallAnswered(false);
expect(microphoneManager.mute).not.toBeCalled();
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.mute).not.toHaveBeenCalled();
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
it('should not act on call answer when call is not a configured condition', () => {
@@ -379,7 +379,7 @@ describe('MicrophoneActionsController', () => {
controller.setCallAnswered(false);
controller.setCallAnswered(true);
expect(microphoneManager.unmute).not.toBeCalled();
expect(microphoneManager.unmute).not.toHaveBeenCalled();
});
});
@@ -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', () => {
@@ -104,7 +104,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
it('should do nothing on resetting same root', () => {
@@ -138,7 +138,7 @@ describe('MediaActionsController', () => {
await controller.setTarget(1, true);
expect(mediaPlayerController.playback?.play).toBeCalled();
expect(mediaPlayerController.playback?.play).toHaveBeenCalled();
});
});
@@ -161,7 +161,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
});
@@ -195,7 +195,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
@@ -214,14 +214,14 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
await controller.setTarget(0, true);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
});
it('should unselect before selecting a new target', async () => {
@@ -241,10 +241,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.pause,
).toBeCalled();
).toHaveBeenCalled();
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should select after target was previously visible', async () => {
@@ -263,20 +263,20 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).not.toBeCalled();
).not.toHaveBeenCalled();
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).not.toBeCalled();
).not.toHaveBeenCalled();
await controller.setTarget(0, true);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalled();
).toHaveBeenCalled();
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalled();
).toHaveBeenCalled();
});
});
@@ -296,10 +296,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
controller.unsetTarget();
@@ -312,10 +312,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
});
describe('should respond to media loaded', () => {
@@ -334,7 +334,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
getPlayer(children[0], 'video')?.dispatchEvent(
new Event('advanced-camera-card:media:loaded'),
@@ -345,7 +345,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(2);
).toHaveBeenCalledTimes(2);
});
it('should unmute after media load', async () => {
@@ -361,7 +361,7 @@ describe('MediaActionsController', () => {
await controller.setTarget(0, true);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
getPlayer(children[0], 'video')?.dispatchEvent(
new Event('advanced-camera-card:media:loaded'),
@@ -371,7 +371,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(2);
).toHaveBeenCalledTimes(2);
});
it('should take no action on unrelated media load', async () => {
@@ -396,10 +396,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[9], 'video')?.getMediaPlayerController())?.playback
?.play,
).not.toBeCalled();
).not.toHaveBeenCalled();
expect(
(await getPlayer(children[9], 'video')?.getMediaPlayerController())?.unmute,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
it('should play and unmute on unselected but targeted media load', async () => {
@@ -418,10 +418,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(1);
).toHaveBeenCalledTimes(1);
getPlayer(children[0], 'video')?.dispatchEvent(
new Event('advanced-camera-card:media:loaded'),
@@ -432,10 +432,10 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.playback
?.play,
).toBeCalledTimes(2);
).toHaveBeenCalledTimes(2);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalledTimes(2);
).toHaveBeenCalledTimes(2);
});
});
@@ -470,7 +470,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
});
@@ -516,7 +516,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
});
@@ -561,7 +561,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
});
@@ -596,7 +596,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).not.toBeCalled();
).not.toHaveBeenCalled();
// There's always a first call to an intersection observer handler. In
// this case the MediaActionsController ignores it.
@@ -610,7 +610,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
});
@@ -645,7 +645,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).not.toBeCalled();
).not.toHaveBeenCalled();
// There's always a first call to an intersection observer handler. In
// this case the MediaActionsController ignores it.
@@ -659,7 +659,7 @@ describe('MediaActionsController', () => {
await getPlayer(children[0], 'video')?.getMediaPlayerController(),
func,
),
).toBeCalledTimes(called ? 1 : 0);
).toHaveBeenCalledTimes(called ? 1 : 0);
},
);
});
@@ -702,7 +702,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should mute after delay after microphone muted', async () => {
@@ -725,7 +725,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should not mute after delay after microphone muted', async () => {
@@ -748,7 +748,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
it('should not act on the initial microphone state', async () => {
@@ -767,7 +767,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
});
@@ -790,7 +790,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should mute the target on call end', async () => {
@@ -811,7 +811,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should not act on the initial call state', async () => {
@@ -831,7 +831,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
it('should not act when call is not a configured condition', async () => {
@@ -851,7 +851,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).not.toBeCalled();
).not.toHaveBeenCalled();
});
it('should apply the call-answer unmute when the target arrives after the call', async () => {
@@ -871,13 +871,13 @@ describe('MediaActionsController', () => {
await flushPromises();
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).not.toBeCalled();
).not.toHaveBeenCalled();
await controller.setTarget(0, true);
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should unmute when the call is already answered on the first call-state signal', async () => {
@@ -900,7 +900,7 @@ describe('MediaActionsController', () => {
expect(
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
).toBeCalled();
).toHaveBeenCalled();
});
it('should defer the call-answer unmute until the media player is ready', async () => {
@@ -928,7 +928,7 @@ describe('MediaActionsController', () => {
// The call is answered while the player is still not ready: no unmute yet.
controller.setCallAnswered(true);
await flushPromises();
expect(mediaPlayerController.unmute).not.toBeCalled();
expect(mediaPlayerController.unmute).not.toHaveBeenCalled();
// Once the media loads the deferred unmute is applied -- exactly once,
// so a later reload cannot clobber a manual mute made during the call.
@@ -936,7 +936,7 @@ describe('MediaActionsController', () => {
await flushPromises();
player.dispatchEvent(new Event('advanced-camera-card:media:loaded'));
await flushPromises();
expect(mediaPlayerController.unmute).toBeCalledTimes(1);
expect(mediaPlayerController.unmute).toHaveBeenCalledTimes(1);
});
});
});
@@ -1,6 +1,18 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { MediaDimensionsContainerController } from '../../src/components-lib/media-dimensions-container-controller';
import {
MediaDimensionsContainerController,
RESIZE_DEBOUNCE_SECONDS,
} from '../../src/components-lib/media-dimensions-container-controller';
import type { CameraDimensionsConfig, Rotation } from '../../src/config/schema/cameras';
import type { MediaLoadedInfo } from '../../src/types';
import {
@@ -11,10 +23,6 @@ import {
ResizeObserverMock,
} from '../test-utils';
vi.mock('lodash-es', () => ({
debounce: vi.fn((fn) => fn),
}));
// @vitest-environment jsdom
describe('MediaDimensionsContainerController', () => {
beforeAll(() => {
@@ -44,8 +52,8 @@ describe('MediaDimensionsContainerController', () => {
const observer = getResizeObserver();
// No resize observer should be created.
expect(observer?.observe).not.toBeCalled();
expect(eventListener).not.toBeCalled();
expect(observer?.observe).not.toHaveBeenCalled();
expect(eventListener).not.toHaveBeenCalled();
});
describe('should connect and disconnect', () => {
@@ -54,14 +62,14 @@ describe('MediaDimensionsContainerController', () => {
const controller = new MediaDimensionsContainerController(host);
const observer = getResizeObserver();
expect(observer?.observe).toBeCalledTimes(0);
expect(observer?.observe).toHaveBeenCalledTimes(0);
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(host);
expect(observer?.observe).toBeCalledTimes(1);
expect(observer?.observe).toHaveBeenCalledWith(host);
expect(observer?.observe).toHaveBeenCalledTimes(1);
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
expect(observer?.disconnect).toHaveBeenCalled();
});
it('should connect and disconnect with a container when host is connected', () => {
@@ -77,13 +85,13 @@ describe('MediaDimensionsContainerController', () => {
const container = createLitElement();
controller.setContainers(container);
expect(observer?.observe).not.toBeCalled();
expect(observer?.observe).not.toHaveBeenCalled();
controller.hostDisconnected();
expect(observer?.disconnect).toBeCalled();
expect(observer?.disconnect).toHaveBeenCalled();
controller.hostConnected();
expect(observer?.observe).toBeCalledWith(host);
expect(observer?.observe).toHaveBeenCalledWith(host);
});
});
@@ -614,6 +622,13 @@ describe('MediaDimensionsContainerController', () => {
});
describe('should respond to slot changes', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should resize container on slotchange event', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
@@ -641,12 +656,20 @@ describe('MediaDimensionsContainerController', () => {
host.removeAttribute('rotated');
innerContainer.dispatchEvent(new Event('slotchange'));
vi.advanceTimersByTime(RESIZE_DEBOUNCE_SECONDS * 1000);
expect(host.hasAttribute('rotated')).toBeTruthy();
});
});
describe('should respond to media load', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should resize container on media load', () => {
const host = createLitElement();
host.getBoundingClientRect = vi.fn().mockReturnValue({
@@ -680,6 +703,7 @@ describe('MediaDimensionsContainerController', () => {
innerContainer.dispatchEvent(
createMediaLoadedInfoEvent({ info: mediaLoadedInfo }),
);
vi.advanceTimersByTime(RESIZE_DEBOUNCE_SECONDS * 1000);
expect(host.hasAttribute('rotated')).toBeTruthy();
});
@@ -32,14 +32,10 @@ import {
import { QuerySource } from '../../src/query-source';
import type { Severity } from '../../src/severity';
import { UnifiedQuery } from '../../src/view/unified-query';
import {
createCameraConfig,
createCameraManager,
createLitElement,
createPerformanceConfig,
createStore,
createView,
} from '../test-utils';
import { createCameraManager, createStore } from '../camera-manager/test-utils';
import { createCameraConfig, createPerformanceConfig } from '../config/test-utils';
import { createLitElement } from '../test-utils';
import { createView } from '../view/test-utils';
const createCameraStore = (options?: { capabilities: Capabilities }) => {
return createStore([
@@ -467,7 +463,7 @@ describe('MediaFilterController', () => {
const host = createLitElement();
const controller = new MediaFilterController(host);
await controller.computeMetadataOptions(cameraManager);
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('with metadata for what', async () => {
@@ -483,7 +479,7 @@ describe('MediaFilterController', () => {
{ value: 'car', label: 'Car' },
{ value: 'person', label: 'Person' },
]);
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('with metadata for where', async () => {
@@ -499,7 +495,7 @@ describe('MediaFilterController', () => {
{ value: 'back_yard', label: 'Back Yard' },
{ value: 'front_door', label: 'Front Door' },
]);
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('with metadata for tags', async () => {
@@ -515,7 +511,7 @@ describe('MediaFilterController', () => {
{ value: 'tag-1', label: 'Tag-1' },
{ value: 'tag-2', label: 'Tag-2' },
]);
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('with metadata for days', async () => {
@@ -536,7 +532,7 @@ describe('MediaFilterController', () => {
}),
]),
);
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
});
});
@@ -555,7 +551,7 @@ describe('MediaFilterController', () => {
{ when: {} },
);
expect(viewManager.setViewByParametersWithExistingQuery).not.toBeCalled();
expect(viewManager.setViewByParametersWithExistingQuery).not.toHaveBeenCalled();
});
describe('with events media type', () => {
@@ -593,7 +589,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
params: expect.objectContaining({
camera: 'camera.kitchen',
}),
@@ -615,7 +611,7 @@ describe('MediaFilterController', () => {
limit: 11,
});
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
},
);
});
@@ -647,7 +643,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
params: expect.objectContaining({
camera: 'camera.kitchen',
}),
@@ -664,7 +660,7 @@ describe('MediaFilterController', () => {
limit: 11,
});
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('with reviews media type', async () => {
@@ -687,7 +683,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalled();
const nodes = getQueryNodes(viewManager);
expect(nodes).toHaveLength(1);
@@ -717,7 +713,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalled();
const nodes = getQueryNodes(viewManager);
expect(nodes).toHaveLength(1);
@@ -746,7 +742,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalled();
const nodes = getQueryNodes(viewManager);
expect(nodes).toHaveLength(2);
@@ -772,7 +768,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
params: expect.objectContaining({
camera: 'camera.kitchen',
}),
@@ -805,7 +801,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
params: expect.objectContaining({
camera: 'camera.kitchen',
}),
@@ -837,7 +833,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalled();
const nodes = getQueryNodes(viewManager);
// All 4 types selected for the single camera
@@ -869,7 +865,7 @@ describe('MediaFilterController', () => {
},
);
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
expect(viewManager.setViewByParametersWithExistingQuery).toHaveBeenCalledWith({
params: {
query: expect.any(UnifiedQuery),
},
@@ -100,7 +100,7 @@ describe('MediaGridController', () => {
it('should be constructable', () => {
const controller = createController(createParent());
expect(controller).toBeTruthy();
expect(masonry.layout).toBeCalled();
expect(masonry.layout).toHaveBeenCalled();
});
it('should set grid contents correctly from regular elements', () => {
@@ -115,7 +115,7 @@ describe('MediaGridController', () => {
]),
);
expect(controller.getGridSize()).toBe(3);
expect(masonry.layout).toBeCalled();
expect(masonry.layout).toHaveBeenCalled();
});
it('should set grid contents correctly from slotted elements', () => {
@@ -205,12 +205,12 @@ describe('MediaGridController', () => {
}
// The grid signals its own state change via media-grid:unselected.
expect(unselectedHandler).toBeCalledTimes(1);
expect(unselectedHandler).toHaveBeenCalledTimes(1);
// Unselecting a second time should do nothing.
controller.unselectAll();
expect(unselectedHandler).toBeCalledTimes(1);
expect(unselectedHandler).toHaveBeenCalledTimes(1);
});
it('should select in constructor', () => {
@@ -337,7 +337,7 @@ describe('MediaGridController', () => {
const children = createChildren();
const parent = createParent({ children: children });
createController(parent);
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
initLayout: false,
@@ -350,7 +350,7 @@ describe('MediaGridController', () => {
it('should set default column size correctly', () => {
const parent = createParent({ children: createChildren() });
createController(parent);
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 245,
@@ -368,8 +368,8 @@ describe('MediaGridController', () => {
// The cells are unchanged, so the new column width is applied to the
// existing Masonry instance rather than by constructing a new one.
expect(Masonry).toBeCalledTimes(1);
expect(masonry.option).toBeCalledWith(
expect(Masonry).toHaveBeenCalledTimes(1);
expect(masonry.option).toHaveBeenCalledWith(
expect.objectContaining({
columnWidth: 1499,
}),
@@ -384,13 +384,13 @@ describe('MediaGridController', () => {
createSlotHost({ slot: slot, children: createChildren() });
createController(slot);
expect(Masonry).toBeCalledTimes(1);
expect(masonry.destroy).not.toBeCalled();
expect(Masonry).toHaveBeenCalledTimes(1);
expect(masonry.destroy).not.toHaveBeenCalled();
slot.dispatchEvent(new Event('slotchange'));
expect(Masonry).toBeCalledTimes(1);
expect(masonry.destroy).not.toBeCalled();
expect(Masonry).toHaveBeenCalledTimes(1);
expect(masonry.destroy).not.toHaveBeenCalled();
});
it('should rebuild the grid and lay it out when the cells change', () => {
@@ -398,17 +398,17 @@ describe('MediaGridController', () => {
const host = createSlotHost({ slot: slot, children: createChildren() });
createController(slot);
expect(Masonry).toBeCalledTimes(1);
expect(Masonry).toHaveBeenCalledTimes(1);
host.replaceChildren(...createChildren());
slot.dispatchEvent(new Event('slotchange'));
expect(Masonry).toBeCalledTimes(2);
expect(masonry.destroy).toBeCalledTimes(1);
expect(Masonry).toHaveBeenCalledTimes(2);
expect(masonry.destroy).toHaveBeenCalledTimes(1);
// A rebuild leaves the cells unpositioned, so the layout must not be left
// to the throttle.
expect(masonry.layout).toBeCalled();
expect(masonry.layout).toHaveBeenCalled();
});
it('should rebuild the grid when the number of cells changes', () => {
@@ -416,13 +416,13 @@ describe('MediaGridController', () => {
const host = createSlotHost({ slot: slot, children: createChildren() });
createController(slot);
expect(Masonry).toBeCalledTimes(1);
expect(Masonry).toHaveBeenCalledTimes(1);
host.append(...createChildren(['new-cell']));
slot.dispatchEvent(new Event('slotchange'));
expect(Masonry).toBeCalledTimes(2);
expect(masonry.destroy).toBeCalledTimes(1);
expect(Masonry).toHaveBeenCalledTimes(2);
expect(masonry.destroy).toHaveBeenCalledTimes(1);
});
it('should not use more columns than the items ask for', () => {
@@ -431,7 +431,7 @@ describe('MediaGridController', () => {
// The lone item takes the whole grid. Sizing from the width alone would
// give it 1 of 5 columns, with the other 4 left empty.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 3000,
@@ -448,7 +448,7 @@ describe('MediaGridController', () => {
// Sizing from the width alone would give the lone item half of a default
// width card.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 492,
@@ -463,7 +463,7 @@ describe('MediaGridController', () => {
const parent = createParent({ width: 3000 });
createController(parent);
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 3000,
@@ -479,7 +479,7 @@ describe('MediaGridController', () => {
const controller = createController(parent);
controller.setDisplayConfig({ mode: 'grid', grid_columns: 4 });
expect(masonry.option).toBeCalledWith(
expect(masonry.option).toHaveBeenCalledWith(
expect.objectContaining({
columnWidth: 749,
}),
@@ -498,7 +498,7 @@ describe('MediaGridController', () => {
// The items ask for 4 columns: 2 for the selection (the default
// `grid_selected_width_factor`) and 1 for each of its siblings.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 749,
@@ -515,7 +515,7 @@ describe('MediaGridController', () => {
// A selection is normally reserved extra columns, but a lone item cannot be
// wider than the grid and so cannot use them.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 3000,
@@ -533,7 +533,7 @@ describe('MediaGridController', () => {
// 3 columns for the selection and 1 for each sibling exhausts the 5
// columns the width allows.
expect(masonry.option).toBeCalledWith(
expect(masonry.option).toHaveBeenCalledWith(
expect.objectContaining({
columnWidth: 599,
}),
@@ -551,7 +551,7 @@ describe('MediaGridController', () => {
// The items span 4 columns, and the widest needs 2 more when selected.
// Ignoring the width factor would give 4 columns of 1049px.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 699,
@@ -569,7 +569,7 @@ describe('MediaGridController', () => {
// Each item asks for one column: the selection fills exactly one at 0.5 x
// 2, and a half-width sibling still occupies a whole one.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 599,
@@ -584,7 +584,7 @@ describe('MediaGridController', () => {
// Room for a selection is reserved whether or not there is one, so the
// three items ask for 4 columns either way.
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 749,
@@ -596,14 +596,14 @@ describe('MediaGridController', () => {
// Selecting an item would otherwise resize the items the user did not
// interact with.
expect(masonry.option).not.toBeCalled();
expect(masonry.option).not.toHaveBeenCalled();
expect(
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
).toBe('749px');
controller.unselectAll();
expect(masonry.option).not.toBeCalled();
expect(masonry.option).not.toHaveBeenCalled();
expect(
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
).toBe('749px');
@@ -642,8 +642,8 @@ describe('MediaGridController', () => {
// Click is consumed; the controller dispatches the selection request but
// does NOT mutate local state. The authoritative selection is applied by
// the parent via `selectCell` once it propagates back.
expect(clickHandler).not.toBeCalled();
expect(selectedHandler).toBeCalledTimes(1);
expect(clickHandler).not.toHaveBeenCalled();
expect(selectedHandler).toHaveBeenCalledTimes(1);
expect(selectedHandler.mock.calls[0][0].detail).toEqual({ selected: '1' });
expect(controller.getSelected()).toBeNull();
});
@@ -659,7 +659,7 @@ describe('MediaGridController', () => {
children[1].click();
// Click will be allowed through.
expect(clickHandler).toBeCalled();
expect(clickHandler).toHaveBeenCalled();
expect(controller.getSelected()).toBe('1');
});
@@ -668,14 +668,14 @@ describe('MediaGridController', () => {
vi.mocked(masonry.layout)?.mockClear();
triggerResizeObserver('cell');
expect(masonry.layout).toBeCalled();
expect(masonry.layout).toHaveBeenCalled();
});
it('should update masonry column width when host size changes', () => {
const children = createChildren();
const parent = createParent({ children: children });
createController(parent);
expect(Masonry).toBeCalledWith(
expect(Masonry).toHaveBeenCalledWith(
parent,
expect.objectContaining({
columnWidth: 245,
@@ -696,12 +696,12 @@ describe('MediaGridController', () => {
// Masonry should not be recreated, but column width should be updated
// via option() and layout should be called.
expect(Masonry).not.toBeCalled();
expect(masonry.option).toBeCalledWith({ columnWidth: 749 });
expect(Masonry).not.toHaveBeenCalled();
expect(masonry.option).toHaveBeenCalledWith({ columnWidth: 749 });
expect(
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
).toBe('749px');
expect(masonry.layout).toBeCalled();
expect(masonry.layout).toHaveBeenCalled();
// Clear mock state.
vi.mocked(Masonry).mockClear();
@@ -710,9 +710,9 @@ describe('MediaGridController', () => {
// Trigger with the same sizes.
triggerResizeObserver('host');
expect(Masonry).not.toBeCalled();
expect(masonry.option).not.toBeCalled();
expect(masonry.layout).not.toBeCalled();
expect(Masonry).not.toHaveBeenCalled();
expect(masonry.option).not.toHaveBeenCalled();
expect(masonry.layout).not.toHaveBeenCalled();
});
describe('describe should sort grid elements correctly', () => {
@@ -1,6 +1,18 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { MediaHeightController } from '../../src/components-lib/media-height-controller';
import {
MediaHeightController,
SET_HEIGHT_DEBOUNCE_SECONDS,
} from '../../src/components-lib/media-height-controller';
import {
callMutationHandler,
callResizeHandler,
@@ -8,11 +20,6 @@ import {
ResizeObserverMock,
} from '../test-utils';
vi.mock('lodash-es', async () => ({
...(await vi.importActual('lodash-es')),
debounce: vi.fn((fn) => fn),
}));
// @vitest-environment jsdom
describe('MediaHeightController', () => {
beforeAll(() => {
@@ -26,6 +33,11 @@ describe('MediaHeightController', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
describe('should set height', () => {
@@ -47,6 +59,8 @@ describe('MediaHeightController', () => {
controller.setSelected(0);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe(`600px`);
});
@@ -60,6 +74,8 @@ describe('MediaHeightController', () => {
controller.setSelected(10);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('');
});
@@ -86,6 +102,8 @@ describe('MediaHeightController', () => {
},
]);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('800px');
});
@@ -111,6 +129,8 @@ describe('MediaHeightController', () => {
},
]);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('');
});
@@ -137,6 +157,8 @@ describe('MediaHeightController', () => {
controller.setSelected(1);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('200px');
});
@@ -154,6 +176,8 @@ describe('MediaHeightController', () => {
controller.setRoot(root);
controller.setSelected(0);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('700px');
child.getBoundingClientRect = vi.fn().mockReturnValue({
@@ -162,9 +186,30 @@ describe('MediaHeightController', () => {
controller.recalculate();
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('900px');
});
it('should not set height when the selected child has no height', () => {
const host = document.createElement('div');
const controller = new MediaHeightController(host, 'div');
const root = document.createElement('div');
const child = document.createElement('div');
child.getBoundingClientRect = vi.fn().mockReturnValue({
height: 0,
});
root.appendChild(child);
controller.setRoot(root);
controller.setSelected(0);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('');
});
it('should allow height to shrink when selected child is shorter', () => {
const host = document.createElement('div');
const controller = new MediaHeightController(host, 'div');
@@ -184,10 +229,14 @@ describe('MediaHeightController', () => {
controller.setRoot(root);
controller.setSelected(0);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('750px');
controller.setSelected(1);
vi.advanceTimersByTime(SET_HEIGHT_DEBOUNCE_SECONDS * 1000);
expect(host.style.maxHeight).toBe('562px');
});
});
@@ -19,7 +19,7 @@ describe('MediaLoadedInfoSinkController', () => {
getTargetID: () => 'target-1',
});
expect(host.addController).toBeCalledWith(controller);
expect(host.addController).toHaveBeenCalledWith(controller);
});
it('should default to an empty info', () => {
@@ -89,8 +89,8 @@ describe('MediaLoadedInfoSinkController', () => {
selected = 'target-B';
controller.hostUpdated();
expect(callback).toBeCalledWith(infoB);
expect(host.requestUpdate).toBeCalled();
expect(callback).toHaveBeenCalledWith(infoB);
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should not fire callback for non-selected target loads', () => {
@@ -110,8 +110,8 @@ describe('MediaLoadedInfoSinkController', () => {
}),
);
expect(callback).not.toBeCalled();
expect(host.requestUpdate).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should fire callback for selected target loads', () => {
@@ -127,8 +127,8 @@ describe('MediaLoadedInfoSinkController', () => {
const info = createMediaLoadedInfo({ targetID: 'target-A' });
host.dispatchEvent(createMediaLoadedInfoEvent({ info }));
expect(callback).toBeCalledWith(info);
expect(host.requestUpdate).toBeCalled();
expect(callback).toHaveBeenCalledWith(info);
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should not re-fire callback when hostUpdated runs without a targetID change', () => {
@@ -148,7 +148,7 @@ describe('MediaLoadedInfoSinkController', () => {
controller.hostUpdated();
controller.hostUpdated();
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should not fire callback when selection switches between empty targets', () => {
@@ -167,7 +167,7 @@ describe('MediaLoadedInfoSinkController', () => {
selected = 'target-B';
controller.hostUpdated();
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should ignore events whose info has no targetID', () => {
@@ -186,7 +186,7 @@ describe('MediaLoadedInfoSinkController', () => {
}),
);
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
expect(controller.get()).toBeNull();
});
});
@@ -231,8 +231,8 @@ describe('MediaLoadedInfoSinkController', () => {
controller.hostDisconnected();
expect(callback).not.toBeCalled();
expect(host.requestUpdate).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
expect(controller.get()).toBeNull();
});
});
@@ -259,8 +259,8 @@ describe('MediaLoadedInfoSinkController', () => {
ac.abort();
expect(controller.get()).toBeNull();
expect(callback).toBeCalledWith(null);
expect(host.requestUpdate).toBeCalled();
expect(callback).toHaveBeenCalledWith(null);
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should not fire callback when an unselected target aborts', () => {
@@ -290,7 +290,7 @@ describe('MediaLoadedInfoSinkController', () => {
acB.abort();
expect(callback).not.toBeCalled();
expect(callback).not.toHaveBeenCalled();
});
it('should not clobber a newer entry when an older signal aborts', () => {
@@ -17,7 +17,7 @@ describe('MediaLoadedInfoSourceController', () => {
getTargetID: () => 'target-1',
});
expect(host.addController).toBeCalledWith(controller);
expect(host.addController).toHaveBeenCalledWith(controller);
});
describe('set', () => {
@@ -32,7 +32,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.set(createMediaLoadedInfo());
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
it('should dispatch a bubbling, composed event with info+targetID and signal', () => {
@@ -46,7 +46,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.set(createMediaLoadedInfo({ width: 320, height: 240 }));
expect(handler).toBeCalledTimes(1);
expect(handler).toHaveBeenCalledTimes(1);
const ev = handler.mock.calls[0][0] as CustomEvent;
expect(ev.bubbles).toBe(true);
expect(ev.composed).toBe(true);
@@ -85,7 +85,7 @@ describe('MediaLoadedInfoSourceController', () => {
createMediaLoadedInfo({ mediaPlayerController: player, technology: ['hls'] }),
);
expect(handler).toBeCalledTimes(1);
expect(handler).toHaveBeenCalledTimes(1);
});
it('should redispatch when mediaPlayerController reference differs', () => {
@@ -103,7 +103,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.set(createMediaLoadedInfo({ mediaPlayerController: player1 }));
controller.set(createMediaLoadedInfo({ mediaPlayerController: player2 }));
expect(handler).toBeCalledTimes(2);
expect(handler).toHaveBeenCalledTimes(2);
});
it('should redispatch when getTargetID changes between calls', () => {
@@ -120,7 +120,7 @@ describe('MediaLoadedInfoSourceController', () => {
targetID = 'target-2';
controller.set(createMediaLoadedInfo());
expect(handler).toBeCalledTimes(2);
expect(handler).toHaveBeenCalledTimes(2);
expect((handler.mock.calls[0][0] as CustomEvent).detail.info.targetID).toBe(
'target-1',
);
@@ -172,7 +172,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.hostDisconnected();
controller.hostConnected();
expect(handler).toBeCalledTimes(2);
expect(handler).toHaveBeenCalledTimes(2);
const secondSignal = (handler.mock.calls[1][0] as CustomEvent).detail.signal;
// The original signal aborted on disconnect, the new one is fresh.
@@ -196,7 +196,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.hostConnected();
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
it('should not redispatch if a registration is already active', () => {
@@ -212,7 +212,7 @@ describe('MediaLoadedInfoSourceController', () => {
// Active registration, no disconnect -- connect should be a no-op.
controller.hostConnected();
expect(handler).toBeCalledTimes(1);
expect(handler).toHaveBeenCalledTimes(1);
});
it('should not replay stale info after targetID flips during disconnect', () => {
@@ -228,7 +228,7 @@ describe('MediaLoadedInfoSourceController', () => {
host.addEventListener('advanced-camera-card:media:loaded', handler);
controller.set(createMediaLoadedInfo());
expect(handler).toBeCalledTimes(1);
expect(handler).toHaveBeenCalledTimes(1);
controller.hostDisconnected();
@@ -237,11 +237,11 @@ describe('MediaLoadedInfoSourceController', () => {
controller.hostConnected();
// No re-dispatch -- the stale cache was discarded.
expect(handler).toBeCalledTimes(1);
expect(handler).toHaveBeenCalledTimes(1);
// A subsequent set() under the new target dispatches fresh.
controller.set(createMediaLoadedInfo({ width: 320, height: 240 }));
expect(handler).toBeCalledTimes(2);
expect(handler).toHaveBeenCalledTimes(2);
expect((handler.mock.calls[1][0] as CustomEvent).detail.info.targetID).toBe(
'target-2',
);
@@ -265,7 +265,7 @@ describe('MediaLoadedInfoSourceController', () => {
controller.hostDisconnected();
expect(cleanup).toBeCalled();
expect(cleanup).toHaveBeenCalled();
expect(signal.aborted).toBe(true);
});
@@ -90,7 +90,7 @@ describe('ImageMediaPlayerController', () => {
);
expect(await controller.getScreenshotURL()).toBe(url);
expect(screenshotImage).not.toBeCalled();
expect(screenshotImage).not.toHaveBeenCalled();
});
});
@@ -135,7 +135,7 @@ describe('ImageMediaPlayerController', () => {
await controller.playback?.play();
expect(updateControl.start).toBeCalled();
expect(updateControl.start).toHaveBeenCalled();
});
it('should stop the update loop on pause', async () => {
@@ -148,7 +148,7 @@ describe('ImageMediaPlayerController', () => {
await controller.playback?.pause();
expect(updateControl.stop).toBeCalled();
expect(updateControl.stop).toHaveBeenCalled();
});
it('should report paused when the update loop is not running', () => {
@@ -23,7 +23,7 @@ describe('JSMPEGMediaPlayerController', () => {
await controller.playback.play();
expect(videoElement.play).toBeCalled();
expect(videoElement.play).toHaveBeenCalled();
});
it('should pause', async () => {
@@ -37,7 +37,7 @@ describe('JSMPEGMediaPlayerController', () => {
await controller.playback.pause();
expect(videoElement.stop).toBeCalled();
expect(videoElement.stop).toHaveBeenCalled();
});
describe('should mute', async () => {
@@ -80,7 +80,7 @@ describe('VideoMediaPlayerController', () => {
await controller.playback.play();
expect(video.play).toBeCalled();
expect(video.play).toHaveBeenCalled();
});
it('should mute if not allowed to play and unmuted', async () => {
@@ -92,7 +92,7 @@ describe('VideoMediaPlayerController', () => {
await controller.playback.play();
expect(video.play).toBeCalledTimes(2);
expect(video.play).toHaveBeenCalledTimes(2);
expect(video.muted).toBeTruthy();
});
@@ -105,7 +105,7 @@ describe('VideoMediaPlayerController', () => {
await controller.playback.play();
expect(video.play).toBeCalledTimes(1);
expect(video.play).toHaveBeenCalledTimes(1);
expect(video.muted).toBeTruthy();
});
@@ -118,7 +118,7 @@ describe('VideoMediaPlayerController', () => {
await controller.playback.play();
expect(video.play).toBeCalledTimes(2);
expect(video.play).toHaveBeenCalledTimes(2);
expect(video.muted).toBeTruthy();
});
@@ -137,7 +137,7 @@ describe('VideoMediaPlayerController', () => {
await controller.playback.pause();
expect(video.pause).toBeCalled();
expect(video.pause).toHaveBeenCalled();
});
describe('should mute', async () => {
@@ -211,7 +211,7 @@ describe('VideoMediaPlayerController', () => {
await controller.seek(10);
expect(hideMediaControlsTemporarily).toBeCalled();
expect(hideMediaControlsTemporarily).toHaveBeenCalled();
expect(video.currentTime).toBe(10);
});
@@ -231,7 +231,7 @@ describe('VideoMediaPlayerController', () => {
await controller.setControls(true);
expect(setControlsOnVideo).toBeCalledWith(video, true);
expect(setControlsOnVideo).toHaveBeenCalledWith(video, true);
});
it('should set controls to default', async () => {
@@ -244,7 +244,7 @@ describe('VideoMediaPlayerController', () => {
await controller.setControls();
expect(setControlsOnVideo).toBeCalledWith(video, true);
expect(setControlsOnVideo).toHaveBeenCalledWith(video, true);
});
it('should ignore calls without a default or value', async () => {
@@ -252,7 +252,7 @@ describe('VideoMediaPlayerController', () => {
await controller.setControls(true);
expect(setControlsOnVideo).not.toBeCalled();
expect(setControlsOnVideo).not.toHaveBeenCalled();
});
});
@@ -15,7 +15,8 @@ import type { NotificationControl } from '../../../src/config/schema/actions/typ
import { formatDateAndTime } from '../../../src/utils/basic';
import { downloadMedia, navigateToTimeline } from '../../../src/utils/media-actions';
import { ViewFolder, ViewMediaType } from '../../../src/view/item';
import { createCardAPI, createFolder, TestViewMedia } from '../../test-utils';
import { createCardAPI, createFolder } from '../../test-utils';
import { TestViewMedia } from '../../view/test-utils';
vi.mock('../../../src/utils/media-actions', async (importOriginal) => ({
...((await importOriginal()) as object),
@@ -38,19 +38,19 @@ import {
} from '../../src/view/view-support.js';
import type { View } from '../../src/view/view.js';
import {
createCameraConfig,
createCameraManager,
createCapabilities,
createConfig,
createStore,
} from '../camera-manager/test-utils';
import { createCameraConfig, createConfig } from '../config/test-utils';
import {
createFolder,
createHASS,
createMediaCapabilities,
createMediaLoadedInfo,
createStateEntity,
createStore,
createView,
TestViewMedia,
} from '../test-utils.js';
import { createView, TestViewMedia } from '../view/test-utils';
vi.mock('../../src/view/view-support.js');
vi.mock('../../src/utils/media-player-controller.js');
+8 -8
View File
@@ -88,7 +88,7 @@ describe('MenuController', () => {
vi.mocked(host.requestUpdate).mockClear();
controller.setLockManagerEpoch(createLock(true, true));
expect(host.requestUpdate).toBeCalledTimes(1);
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
});
it('should not trigger update when lock epoch is unchanged', () => {
@@ -99,7 +99,7 @@ describe('MenuController', () => {
vi.mocked(host.requestUpdate).mockClear();
controller.setLockManagerEpoch({ manager: lock.manager, locked: lock.locked });
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should reflect lock state in shouldButtonBeInert', () => {
@@ -406,7 +406,7 @@ describe('MenuController', () => {
];
controller.setButtons(buttons);
expect(host.requestUpdate).toBeCalledTimes(1);
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
controller.setButtons([
{
@@ -414,7 +414,7 @@ describe('MenuController', () => {
icon: 'mdi:cow',
},
]);
expect(host.requestUpdate).toBeCalledTimes(1);
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
});
});
@@ -535,7 +535,7 @@ describe('MenuController', () => {
const controller = new MenuController(host);
controller.handleAction(createInteractionActionEvent('tap'));
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
it('should execute simple action in non-hidden menu', () => {
@@ -546,7 +546,7 @@ describe('MenuController', () => {
const controller = new MenuController(host);
controller.handleAction(createInteractionActionEvent('tap'), tapActionConfig);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { actions: [action], config: tapActionConfig },
}),
@@ -564,7 +564,7 @@ describe('MenuController', () => {
controller.handleAction(
createSubmenuInteractionActionEvent('tap', tapActionConfig as SubmenuItem),
);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { actions: [action], config: tapActionConfig },
}),
@@ -580,7 +580,7 @@ describe('MenuController', () => {
controller.handleAction(createInteractionActionEvent('tap'), tapActionConfigMulti);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { actions: [action, action, action], config: tapActionConfigMulti },
}),
+19 -15
View File
@@ -22,12 +22,8 @@ import { QuerySource } from '../../src/query-source';
import { ViewFolder, ViewMedia } from '../../src/view/item';
import { UnifiedQuery } from '../../src/view/unified-query';
import { UnifiedQueryBuilder } from '../../src/view/unified-query-builder';
import {
createCardAPI,
createFolder,
createView,
createViewWithMedia,
} from '../test-utils';
import { createCardAPI, createFolder } from '../test-utils';
import { createView, createViewWithMedia } from '../view/test-utils';
const createFolderQuery = (
folder: ReturnType<typeof createFolder>,
@@ -83,7 +79,9 @@ describe('navigateUp', () => {
navigateUp(options);
expect(api.getViewManager().setViewByParametersWithExistingQuery).not.toBeCalled();
expect(
api.getViewManager().setViewByParametersWithExistingQuery,
).not.toHaveBeenCalled();
});
it('should ignore folder query without parent to go up to', () => {
@@ -109,7 +107,9 @@ describe('navigateUp', () => {
navigateUp(options);
expect(api.getViewManager().setViewByParametersWithExistingQuery).not.toBeCalled();
expect(
api.getViewManager().setViewByParametersWithExistingQuery,
).not.toHaveBeenCalled();
});
it('should go up in the folder hierarchy', () => {
@@ -139,7 +139,9 @@ describe('navigateUp', () => {
navigateUp(options);
expect(api.getViewManager().setViewByParametersWithExistingQuery).toBeCalledWith({
expect(
api.getViewManager().setViewByParametersWithExistingQuery,
).toHaveBeenCalledWith({
params: {
query: expect.any(UnifiedQuery),
},
@@ -220,7 +222,9 @@ describe('navigateToFolder', () => {
const item = new ViewFolder(folder, [{ ha: { id: 'root' } }]);
navigateToFolder(item, options);
expect(api.getViewManager().setViewByParametersWithExistingQuery).toBeCalledWith({
expect(
api.getViewManager().setViewByParametersWithExistingQuery,
).toHaveBeenCalledWith({
params: {
query: expect.any(UnifiedQuery),
},
@@ -317,7 +321,7 @@ describe('navigateToMedia', () => {
navigateToMedia(media, options);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect(api.getViewManager().setViewByParameters).toHaveBeenCalledWith(
expect.objectContaining({
params: expect.objectContaining({
view: 'media',
@@ -364,7 +368,7 @@ describe('navigateToMedia', () => {
navigateToMedia(media, options);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect(api.getViewManager().setViewByParameters).toHaveBeenCalledWith(
expect.objectContaining({
params: expect.objectContaining({
view: 'media',
@@ -391,7 +395,7 @@ describe('navigateToMedia', () => {
navigateToMedia(media, options);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect(api.getViewManager().setViewByParameters).toHaveBeenCalledWith(
expect.objectContaining({
modifiers: [modifier],
}),
@@ -412,7 +416,7 @@ describe('navigateToMedia', () => {
navigateToMedia(media, options);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toHaveBeenCalled();
});
it('should do nothing if view is missing', () => {
@@ -428,6 +432,6 @@ describe('navigateToMedia', () => {
navigateToMedia(media, options);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toHaveBeenCalled();
});
});
@@ -29,7 +29,7 @@ describe('handleControlAction', () => {
const host = document.createElement('div');
handleControlAction(ev, createControl(), host);
expect(stopEventFromActivatingCardWideActions).toBeCalledWith(ev);
expect(stopEventFromActivatingCardWideActions).toHaveBeenCalledWith(ev);
});
it('should dispatch action when getActionConfigGivenAction returns an action', () => {
@@ -42,7 +42,7 @@ describe('handleControlAction', () => {
handleControlAction(ev, control, host);
expect(dispatchActionExecutionRequest).toBeCalledWith(host, {
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(host, {
actions: [action],
});
});
@@ -55,7 +55,7 @@ describe('handleControlAction', () => {
handleControlAction(ev, createControl(), host);
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should call onDismiss when dismiss is not false', () => {
@@ -67,7 +67,7 @@ describe('handleControlAction', () => {
handleControlAction(ev, createControl({ dismiss: true }), host, onDismiss);
expect(onDismiss).toBeCalled();
expect(onDismiss).toHaveBeenCalled();
});
it('should not call onDismiss when dismiss is false', () => {
@@ -79,7 +79,7 @@ describe('handleControlAction', () => {
handleControlAction(ev, createControl({ dismiss: false }), host, onDismiss);
expect(onDismiss).not.toBeCalled();
expect(onDismiss).not.toHaveBeenCalled();
});
it('should not call onDismiss when no onDismiss is provided', () => {
@@ -45,7 +45,7 @@ describe('PTZDragController', () => {
it('should register as a controller on the host', () => {
const host = createLitElement();
new PTZDragController(host);
expect(host.addController).toBeCalled();
expect(host.addController).toHaveBeenCalled();
});
describe('activation', () => {
@@ -56,7 +56,7 @@ describe('PTZDragController', () => {
const element = document.createElement('div');
controller.activateIfNecessary(element);
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should set cursor and touch-action styles on the element', () => {
@@ -93,7 +93,7 @@ describe('PTZDragController', () => {
controller.activateIfNecessary(element);
controller.activateIfNecessary(element);
expect(createGesture).toBeCalledTimes(1);
expect(createGesture).toHaveBeenCalledTimes(1);
});
it('should create gesture with drag, pinch, and wheel actions', () => {
@@ -102,7 +102,7 @@ describe('PTZDragController', () => {
controller.activateIfNecessary(document.createElement('div'));
expect(createGesture).toBeCalled();
expect(createGesture).toHaveBeenCalled();
});
});
@@ -117,7 +117,7 @@ describe('PTZDragController', () => {
controller.deactivateIfNecessary();
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should destroy the gesture recognizer', () => {
@@ -127,7 +127,7 @@ describe('PTZDragController', () => {
controller.activateIfNecessary(document.createElement('div'));
controller.deactivateIfNecessary();
expect(destroy).toBeCalled();
expect(destroy).toHaveBeenCalled();
});
it('should not deactivate when not active', () => {
@@ -136,7 +136,7 @@ describe('PTZDragController', () => {
controller.deactivateIfNecessary();
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should stop active directions on deactivation', () => {
@@ -154,11 +154,11 @@ describe('PTZDragController', () => {
controller.deactivateIfNecessary();
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down', 'stop'),
);
@@ -175,7 +175,7 @@ describe('PTZDragController', () => {
controller.deactivateIfNecessary();
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_in', 'stop'),
);
@@ -190,7 +190,7 @@ describe('PTZDragController', () => {
controller.activateIfNecessary(document.createElement('div'));
controller.hostDisconnected();
expect(destroy).toBeCalled();
expect(destroy).toHaveBeenCalled();
});
});
@@ -208,7 +208,7 @@ describe('PTZDragController', () => {
movement: [110, 0],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'start'),
);
@@ -226,7 +226,7 @@ describe('PTZDragController', () => {
movement: [-110, 0],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('right', 'start'),
);
@@ -244,7 +244,7 @@ describe('PTZDragController', () => {
movement: [0, 110],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('up', 'start'),
);
@@ -262,7 +262,7 @@ describe('PTZDragController', () => {
movement: [0, -110],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down', 'start'),
);
@@ -280,11 +280,11 @@ describe('PTZDragController', () => {
movement: [110, -110],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'start'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down', 'start'),
);
@@ -309,7 +309,7 @@ describe('PTZDragController', () => {
movement: [120, 0],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should stop old and start new on X direction reversal', () => {
@@ -331,11 +331,11 @@ describe('PTZDragController', () => {
movement: [-110, 0],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('right', 'start'),
);
@@ -360,11 +360,11 @@ describe('PTZDragController', () => {
movement: [0, -110],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('up', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down', 'start'),
);
@@ -390,15 +390,15 @@ describe('PTZDragController', () => {
movement: [0, 0],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('up', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledTimes(2);
expect(dispatchActionExecutionRequest).toHaveBeenCalledTimes(2);
});
it('should stop active directions on drag end', () => {
@@ -420,11 +420,11 @@ describe('PTZDragController', () => {
movement: [110, -110],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down', 'stop'),
);
@@ -450,8 +450,8 @@ describe('PTZDragController', () => {
});
// Only the stop is dispatched, not a relative action.
expect(dispatchActionExecutionRequest).toBeCalledTimes(1);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledTimes(1);
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
@@ -471,7 +471,7 @@ describe('PTZDragController', () => {
movement: [30, -20],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should dispatch relative left and down on small drag end', () => {
@@ -491,8 +491,14 @@ describe('PTZDragController', () => {
movement: [30, -20],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('left'));
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('down'));
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left'),
);
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('down'),
);
});
it('should dispatch relative right and up on small drag end', () => {
@@ -512,8 +518,14 @@ describe('PTZDragController', () => {
movement: [-30, 20],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('right'));
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('up'));
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('right'),
);
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('up'),
);
});
it('should not dispatch relative on zero movement', () => {
@@ -528,7 +540,7 @@ describe('PTZDragController', () => {
movement: [0, 0],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
});
@@ -545,7 +557,7 @@ describe('PTZDragController', () => {
movement: [80, 80],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should stop active continuous directions when pinch starts', () => {
@@ -567,7 +579,7 @@ describe('PTZDragController', () => {
movement: [120, 0],
});
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('left', 'stop'),
);
@@ -593,7 +605,7 @@ describe('PTZDragController', () => {
movement: [110, 0],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should resume drag handling after poisoned gesture ends', () => {
@@ -616,7 +628,7 @@ describe('PTZDragController', () => {
movement: [80, 0],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
});
@@ -632,7 +644,7 @@ describe('PTZDragController', () => {
movement: [0, 0],
});
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
});
@@ -645,7 +657,7 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [1], last: false });
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_in', 'start'),
);
@@ -659,7 +671,7 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [-1], last: false });
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_out', 'start'),
);
@@ -676,11 +688,11 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [-1], last: false });
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_in', 'stop'),
);
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_out', 'start'),
);
@@ -697,7 +709,7 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [1], last: true });
expect(dispatchActionExecutionRequest).toBeCalledWith(
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_in', 'stop'),
);
@@ -711,7 +723,7 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [0], last: false });
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
it('should not re-dispatch when zoom direction is unchanged', () => {
@@ -725,7 +737,7 @@ describe('PTZDragController', () => {
getHandlers().onPinch({ direction: [1], last: false });
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
});
@@ -738,7 +750,10 @@ describe('PTZDragController', () => {
getHandlers().onWheel({ delta: [0, 100] });
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('zoom_out'));
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_out'),
);
});
it('should dispatch zoom_in on scroll up', () => {
@@ -749,7 +764,10 @@ describe('PTZDragController', () => {
getHandlers().onWheel({ delta: [0, -100] });
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('zoom_in'));
expect(dispatchActionExecutionRequest).toHaveBeenCalledWith(
host,
ptzAction('zoom_in'),
);
});
it('should not dispatch on zero delta', () => {
@@ -760,7 +778,7 @@ describe('PTZDragController', () => {
getHandlers().onWheel({ delta: [0, 0] });
expect(dispatchActionExecutionRequest).not.toBeCalled();
expect(dispatchActionExecutionRequest).not.toHaveBeenCalled();
});
});
@@ -8,7 +8,11 @@ import {
type PTZControlsConfig,
} from '../../../src/config/schema/common/controls/ptz';
import { PTZMovementType } from '../../../src/types';
import { createCameraManager, createCapabilities, createStore } from '../../test-utils';
import {
createCameraManager,
createCapabilities,
createStore,
} from '../../camera-manager/test-utils';
const createConfig = (config?: Partial<PTZControlsConfig>): PTZControlsConfig => {
return ptzControlsConfigSchema.parse({
@@ -405,8 +409,8 @@ describe('PTZController', () => {
controller.toggleTypeHandler(ev, 'gestures');
expect(ev.stopPropagation).toBeCalled();
expect(handler).toBeCalledWith(
expect(ev.stopPropagation).toHaveBeenCalled();
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
@@ -429,7 +433,7 @@ describe('PTZController', () => {
controller.toggleTypeHandler(ev, 'buttons');
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
@@ -452,7 +456,7 @@ describe('PTZController', () => {
controller.toggleTypeHandler(ev);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
@@ -487,7 +491,7 @@ describe('PTZController', () => {
config,
);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: action,
@@ -508,7 +512,7 @@ describe('PTZController', () => {
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
);
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
it('should not call action without hass', () => {
@@ -522,7 +526,7 @@ describe('PTZController', () => {
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
);
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
});
});
@@ -30,7 +30,7 @@ describe('SignedURLController', () => {
const host = mock<ReactiveControllerHost>();
const controller = new SignedURLController(host, () => ({}));
expect(host.addController).toBeCalledWith(controller);
expect(host.addController).toHaveBeenCalledWith(controller);
expect(controller.getValue()).toBeNull();
});
@@ -65,7 +65,7 @@ describe('SignedURLController', () => {
expect(controller.getValue()).toBeNull();
expect(createProxiedEndpointIfNecessary).toBeCalledWith(
expect(createProxiedEndpointIfNecessary).toHaveBeenCalledWith(
hass,
{ endpoint: 'http://test-url.com/', sign: false },
proxyConfig,
@@ -74,9 +74,9 @@ describe('SignedURLController', () => {
await flushPromises();
expect(homeAssistantGetSignedURLIfNecessary).toBeCalled();
expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalled();
expect(controller.getValue()).toBe('http://signed-proxied-url.com');
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should not fetch if inputs are missing', async () => {
@@ -86,7 +86,7 @@ describe('SignedURLController', () => {
controller.hostUpdate();
await flushPromises();
expect(createProxiedEndpointIfNecessary).not.toBeCalled();
expect(createProxiedEndpointIfNecessary).not.toHaveBeenCalled();
expect(controller.getValue()).toBeNull();
});
@@ -102,7 +102,7 @@ describe('SignedURLController', () => {
controller.hostUpdate();
await flushPromises();
expect(createProxiedEndpointIfNecessary).not.toBeCalled();
expect(createProxiedEndpointIfNecessary).not.toHaveBeenCalled();
expect(controller.getValue()).toBe('http://test-url.com');
});
@@ -208,7 +208,7 @@ describe('SignedURLController', () => {
await flushPromises();
expect(controller.getValue()).toBeNull();
expect(valueChangeCallback).not.toBeCalled();
expect(valueChangeCallback).not.toHaveBeenCalled();
});
it('should not call valueChangeCallback on null signed URL', async () => {
@@ -243,7 +243,7 @@ describe('SignedURLController', () => {
await flushPromises();
expect(controller.getValue()).toBeNull();
expect(valueChangeCallback).not.toBeCalled();
expect(valueChangeCallback).not.toHaveBeenCalled();
});
it('should ignore successful fetch if inputs become invalid', async () => {
@@ -281,7 +281,7 @@ describe('SignedURLController', () => {
await flushPromises();
expect(controller.getValue()).toBeNull();
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should invalidate cache if input changes', async () => {
@@ -401,7 +401,7 @@ describe('SignedURLController', () => {
expect(controller.getValue()).toBeNull();
expect(controller.getError()).toBe('proxy');
expect(host.requestUpdate).toBeCalledTimes(1);
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
});
it('should not retry after sign error with same inputs', async () => {
@@ -677,7 +677,7 @@ describe('SignedURLController', () => {
rejectProxy?.(new Error('fail'));
await flushPromises();
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should clear value if proxy endpoint is null', async () => {
@@ -704,7 +704,7 @@ describe('SignedURLController', () => {
expect(controller.getValue()).toBeNull();
expect(controller.getError()).toBe('proxy');
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should not retry after proxy error with same inputs', async () => {
@@ -881,7 +881,7 @@ describe('SignedURLController', () => {
await flushPromises();
expect(controller.getValue()).toBeNull();
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should ignore stale null signed URL after request ID changed', async () => {
@@ -925,7 +925,7 @@ describe('SignedURLController', () => {
await flushPromises();
// Stale result should be discarded.
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
it('should sign endpoint without proxying when sign is set', async () => {
@@ -946,10 +946,10 @@ describe('SignedURLController', () => {
controller.hostUpdate();
await flushPromises();
expect(createProxiedEndpointIfNecessary).not.toBeCalled();
expect(homeAssistantGetSignedURLIfNecessary).toBeCalled();
expect(createProxiedEndpointIfNecessary).not.toHaveBeenCalled();
expect(homeAssistantGetSignedURLIfNecessary).toHaveBeenCalled();
expect(controller.getValue()).toBe('http://ha.local/api/some/endpoint?authSig=abc');
expect(host.requestUpdate).toBeCalled();
expect(host.requestUpdate).toHaveBeenCalled();
});
it('should return url directly when sign is false and proxy is disabled', () => {
@@ -998,7 +998,7 @@ describe('SignedURLController', () => {
resolveProxy?.(null);
await flushPromises();
expect(host.requestUpdate).not.toBeCalled();
expect(host.requestUpdate).not.toHaveBeenCalled();
});
});
@@ -403,7 +403,7 @@ describe('StatusBarController', () => {
const controller = new StatusBarController(host);
controller.actionHandler(createInteractionActionEvent('tap'));
expect(handler).not.toBeCalled();
expect(handler).not.toHaveBeenCalled();
});
it('should request action execution', () => {
@@ -422,7 +422,7 @@ describe('StatusBarController', () => {
controller.actionHandler(createInteractionActionEvent('tap'), tapActionConfig);
expect(handler).toBeCalledWith(
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { actions: [action], config: tapActionConfig },
}),
@@ -5,7 +5,8 @@ import { mock } from 'vitest-mock-extended';
import type { CameraManager } from '../../../../src/camera-manager/manager';
import { ThumbnailFeatureController } from '../../../../src/components-lib/thumbnail/feature/controller';
import { ViewFolder } from '../../../../src/view/item';
import { createFolder, TestViewMedia } from '../../../test-utils';
import { createFolder } from '../../../test-utils';
import { TestViewMedia } from '../../../view/test-utils';
describe('ThumbnailFeatureController', () => {
const itemWithTime = new TestViewMedia({
+3 -6
View File
@@ -38,12 +38,9 @@ import type { ConditionStateManagerReadonlyInterface } from '../../../src/condit
import { QuerySource } from '../../../src/query-source';
import { ViewMediaType } from '../../../src/view/item';
import { UnifiedQuery, type QueryNode } from '../../../src/view/unified-query';
import {
createCameraManager,
createFolder,
createStore,
TestViewMedia,
} from '../../test-utils';
import { createCameraManager, createStore } from '../../camera-manager/test-utils';
import { createFolder } from '../../test-utils';
import { TestViewMedia } from '../../view/test-utils';
const CAMERA_ID = 'camera-1';
const TEST_MEDIA_ID = 'TEST_MEDIA_ID';
@@ -83,7 +83,7 @@ describe('ZoomController', () => {
// Won't zoom without control key.
const ev_1 = new WheelEvent('wheel', { bubbles: false, deltaY: -120 });
element.dispatchEvent(ev_1);
expect(panzoom.zoomWithWheel).not.toBeCalled();
expect(panzoom.zoomWithWheel).not.toHaveBeenCalled();
const ev_2 = new WheelEvent('wheel', {
bubbles: false,
@@ -91,21 +91,21 @@ describe('ZoomController', () => {
ctrlKey: true,
});
element.dispatchEvent(ev_2);
expect(panzoom.zoomWithWheel).toBeCalledWith(ev_2);
expect(panzoom.zoomWithWheel).toHaveBeenCalledWith(ev_2);
panzoom.getScale = vi.fn().mockReturnValue(1.2);
const ev_3 = new PointerEvent('pointerdown');
element.dispatchEvent(ev_3);
expect(panzoom.handleDown).toBeCalledWith(ev_3);
expect(panzoom.handleDown).toHaveBeenCalledWith(ev_3);
const ev_4 = new PointerEvent('pointermove');
element.dispatchEvent(ev_4);
expect(panzoom.handleMove).toBeCalledWith(ev_4);
expect(panzoom.handleMove).toHaveBeenCalledWith(ev_4);
const ev_5 = new PointerEvent('pointerup');
element.dispatchEvent(ev_5);
expect(panzoom.handleUp).toBeCalledWith(ev_5);
expect(panzoom.handleUp).toHaveBeenCalledWith(ev_5);
});
it('should not respond to pointer when not zoomed', () => {
@@ -118,15 +118,15 @@ describe('ZoomController', () => {
const ev_1 = new PointerEvent('pointerdown');
element.dispatchEvent(ev_1);
expect(panzoom.handleDown).not.toBeCalledWith(ev_1);
expect(panzoom.handleDown).not.toHaveBeenCalledWith(ev_1);
const ev_2 = new PointerEvent('pointermove');
element.dispatchEvent(ev_2);
expect(panzoom.handleDown).not.toBeCalledWith(ev_2);
expect(panzoom.handleDown).not.toHaveBeenCalledWith(ev_2);
const ev_3 = new PointerEvent('pointerup');
element.dispatchEvent(ev_3);
expect(panzoom.handleDown).not.toBeCalledWith(ev_3);
expect(panzoom.handleDown).not.toHaveBeenCalledWith(ev_3);
});
it('should respond with touch', () => {
@@ -143,21 +143,21 @@ describe('ZoomController', () => {
touches: [createTouch({ target: element }), createTouch({ target: element })],
});
element.dispatchEvent(ev_1);
expect(panzoom.handleDown).toBeCalledWith(ev_1);
expect(panzoom.handleDown).toHaveBeenCalledWith(ev_1);
panzoom.getScale = vi.fn().mockReturnValue(1.2);
const ev_3 = createTouchEvent('touchstart');
element.dispatchEvent(ev_3);
expect(panzoom.handleDown).toBeCalledWith(ev_3);
expect(panzoom.handleDown).toHaveBeenCalledWith(ev_3);
const ev_4 = createTouchEvent('touchmove');
element.dispatchEvent(ev_4);
expect(panzoom.handleMove).toBeCalledWith(ev_4);
expect(panzoom.handleMove).toHaveBeenCalledWith(ev_4);
const ev_5 = createTouchEvent('touchend');
element.dispatchEvent(ev_5);
expect(panzoom.handleUp).toBeCalledWith(ev_5);
expect(panzoom.handleUp).toHaveBeenCalledWith(ev_5);
});
});
@@ -179,7 +179,7 @@ describe('ZoomController', () => {
// A click on its own will be fine.
const click_1 = new MouseEvent('click', { bubbles: true });
inner.dispatchEvent(click_1);
expect(clickHandler).toBeCalledTimes(1);
expect(clickHandler).toHaveBeenCalledTimes(1);
// A click after a pointerdown will be ignored.
const pointerdown_1 = new PointerEvent('pointerdown');
@@ -189,7 +189,7 @@ describe('ZoomController', () => {
inner.dispatchEvent(click_2);
// Click will have been ignored.
//expect(clickHandler).toBeCalledTimes(1);
//expect(clickHandler).toHaveBeenCalledTimes(1);
// Simulate being zoomed out.
panzoom.getScale = vi.fn().mockReturnValue(1.0);
@@ -200,7 +200,7 @@ describe('ZoomController', () => {
inner.dispatchEvent(click_3);
// Click will have been processed.
expect(clickHandler).toBeCalledTimes(2);
expect(clickHandler).toHaveBeenCalledTimes(2);
});
it('deactivate should remove event handlers', () => {
@@ -217,7 +217,7 @@ describe('ZoomController', () => {
ctrlKey: true,
});
element.dispatchEvent(ev_1);
expect(panzoom.zoomWithWheel).not.toBeCalled();
expect(panzoom.zoomWithWheel).not.toHaveBeenCalled();
});
describe('should fire events', () => {
@@ -241,8 +241,8 @@ describe('ZoomController', () => {
},
});
element.dispatchEvent(ev_1);
expect(zoomedFunc).toBeCalled();
expect(unzoomedFunc).not.toBeCalled();
expect(zoomedFunc).toHaveBeenCalled();
expect(unzoomedFunc).not.toHaveBeenCalled();
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
detail: {
@@ -254,7 +254,7 @@ describe('ZoomController', () => {
},
});
element.dispatchEvent(ev_2);
expect(unzoomedFunc).toBeCalled();
expect(unzoomedFunc).toHaveBeenCalled();
});
it('when state has not changed or spurious events received', () => {
@@ -279,8 +279,8 @@ describe('ZoomController', () => {
element.dispatchEvent(ev_1);
// Unzoomed event with scale === 1, this._zoomed will already be false.
expect(unzoomedFunc).not.toBeCalled();
expect(zoomedFunc).not.toBeCalled();
expect(unzoomedFunc).not.toHaveBeenCalled();
expect(zoomedFunc).not.toHaveBeenCalled();
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
detail: {
@@ -292,12 +292,12 @@ describe('ZoomController', () => {
},
});
element.dispatchEvent(ev_2);
expect(zoomedFunc).toBeCalledTimes(1);
expect(unzoomedFunc).not.toBeCalled();
expect(zoomedFunc).toHaveBeenCalledTimes(1);
expect(unzoomedFunc).not.toHaveBeenCalled();
// Another call when already zoomed will be ignored.
element.dispatchEvent(ev_2);
expect(zoomedFunc).toBeCalledTimes(1);
expect(zoomedFunc).toHaveBeenCalledTimes(1);
});
describe('on default/non-default', () => {
@@ -453,11 +453,11 @@ describe('ZoomController', () => {
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
// Controller was not activated, config setting will not update pan/zoom.
expect(panzoom.zoom).not.toBeCalled();
expect(panzoom.pan).not.toBeCalled();
expect(panzoom.zoom).not.toHaveBeenCalled();
expect(panzoom.pan).not.toHaveBeenCalled();
controller.activate();
expect(Panzoom).toBeCalledWith(
expect(Panzoom).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
contain: 'outside',
@@ -485,8 +485,8 @@ describe('ZoomController', () => {
triggerResizeObserver();
expect(panzoom.zoom).toBeCalledWith(2, { animate: false });
expect(panzoom.pan).toBeCalledWith(115.62, 63.6525, {
expect(panzoom.zoom).toHaveBeenCalledWith(2, { animate: false });
expect(panzoom.pan).toHaveBeenCalledWith(115.62, 63.6525, {
animate: true,
duration: 100,
});
@@ -504,8 +504,8 @@ describe('ZoomController', () => {
// This call will do nothing since this is what zoom/pan already are.
controller.setDefaultSettings({ zoom: 1, pan: { x: 0, y: 0 } });
expect(panzoom.zoom).not.toBeCalled();
expect(panzoom.pan).not.toBeCalled();
expect(panzoom.zoom).not.toHaveBeenCalled();
expect(panzoom.pan).not.toHaveBeenCalled();
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
@@ -547,8 +547,8 @@ describe('ZoomController', () => {
controller.setSettings({ zoom: 2 });
expect(panzoom.zoom).toBeCalledTimes(1);
expect(panzoom.pan).toBeCalledTimes(1);
expect(panzoom.zoom).toHaveBeenCalledTimes(1);
expect(panzoom.pan).toHaveBeenCalledTimes(1);
expect(panzoom.zoom).toHaveBeenNthCalledWith(1, 2, { animate: false });
expect(panzoom.pan).toHaveBeenNthCalledWith(1, 0, 0, {
animate: true,
@@ -559,8 +559,8 @@ describe('ZoomController', () => {
vi.mocked(panzoom.getPan).mockReturnValue({ x: 0, y: 0 });
controller.setSettings({ zoom: 2 });
expect(panzoom.zoom).toBeCalledTimes(1);
expect(panzoom.pan).toBeCalledTimes(1);
expect(panzoom.zoom).toHaveBeenCalledTimes(1);
expect(panzoom.pan).toHaveBeenCalledTimes(1);
});
it('when config is set to empty', () => {
@@ -577,8 +577,8 @@ describe('ZoomController', () => {
controller.setSettings({});
// Should fall back to default.
expect(panzoom.zoom).toBeCalledWith(2, { animate: false });
expect(panzoom.pan).toBeCalledWith(115.62, 63.6525, {
expect(panzoom.zoom).toHaveBeenCalledWith(2, { animate: false });
expect(panzoom.pan).toHaveBeenCalledWith(115.62, 63.6525, {
animate: true,
duration: 100,
});
@@ -624,8 +624,8 @@ describe('ZoomController', () => {
triggerResizeObserver();
expect(panzoom.zoom).not.toBeCalled();
expect(panzoom.pan).not.toBeCalled();
expect(panzoom.zoom).not.toHaveBeenCalled();
expect(panzoom.pan).not.toHaveBeenCalled();
});
it('when element has no size', () => {
@@ -641,8 +641,8 @@ describe('ZoomController', () => {
triggerResizeObserver();
expect(panzoom.zoom).not.toBeCalled();
expect(panzoom.pan).not.toBeCalled();
expect(panzoom.zoom).not.toHaveBeenCalled();
expect(panzoom.pan).not.toHaveBeenCalled();
});
});
@@ -673,7 +673,7 @@ describe('ZoomController', () => {
const ev = new PointerEvent('pointerdown');
element.dispatchEvent(ev);
expect(panzoom.handleDown).not.toBeCalled();
expect(panzoom.handleDown).not.toHaveBeenCalled();
});
it('should set touch action on zoom/unzoom', () => {
@@ -88,7 +88,7 @@ describe('handleZoomSettingsObservedEvent', () => {
viewManager,
);
expect(viewManager.setViewByParameters).not.toBeCalled();
expect(viewManager.setViewByParameters).not.toHaveBeenCalled();
});
it('should handle observed zoom settings ', () => {
@@ -106,13 +106,13 @@ describe('handleZoomSettingsObservedEvent', () => {
viewManager,
'target',
);
expect(viewManager.setViewByParameters).toBeCalledWith(
expect(viewManager.setViewByParameters).toHaveBeenCalledWith(
expect.objectContaining({
modifiers: [expect.any(MergeContextViewModifier)],
}),
);
expect(MergeContextViewModifier).toBeCalledWith({
expect(MergeContextViewModifier).toHaveBeenCalledWith({
zoom: {
target: {
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },