Set timeline to now not to most recent event.
This commit is contained in:
@@ -1,16 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
CardElementManager,
|
||||
CardHTMLElement,
|
||||
} from '../../src/card-controller/card-element-manager';
|
||||
import { createCardAPI } from '../test-utils';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
const createElement = (): CardHTMLElement => {
|
||||
const element = document.createElement('div') as unknown as CardHTMLElement;
|
||||
element.requestUpdate = vi.fn();
|
||||
return element as CardHTMLElement;
|
||||
};
|
||||
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
||||
import { createCardAPI, createLitElement } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('CardElementManager', () => {
|
||||
@@ -20,7 +11,7 @@ describe('CardElementManager', () => {
|
||||
});
|
||||
|
||||
it('should get element', () => {
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
const manager = new CardElementManager(
|
||||
createCardAPI(),
|
||||
element,
|
||||
@@ -35,7 +26,7 @@ describe('CardElementManager', () => {
|
||||
const callback = vi.fn();
|
||||
const manager = new CardElementManager(
|
||||
createCardAPI(),
|
||||
createElement(),
|
||||
createLitElement(),
|
||||
callback,
|
||||
() => undefined,
|
||||
);
|
||||
@@ -49,7 +40,7 @@ describe('CardElementManager', () => {
|
||||
const callback = vi.fn();
|
||||
const manager = new CardElementManager(
|
||||
createCardAPI(),
|
||||
createElement(),
|
||||
createLitElement(),
|
||||
() => undefined,
|
||||
callback,
|
||||
);
|
||||
@@ -60,7 +51,7 @@ describe('CardElementManager', () => {
|
||||
});
|
||||
|
||||
it('should update', () => {
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
const manager = new CardElementManager(
|
||||
createCardAPI(),
|
||||
element,
|
||||
@@ -73,7 +64,7 @@ describe('CardElementManager', () => {
|
||||
});
|
||||
|
||||
it('should get hasUpdated', () => {
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
element.hasUpdated = true;
|
||||
const manager = new CardElementManager(
|
||||
createCardAPI(),
|
||||
@@ -86,7 +77,7 @@ describe('CardElementManager', () => {
|
||||
});
|
||||
|
||||
it('should get height', () => {
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
width: 200,
|
||||
height: 800,
|
||||
@@ -106,7 +97,7 @@ describe('CardElementManager', () => {
|
||||
const windowAddEventListener = vi.spyOn(global.window, 'addEventListener');
|
||||
|
||||
const addEventListener = vi.fn();
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
element.addEventListener = addEventListener;
|
||||
|
||||
const api = createCardAPI();
|
||||
@@ -147,7 +138,7 @@ describe('CardElementManager', () => {
|
||||
it('should disconnect', () => {
|
||||
const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener');
|
||||
|
||||
const element = createElement();
|
||||
const element = createLitElement();
|
||||
element.setAttribute('panel', '');
|
||||
|
||||
const removeEventListener = vi.fn();
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('ViewManager.setView', () => {
|
||||
it('should set view with new context', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api);
|
||||
const context = { thumbnails: { fetch: false } };
|
||||
const context = { live: { fetchThumbnails: false } };
|
||||
|
||||
// Setting context with no existing view does nothing.
|
||||
manager.setViewWithNewContext(context);
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LiveController } from '../../../src/components-lib/live/live-controller';
|
||||
import { dispatchMessageEvent } from '../../../src/components/message';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
} from '../../../src/utils/media-to-view';
|
||||
import { EventMediaQueries } from '../../../src/view/media-queries';
|
||||
import {
|
||||
IntersectionObserverMock,
|
||||
callIntersectionHandler,
|
||||
createCameraManager,
|
||||
createConfig,
|
||||
createLitElement,
|
||||
createMediaLoadedInfo,
|
||||
createMediaLoadedInfoEvent,
|
||||
createParent,
|
||||
createView,
|
||||
createViewChangeEvent,
|
||||
} from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/utils/media-to-view');
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('LiveController', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||
});
|
||||
|
||||
it('should be constructable', () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
expect(controller).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should connect and disconnect', () => {
|
||||
const host = createLitElement();
|
||||
const parent = createParent({ children: [host] });
|
||||
const eventListener = vi.fn();
|
||||
parent.addEventListener('frigate-card:message', eventListener);
|
||||
|
||||
const controller = new LiveController(host);
|
||||
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
|
||||
expect(eventListener).toBeCalledTimes(0);
|
||||
|
||||
controller.hostDisconnected();
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
|
||||
expect(eventListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('should handle background / foreground', () => {
|
||||
it('should start in the foreground', () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
expect(controller.isInBackground()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should handle changing to background', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new LiveController(element);
|
||||
expect(controller.isInBackground()).toBeFalsy();
|
||||
expect(element.requestUpdate).toBeCalledTimes(0);
|
||||
|
||||
callIntersectionHandler(true);
|
||||
expect(controller.isInBackground()).toBeFalsy();
|
||||
expect(element.requestUpdate).toBeCalledTimes(0);
|
||||
|
||||
callIntersectionHandler(false);
|
||||
expect(controller.isInBackground()).toBeTruthy();
|
||||
expect(element.requestUpdate).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should dispatch media loaded on background change', () => {
|
||||
const host = createLitElement();
|
||||
const parent = createParent({ children: [host] });
|
||||
const eventListener = vi.fn();
|
||||
parent.addEventListener('frigate-card:media:loaded', eventListener);
|
||||
|
||||
const controller = new LiveController(host);
|
||||
const mediaLoadedInfo = createMediaLoadedInfo();
|
||||
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
expect(controller.isInBackground()).toBeTruthy();
|
||||
|
||||
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
||||
expect(eventListener).toBeCalledTimes(0);
|
||||
|
||||
callIntersectionHandler(true);
|
||||
expect(eventListener).toBeCalledTimes(1);
|
||||
expect(eventListener).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: mediaLoadedInfo,
|
||||
}),
|
||||
);
|
||||
|
||||
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
||||
expect(eventListener).toBeCalledTimes(2);
|
||||
|
||||
callIntersectionHandler(false);
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
callIntersectionHandler(true);
|
||||
expect(eventListener).toBeCalledTimes(2);
|
||||
|
||||
controller.clearMessageReceived();
|
||||
callIntersectionHandler(true);
|
||||
expect(eventListener).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should correctly allow updates', () => {
|
||||
it('when not in background', () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
expect(controller.shouldUpdate()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('when in background without message', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new LiveController(host);
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
|
||||
expect(controller.shouldUpdate()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('when in background with message', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new LiveController(host);
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
|
||||
expect(controller.shouldUpdate()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle message', () => {
|
||||
const host = createLitElement();
|
||||
const parent = createParent({ children: [host] });
|
||||
const eventListener = vi.fn();
|
||||
parent.addEventListener('frigate-card:message', eventListener);
|
||||
|
||||
const controller = new LiveController(host);
|
||||
controller.hostConnected();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
expect(controller.isInBackground()).toBeTruthy();
|
||||
|
||||
const firstRenderEpoch = controller.getRenderEpoch();
|
||||
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
expect(eventListener).toBeCalledTimes(0);
|
||||
|
||||
const secondRenderEpoch = controller.getRenderEpoch();
|
||||
expect(secondRenderEpoch).not.toBe(firstRenderEpoch);
|
||||
|
||||
callIntersectionHandler(true);
|
||||
|
||||
dispatchMessageEvent(host, 'message', 'info');
|
||||
expect(eventListener).toBeCalledTimes(1);
|
||||
expect(controller.getRenderEpoch()).toBe(secondRenderEpoch);
|
||||
});
|
||||
|
||||
it('should handle view change', () => {
|
||||
const host = createLitElement();
|
||||
const parent = createParent({ children: [host] });
|
||||
const eventListener = vi.fn();
|
||||
parent.addEventListener('frigate-card:view:change', eventListener);
|
||||
|
||||
const controller = new LiveController(host);
|
||||
controller.hostConnected();
|
||||
const view = createView();
|
||||
|
||||
callIntersectionHandler(false);
|
||||
expect(controller.isInBackground()).toBeTruthy();
|
||||
host.dispatchEvent(createViewChangeEvent(view));
|
||||
|
||||
expect(eventListener).toBeCalledTimes(0);
|
||||
|
||||
callIntersectionHandler(true);
|
||||
expect(controller.isInBackground()).toBeFalsy();
|
||||
|
||||
host.dispatchEvent(createViewChangeEvent(view));
|
||||
|
||||
expect(eventListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('should fetch media', () => {
|
||||
it('when in background', async () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
|
||||
callIntersectionHandler(false);
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
createView(),
|
||||
createCameraManager(),
|
||||
{},
|
||||
createConfig().live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('when has existing query', async () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
createView({ query: new EventMediaQueries() }),
|
||||
createCameraManager(),
|
||||
{},
|
||||
createConfig().live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('when has no thumbnails', async () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
createView(),
|
||||
createCameraManager(),
|
||||
{},
|
||||
createConfig({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
}).live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('when fetch disabled in context', async () => {
|
||||
const controller = new LiveController(createLitElement());
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
createView({
|
||||
context: {
|
||||
live: {
|
||||
fetchThumbnails: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
createCameraManager(),
|
||||
{},
|
||||
createConfig().live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('with fetch', () => {
|
||||
const now = new Date('2024-04-07T19:43');
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('events', async () => {
|
||||
const host = createLitElement();
|
||||
const controller = new LiveController(host);
|
||||
const view = createView();
|
||||
const cameraManager = createCameraManager();
|
||||
const cardWideConfig = {};
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
view,
|
||||
cameraManager,
|
||||
cardWideConfig,
|
||||
createConfig({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
media_type: 'events',
|
||||
events_media_type: 'all',
|
||||
},
|
||||
timeline: {
|
||||
window_seconds: 3600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).toBeCalledWith(
|
||||
host,
|
||||
cameraManager,
|
||||
cardWideConfig,
|
||||
view,
|
||||
expect.objectContaining({
|
||||
allCameras: false,
|
||||
targetView: 'live',
|
||||
eventsMediaType: 'all',
|
||||
select: 'latest',
|
||||
viewContext: expect.objectContaining({
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date('2024-04-07T18:43'),
|
||||
end: now,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('recordings', async () => {
|
||||
const host = createLitElement();
|
||||
const controller = new LiveController(host);
|
||||
const view = createView();
|
||||
const cameraManager = createCameraManager();
|
||||
const cardWideConfig = {};
|
||||
|
||||
await controller.fetchMediaInBackgroundIfNecessary(
|
||||
view,
|
||||
cameraManager,
|
||||
cardWideConfig,
|
||||
createConfig({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
media_type: 'recordings',
|
||||
},
|
||||
timeline: {
|
||||
window_seconds: 3600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).live,
|
||||
);
|
||||
|
||||
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||
expect(changeViewToRecentRecordingForCameraAndDependents).toBeCalledWith(
|
||||
host,
|
||||
cameraManager,
|
||||
cardWideConfig,
|
||||
view,
|
||||
expect.objectContaining({
|
||||
allCameras: false,
|
||||
targetView: 'live',
|
||||
select: 'latest',
|
||||
viewContext: expect.objectContaining({
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date('2024-04-07T18:43'),
|
||||
end: now,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import sub from 'date-fns/sub';
|
||||
import { LitElement } from 'lit';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import { CameraManagerStore } from '../../src/camera-manager/store';
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
createCameraConfig,
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createLitElement,
|
||||
createPerformanceConfig,
|
||||
createStore,
|
||||
createView,
|
||||
@@ -30,12 +30,6 @@ import {
|
||||
|
||||
vi.mock('../../src/utils/media-to-view');
|
||||
|
||||
const createHost = (): LitElement => {
|
||||
const host = document.createElement('div') as unknown as LitElement;
|
||||
host.requestUpdate = vi.fn();
|
||||
return host;
|
||||
};
|
||||
|
||||
const createCameraStore = (options?: {
|
||||
capabilities: Capabilities;
|
||||
}): CameraManagerStore => {
|
||||
@@ -64,7 +58,7 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should have correct default options', () => {
|
||||
it('media type', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getMediaTypeOptions()).toEqual([
|
||||
{
|
||||
value: MediaFilterMediaType.Clips,
|
||||
@@ -82,7 +76,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('favorite', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getFavoriteOptions()).toEqual([
|
||||
{
|
||||
value: MediaFilterCoreFavoriteSelection.Favorite,
|
||||
@@ -96,7 +90,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('when', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getWhenOptions()).toEqual([
|
||||
{
|
||||
value: MediaFilterCoreWhen.Today,
|
||||
@@ -122,22 +116,22 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('cameras', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getCameraOptions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('what', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getWhatOptions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('where', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getWhereOptions()).toEqual([]);
|
||||
});
|
||||
|
||||
it('tags', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getTagsOptions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -152,7 +146,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.computeCameraOptions(cameraManager);
|
||||
expect(controller.getCameraOptions()).toEqual([
|
||||
{
|
||||
@@ -178,7 +172,7 @@ describe('MediaFilterController', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.computeCameraOptions(cameraManager);
|
||||
expect(controller.getCameraOptions()).toEqual([]);
|
||||
});
|
||||
@@ -188,7 +182,7 @@ describe('MediaFilterController', () => {
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue(null);
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.computeCameraOptions(cameraManager);
|
||||
expect(controller.getCameraOptions()).toEqual([
|
||||
{
|
||||
@@ -206,7 +200,7 @@ describe('MediaFilterController', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getMediaMetadata).mockRejectedValue(new Error('error'));
|
||||
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.computeMetadataOptions(cameraManager);
|
||||
expect(host.requestUpdate).not.toBeCalled();
|
||||
@@ -218,7 +212,7 @@ describe('MediaFilterController', () => {
|
||||
what: new Set(['person', 'car']),
|
||||
});
|
||||
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.computeMetadataOptions(cameraManager);
|
||||
expect(controller.getWhatOptions()).toEqual([
|
||||
@@ -240,7 +234,7 @@ describe('MediaFilterController', () => {
|
||||
where: new Set(['front_door', 'back_yard']),
|
||||
});
|
||||
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.computeMetadataOptions(cameraManager);
|
||||
expect(controller.getWhereOptions()).toEqual([
|
||||
@@ -262,7 +256,7 @@ describe('MediaFilterController', () => {
|
||||
tags: new Set(['tag-1', 'tag-2']),
|
||||
});
|
||||
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.computeMetadataOptions(cameraManager);
|
||||
expect(controller.getTagsOptions()).toEqual([
|
||||
@@ -284,7 +278,7 @@ describe('MediaFilterController', () => {
|
||||
days: new Set(['2024-02-04', '2024-02-05']),
|
||||
});
|
||||
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.computeMetadataOptions(cameraManager);
|
||||
|
||||
@@ -306,7 +300,7 @@ describe('MediaFilterController', () => {
|
||||
const view = createView({ query: new EventMediaQueries() });
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
events: true,
|
||||
recordings: false,
|
||||
@@ -317,7 +311,7 @@ describe('MediaFilterController', () => {
|
||||
const view = createView({ query: new RecordingMediaQueries() });
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
events: false,
|
||||
recordings: true,
|
||||
@@ -331,7 +325,7 @@ describe('MediaFilterController', () => {
|
||||
createCapabilities({ 'favorite-events': true }),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
favorites: true,
|
||||
});
|
||||
@@ -344,7 +338,7 @@ describe('MediaFilterController', () => {
|
||||
createCapabilities({ 'favorite-recordings': true }),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
favorites: true,
|
||||
});
|
||||
@@ -354,7 +348,7 @@ describe('MediaFilterController', () => {
|
||||
const view = createView();
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
favorites: false,
|
||||
});
|
||||
@@ -363,7 +357,7 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should handle value change', () => {
|
||||
it('must have visible cameras', async () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.valueChangeHandler(
|
||||
createCameraManager(),
|
||||
@@ -380,7 +374,7 @@ describe('MediaFilterController', () => {
|
||||
'%s',
|
||||
async (viewName: 'clips' | 'snapshots') => {
|
||||
const eventListener = vi.fn();
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
host.addEventListener('frigate-card:view:change', eventListener);
|
||||
|
||||
const controller = new MediaFilterController(host);
|
||||
@@ -458,7 +452,7 @@ describe('MediaFilterController', () => {
|
||||
|
||||
it('with recordings media type', async () => {
|
||||
const eventListener = vi.fn();
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
host.addEventListener('frigate-card:view:change', eventListener);
|
||||
|
||||
const controller = new MediaFilterController(host);
|
||||
@@ -522,7 +516,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('without favorites', async () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
@@ -583,7 +577,7 @@ describe('MediaFilterController', () => {
|
||||
new Date('2024-02-29T23:59:59.999'),
|
||||
],
|
||||
])('%s', async (value: MediaFilterCoreWhen | string, from: Date, to: Date) => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
@@ -614,7 +608,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('custom without values', async () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
@@ -646,7 +640,7 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should calculate correct defaults', () => {
|
||||
it('with no queries', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
|
||||
controller.computeInitialDefaultsFromView(createCameraManager(), createView());
|
||||
|
||||
@@ -654,7 +648,7 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('with no cameras', () => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
|
||||
controller.computeInitialDefaultsFromView(
|
||||
createCameraManager(),
|
||||
@@ -956,7 +950,7 @@ describe('MediaFilterController', () => {
|
||||
mediaQueries: MediaQueries,
|
||||
defaults: MediaFilterCoreDefaults | null,
|
||||
) => {
|
||||
const controller = new MediaFilterController(createHost());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import {
|
||||
HASSDomEvent,
|
||||
handleActionConfig,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { LitElement } from 'lit';
|
||||
import { HASSDomEvent, handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FRIGATE_ICON_SVG_PATH } from '../../src/camera-manager/frigate/icon';
|
||||
import { MenuController } from '../../src/components-lib/menu-controller';
|
||||
import { ActionsConfig, MenuConfig, menuConfigSchema } from '../../src/config/types';
|
||||
import { StateParameters } from '../../src/types';
|
||||
import { refreshDynamicStateParameters } from '../../src/utils/ha';
|
||||
import { createHASS } from '../test-utils';
|
||||
import { createHASS, createLitElement } from '../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/custom-card-helpers');
|
||||
vi.mock('../../src/utils/ha');
|
||||
|
||||
const createHost = (): LitElement => {
|
||||
const host = document.createElement('div') as unknown as LitElement;
|
||||
host.requestUpdate = vi.fn();
|
||||
return host;
|
||||
};
|
||||
|
||||
const createMenuConfig = (config: unknown): MenuConfig => {
|
||||
return menuConfigSchema.parse(config);
|
||||
};
|
||||
@@ -59,7 +49,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('should set and get menu config', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const controller = new MenuController(host);
|
||||
|
||||
const config = createMenuConfig({
|
||||
@@ -79,7 +69,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should set and sort buttons', () => {
|
||||
it('by priority', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.setButtons([
|
||||
{
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -146,7 +136,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('with frigate button first', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
style: 'hidden',
|
||||
@@ -197,7 +187,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should get buttons', () => {
|
||||
it('with matching alignment', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.setButtons([
|
||||
{
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -229,7 +219,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('with disabled buttons', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.setButtons([
|
||||
{
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -261,7 +251,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('with hidden non-expanded menu', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
style: 'hidden',
|
||||
@@ -303,7 +293,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should get fresh button state', () => {
|
||||
it('on state icon', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
const stateButton = {
|
||||
type: 'custom:frigate-card-menu-state-icon' as const,
|
||||
icon: 'mdi:sheep',
|
||||
@@ -322,7 +312,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('on non state icon', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
const button = {
|
||||
type: 'custom:frigate-card-menu-icon' as const,
|
||||
icon: 'mdi:sheep',
|
||||
@@ -335,7 +325,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should get svg path', () => {
|
||||
it('frigate icon', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
const button = {
|
||||
type: 'custom:frigate-card-menu-icon' as const,
|
||||
icon: 'frigate',
|
||||
@@ -344,7 +334,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('non-frigate icon', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
const button = {
|
||||
type: 'custom:frigate-card-menu-icon' as const,
|
||||
icon: 'mdi:cow',
|
||||
@@ -355,13 +345,13 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without config', () => {
|
||||
const controller = new MenuController(createHost());
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.actionHandler(createHASS(), createEvent('tap'));
|
||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute simple action in non-hidden menu', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
|
||||
@@ -377,7 +367,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('should execute simple action in with config in event', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
|
||||
@@ -391,7 +381,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('should execute simple array of actions in non-hidden menu', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
|
||||
@@ -401,7 +391,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should close menu', () => {
|
||||
it('tap', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
@@ -418,7 +408,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('end_tap', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
@@ -439,7 +429,7 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should not close menu', () => {
|
||||
it('start_tap with later action', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
@@ -459,7 +449,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('with a menu toggle action', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
@@ -479,7 +469,7 @@ describe('MenuController', () => {
|
||||
});
|
||||
|
||||
it('when no action is actually taken', () => {
|
||||
const host = createHost();
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
|
||||
+40
-2
@@ -1,5 +1,6 @@
|
||||
import { CurrentUser, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { LitElement } from 'lit';
|
||||
import { expect, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Camera } from '../src/camera-manager/camera';
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
CameraEventCallback,
|
||||
CameraManagerMediaCapabilities,
|
||||
} from '../src/camera-manager/types';
|
||||
import { CapabilitiesRaw } from '../src/types';
|
||||
import { ActionsManager } from '../src/card-controller/actions-manager';
|
||||
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
||||
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
frigateCardConfigSchema,
|
||||
performanceConfigSchema,
|
||||
} from '../src/config/types';
|
||||
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||
import { CapabilitiesRaw, ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||
import { EntityRegistryManager } from '../src/utils/ha/entity-registry';
|
||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||
@@ -248,6 +248,24 @@ export const createMediaLoadedInfo = (
|
||||
};
|
||||
};
|
||||
|
||||
export const createMediaLoadedInfoEvent = (
|
||||
mediaLoadedInfo?: MediaLoadedInfo,
|
||||
): CustomEvent<MediaLoadedInfo> => {
|
||||
return new CustomEvent('frigate-card:media:loaded', {
|
||||
detail: mediaLoadedInfo ?? createMediaLoadedInfo(),
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const createViewChangeEvent = (view?: View): CustomEvent<View> => {
|
||||
return new CustomEvent('frigate-card:view:change', {
|
||||
detail: view ?? createView(),
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const createPerformanceConfig = (config: unknown): PerformanceConfig => {
|
||||
return performanceConfigSchema.parse(config);
|
||||
};
|
||||
@@ -342,6 +360,20 @@ export const requestAnimationFrameMock = (callback: FrameRequestCallback) => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
export const callIntersectionHandler = (intersecting = true, n = 0): void => {
|
||||
const mockResult = vi.mocked(IntersectionObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
return;
|
||||
}
|
||||
const observer = mockResult.value;
|
||||
vi.mocked(IntersectionObserver).mock.calls[n][0](
|
||||
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
||||
// just provides the bare basics current implementation uses.
|
||||
intersecting ? [{ isIntersecting: true } as IntersectionObserverEntry] : [],
|
||||
observer,
|
||||
);
|
||||
};
|
||||
|
||||
export const createSlotHost = (options?: {
|
||||
slot?: HTMLSlotElement;
|
||||
children?: HTMLElement[];
|
||||
@@ -370,6 +402,12 @@ export const createParent = (options?: { children?: HTMLElement[] }): HTMLElemen
|
||||
return parent;
|
||||
};
|
||||
|
||||
export const createLitElement = (): LitElement => {
|
||||
const element = document.createElement('div') as unknown as LitElement;
|
||||
element.requestUpdate = vi.fn();
|
||||
return element;
|
||||
};
|
||||
|
||||
export const createCardAPI = (): CardController => {
|
||||
const api = mock<CardController>();
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ import {
|
||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../../../../src/utils/media-info';
|
||||
import {
|
||||
IntersectionObserverMock,
|
||||
callIntersectionHandler,
|
||||
createMediaLoadedInfo,
|
||||
createParent,
|
||||
} from '../../../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callIntersectionHandler,
|
||||
callVisibilityHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
|
||||
@@ -3,12 +3,12 @@ import AutoSize from '../../../../../src/utils/embla/plugins/auto-size/auto-size
|
||||
import {
|
||||
IntersectionObserverMock,
|
||||
ResizeObserverMock,
|
||||
callIntersectionHandler,
|
||||
createParent,
|
||||
requestAnimationFrameMock,
|
||||
} from '../../../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callIntersectionHandler,
|
||||
callResizeHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
|
||||
@@ -43,20 +43,6 @@ export const callVisibilityHandler = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
export const callIntersectionHandler = (intersecting = true, n = 0): void => {
|
||||
const mockResult = vi.mocked(IntersectionObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
return;
|
||||
}
|
||||
const observer = mockResult.value;
|
||||
vi.mocked(IntersectionObserver).mock.calls[n][0](
|
||||
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
||||
// just provides the bare basics current implementation uses.
|
||||
intersecting ? [{ isIntersecting: true } as IntersectionObserverEntry] : [],
|
||||
observer,
|
||||
);
|
||||
};
|
||||
|
||||
export const callMutationHandler = (n = 0): void => {
|
||||
const mockResult = vi.mocked(MutationObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
|
||||
Reference in New Issue
Block a user