Refactor views to support dynamic updates.
This commit is contained in:
@@ -19,10 +19,12 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
viewName: 'live',
|
||||
cameraID: 'camera',
|
||||
params: {
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
},
|
||||
failSafe: true,
|
||||
}),
|
||||
);
|
||||
@@ -49,10 +51,12 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
viewName: 'timeline',
|
||||
cameraID: 'camera',
|
||||
params: {
|
||||
view: 'timeline',
|
||||
camera: 'camera',
|
||||
},
|
||||
failSafe: true,
|
||||
}),
|
||||
);
|
||||
@@ -86,10 +90,12 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
viewName: 'clips',
|
||||
cameraID: 'camera',
|
||||
params: {
|
||||
view: 'clips',
|
||||
camera: 'camera',
|
||||
},
|
||||
failSafe: true,
|
||||
}),
|
||||
);
|
||||
@@ -114,10 +120,12 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
viewName: 'live',
|
||||
cameraID: 'camera',
|
||||
params: {
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
},
|
||||
failSafe: true,
|
||||
}),
|
||||
);
|
||||
@@ -141,7 +149,7 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('without a current view', async () => {
|
||||
@@ -158,6 +166,6 @@ describe('should handle camera_select action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,5 +15,9 @@ it('should handle default action', async () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
displayMode: 'grid',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { SubstreamOffAction } from '../../../../src/card-controller/actions/actions/substream-off';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { SubstreamOffViewModifier } from '../../../../src/card-controller/view/modifiers/substream-off';
|
||||
|
||||
it('should handle live_substream_off action', async () => {
|
||||
const api = createCardAPI();
|
||||
@@ -14,5 +15,7 @@ it('should handle live_substream_off action', async () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithoutSubstream).toBeCalled();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
modifiers: [expect.any(SubstreamOffViewModifier)],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { SubstreamOnAction } from '../../../../src/card-controller/actions/actions/substream-on';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { SubstreamOnViewModifier } from '../../../../src/card-controller/view/modifiers/substream-on';
|
||||
|
||||
it('should handle live_substream_on action', async () => {
|
||||
const api = createCardAPI();
|
||||
@@ -14,5 +15,7 @@ it('should handle live_substream_on action', async () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
modifiers: [expect.any(SubstreamOnViewModifier)],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { SubstreamSelectAction } from '../../../../src/card-controller/actions/actions/substream-select';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { SubstreamSelectViewModifier } from '../../../../src/card-controller/view/modifiers/substream-select';
|
||||
|
||||
it('should handle live_substream_select action', async () => {
|
||||
const api = createCardAPI();
|
||||
@@ -15,5 +16,9 @@ it('should handle live_substream_select action', async () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream');
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
modifiers: expect.arrayContaining([expect.any(SubstreamSelectViewModifier)]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,9 +27,11 @@ describe('should handle view action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
viewName: viewName,
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MicrophoneManager } from '../../src/card-controller/microphone-manager'
|
||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||
import { StyleManager } from '../../src/card-controller/style-manager';
|
||||
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
||||
import { ViewManager } from '../../src/card-controller/view-manager';
|
||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||
import { FrigateCardEditor } from '../../src/editor';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||
@@ -52,7 +52,7 @@ vi.mock('../../src/card-controller/microphone-manager');
|
||||
vi.mock('../../src/card-controller/query-string-manager');
|
||||
vi.mock('../../src/card-controller/style-manager');
|
||||
vi.mock('../../src/card-controller/triggers-manager');
|
||||
vi.mock('../../src/card-controller/view-manager');
|
||||
vi.mock('../../src/card-controller/view/view-manager');
|
||||
vi.mock('../../src/utils/ha/entity-registry');
|
||||
vi.mock('../../src/utils/ha/resolved-media');
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('InitializationManager', () => {
|
||||
expect(loadLanguages).toBeCalled();
|
||||
expect(sideLoadHomeAssistantElements).toBeCalled();
|
||||
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
expect(api.getMicrophoneManager().connect).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||
import { SubstreamSelectViewModifier } from '../../src/card-controller/view/modifiers/substream-select';
|
||||
import { createCardAPI } from '../test-utils';
|
||||
|
||||
const setQueryString = (qs: string): void => {
|
||||
@@ -51,8 +52,10 @@ describe('QueryStringManager', () => {
|
||||
manager.executeAll();
|
||||
|
||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: viewName,
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,7 +95,7 @@ describe('QueryStringManager', () => {
|
||||
|
||||
manager.executeAll();
|
||||
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
|
||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||
@@ -107,8 +110,10 @@ describe('QueryStringManager', () => {
|
||||
|
||||
manager.executeAll();
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
camera: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||
@@ -124,8 +129,9 @@ describe('QueryStringManager', () => {
|
||||
|
||||
manager.executeAll();
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
substream: 'camera.office_hd',
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
modifiers: [expect.any(SubstreamSelectViewModifier)],
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||
@@ -190,8 +196,10 @@ describe('QueryStringManager', () => {
|
||||
manager.executeAll();
|
||||
|
||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: viewName,
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -230,11 +238,13 @@ describe('QueryStringManager', () => {
|
||||
|
||||
manager.executeAll();
|
||||
|
||||
expect(api.getViewManager().setViewDefault).toBeCalledWith({
|
||||
cameraID: 'camera.kitchen',
|
||||
substream: 'camera.kitchen_hd',
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
camera: 'camera.kitchen',
|
||||
},
|
||||
modifiers: [expect.any(SubstreamSelectViewModifier)],
|
||||
});
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('multiple cameras specified', () => {
|
||||
@@ -248,8 +258,10 @@ describe('QueryStringManager', () => {
|
||||
|
||||
manager.executeAll();
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
camera: 'camera.office',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createConfig,
|
||||
createStore,
|
||||
createView,
|
||||
flushPromises,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
@@ -109,7 +110,7 @@ describe('TriggersManager', () => {
|
||||
});
|
||||
|
||||
describe('trigger actions', () => {
|
||||
it('update', () => {
|
||||
it('update', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
...baseTriggersConfig,
|
||||
@@ -122,16 +123,18 @@ describe('TriggersManager', () => {
|
||||
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
type: 'new',
|
||||
});
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setView).toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
queryExecutorOptions: { useCache: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('default', () => {
|
||||
it('default', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
...baseTriggersConfig,
|
||||
@@ -144,11 +147,13 @@ describe('TriggersManager', () => {
|
||||
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewDefault).toBeCalledWith({
|
||||
cameraID: 'camera_1',
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
camera: 'camera_1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,9 +173,11 @@ describe('TriggersManager', () => {
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: 'live',
|
||||
cameraID: 'camera_1',
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
view: 'live',
|
||||
camera: 'camera_1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,12 +214,16 @@ describe('TriggersManager', () => {
|
||||
});
|
||||
|
||||
if (!viewName) {
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(
|
||||
api.getViewManager().setViewByParametersWithNewQuery,
|
||||
).not.toBeCalled();
|
||||
} else {
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
cameraID: 'camera_1',
|
||||
viewName: viewName,
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
camera: 'camera_1',
|
||||
view: viewName,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -235,9 +246,8 @@ describe('TriggersManager', () => {
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setView).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,12 +273,11 @@ describe('TriggersManager', () => {
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getViewManager().setView).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('default', () => {
|
||||
it('default', async () => {
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
...baseTriggersConfig,
|
||||
@@ -281,15 +290,16 @@ describe('TriggersManager', () => {
|
||||
});
|
||||
|
||||
const manager = new TriggersManager(api);
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'end' });
|
||||
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
||||
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'end' });
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -343,9 +353,8 @@ describe('TriggersManager', () => {
|
||||
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new', fidelity: 'high' });
|
||||
|
||||
expect(api.getViewManager().setView).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('with non-live default', () => {
|
||||
@@ -364,9 +373,8 @@ describe('TriggersManager', () => {
|
||||
|
||||
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new', fidelity: 'high' });
|
||||
|
||||
expect(api.getViewManager().setView).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,7 +392,8 @@ describe('TriggersManager', () => {
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
|
||||
manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
@@ -396,7 +405,8 @@ describe('TriggersManager', () => {
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should take no actions when actions are set to none', () => {
|
||||
@@ -415,7 +425,8 @@ describe('TriggersManager', () => {
|
||||
type: 'new',
|
||||
});
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
|
||||
manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
@@ -426,10 +437,11 @@ describe('TriggersManager', () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should take actions with human interactions when interaction mode is active', () => {
|
||||
it('should take actions with human interactions when interaction mode is active', async () => {
|
||||
const api = createTriggerAPI({
|
||||
// Interaction present.
|
||||
interaction: true,
|
||||
@@ -443,31 +455,34 @@ describe('TriggersManager', () => {
|
||||
},
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
type: 'new',
|
||||
});
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: 'live' as const,
|
||||
cameraID: 'camera_1' as const,
|
||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||
params: {
|
||||
view: 'live' as const,
|
||||
camera: 'camera_1' as const,
|
||||
}
|
||||
});
|
||||
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
type: 'end',
|
||||
});
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||
});
|
||||
|
||||
it('should report multiple triggered cameras', () => {
|
||||
it('should report multiple triggered cameras', async () => {
|
||||
const api = createTriggerAPI();
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
@@ -496,11 +511,11 @@ describe('TriggersManager', () => {
|
||||
expect(manager.getMostRecentlyTriggeredCameraID()).toBeNull();
|
||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
||||
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
type: 'new',
|
||||
});
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_2',
|
||||
type: 'new',
|
||||
});
|
||||
@@ -513,7 +528,7 @@ describe('TriggersManager', () => {
|
||||
manager.getMostRecentlyTriggeredCameraID(),
|
||||
);
|
||||
|
||||
manager.handleCameraEvent({
|
||||
await manager.handleCameraEvent({
|
||||
cameraID: 'camera_1',
|
||||
type: 'end',
|
||||
});
|
||||
@@ -521,6 +536,8 @@ describe('TriggersManager', () => {
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_2']));
|
||||
expect(manager.getMostRecentlyTriggeredCameraID()).toBe('camera_2');
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
||||
import { QueryExecutor } from '../../../src/card-controller/view/query-executor';
|
||||
import { ViewModifier } from '../../../src/card-controller/view/types';
|
||||
import { FrigateCardView, ViewDisplayMode } from '../../../src/config/types';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
RecordingMediaQueries,
|
||||
} from '../../../src/view/media-queries';
|
||||
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
|
||||
import { View } from '../../../src/view/view';
|
||||
import {
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createStore,
|
||||
} from '../../test-utils';
|
||||
import { createPopulatedAPI } from './test-utils';
|
||||
|
||||
describe('getViewDefault', () => {
|
||||
it('should return null without config', () => {
|
||||
const factory = new ViewFactory(createCardAPI());
|
||||
expect(factory.getViewDefault()).toBeNull();
|
||||
});
|
||||
|
||||
it('should create view', () => {
|
||||
const factory = new ViewFactory(createPopulatedAPI());
|
||||
const view = factory.getViewDefault();
|
||||
|
||||
expect(view?.is('live')).toBeTruthy();
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('should cycle camera when configured', () => {
|
||||
const api = createPopulatedAPI();
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
default_cycle_camera: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
|
||||
let view = factory.getViewDefault();
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
|
||||
view = factory.getViewDefault({ baseView: view });
|
||||
expect(view?.camera).toBe('camera.kitchen');
|
||||
|
||||
view = factory.getViewDefault({ baseView: view });
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
|
||||
// When a parameter is specified, it will not cycle.
|
||||
view = factory.getViewDefault({
|
||||
params: { camera: 'camera.office' },
|
||||
baseView: view,
|
||||
});
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('should respect parameters', () => {
|
||||
const factory = new ViewFactory(createPopulatedAPI());
|
||||
const view = factory.getViewDefault({
|
||||
params: {
|
||||
camera: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.is('live')).toBeTruthy();
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewByParameters', () => {
|
||||
it('should get view by parameters specifying camera and view', () => {
|
||||
const api = createPopulatedAPI();
|
||||
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({
|
||||
clips: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
const view = factory.getViewByParameters({
|
||||
params: {
|
||||
camera: 'camera.kitchen',
|
||||
view: 'clips',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.is('clips')).toBeTruthy();
|
||||
expect(view?.camera).toBe('camera.kitchen');
|
||||
});
|
||||
|
||||
it('should get view by parameters using base view if unspecified', () => {
|
||||
const api = createPopulatedAPI();
|
||||
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({
|
||||
clips: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
const baseView = new View({
|
||||
camera: 'camera.kitchen',
|
||||
view: 'clips',
|
||||
});
|
||||
|
||||
const view = factory.getViewByParameters({
|
||||
baseView: baseView,
|
||||
params: {
|
||||
camera: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.view).toBe('clips');
|
||||
expect(view?.camera).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('should set view by parameters using config as fallback', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({ live: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
const view = factory.getViewByParameters({
|
||||
params: {
|
||||
camera: 'camera.kitchen',
|
||||
|
||||
// No prior view, and no specified view. This could happen during query
|
||||
// string based initialization.
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.is('live')).toBeTruthy();
|
||||
expect(view?.camera).toBe('camera.kitchen');
|
||||
});
|
||||
|
||||
it('should not set view by parameters without config', () => {
|
||||
const factory = new ViewFactory(createCardAPI());
|
||||
|
||||
const view = factory.getViewByParameters({
|
||||
params: {
|
||||
view: 'live',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw without camera and without failsafe', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({ snapshots: false }),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
// No capabilities.
|
||||
capabilities: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
expect(() =>
|
||||
factory.getViewByParameters({
|
||||
// Since no camera is specified, and no camera supports the capabilities
|
||||
// necessary for this view, the view will be null.
|
||||
params: {
|
||||
view: 'snapshots',
|
||||
},
|
||||
}),
|
||||
).toThrowError(/No cameras support this view/);
|
||||
});
|
||||
|
||||
describe('should handle unsupported view', () => {
|
||||
it('should throw without failsafe', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({ snapshots: false }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({
|
||||
snapshots: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
expect(() =>
|
||||
factory.getViewByParameters({
|
||||
params: {
|
||||
camera: 'camera.kitchen',
|
||||
view: 'snapshots',
|
||||
},
|
||||
}),
|
||||
).toThrowError(/The selected camera does not support this view/);
|
||||
});
|
||||
|
||||
it('should choose live view with failsafe', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({ live: true, snapshots: false }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({
|
||||
live: true,
|
||||
snapshots: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
const view = factory.getViewByParameters({
|
||||
params: {
|
||||
camera: 'camera.kitchen',
|
||||
view: 'snapshots',
|
||||
},
|
||||
failSafe: true,
|
||||
});
|
||||
expect(view?.is('live')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call modifiers', () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({ live: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||
|
||||
const modifyCallback = vi.fn();
|
||||
class TestViewModifier implements ViewModifier {
|
||||
modify = modifyCallback;
|
||||
}
|
||||
|
||||
const view = new View({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
const factory = new ViewFactory(api);
|
||||
const modifiedView = factory.getViewByParameters({
|
||||
baseView: view,
|
||||
modifiers: [new TestViewModifier()],
|
||||
});
|
||||
|
||||
expect(modifiedView?.is('live')).toBeTruthy();
|
||||
expect(modifiedView?.camera).toBe('camera.office');
|
||||
expect(view).not.toBe(modifiedView);
|
||||
expect(modifyCallback).toHaveBeenCalledWith(modifiedView);
|
||||
});
|
||||
|
||||
describe('should get correct default display mode', () => {
|
||||
describe.each([['single' as const], ['grid' as const]])(
|
||||
'%s',
|
||||
(displayMode: ViewDisplayMode) => {
|
||||
it.each([
|
||||
['media' as const],
|
||||
['clip' as const],
|
||||
['recording' as const],
|
||||
['snapshot' as const],
|
||||
['live' as const],
|
||||
])('%s', (viewName: FrigateCardView) => {
|
||||
const api = createPopulatedAPI({
|
||||
media_viewer: {
|
||||
display: {
|
||||
mode: displayMode,
|
||||
},
|
||||
},
|
||||
live: {
|
||||
display: {
|
||||
mode: displayMode,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
expect(
|
||||
factory.getViewByParameters({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
})?.displayMode,
|
||||
).toBe(displayMode);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewByParametersWithNewQuery', () => {
|
||||
it('should not execute query without config', async () => {
|
||||
const factory = new ViewFactory(createCardAPI());
|
||||
expect(await factory.getViewByParametersWithNewQuery()).toBeNull();
|
||||
});
|
||||
|
||||
describe('with a live view', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-07-21T13:22:06Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should set timeline window', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.context).toEqual({
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date('2024-07-21T12:22:06.000Z'),
|
||||
end: new Date('2024-07-21T13:22:06.000Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch anything if configured for no thumbnails', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(
|
||||
createPopulatedAPI({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch events', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
eventsMediaType: 'all',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch recordings', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new RecordingMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultRecordingQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(
|
||||
createPopulatedAPI({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
media_type: 'recordings',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).not.toBeCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a media view', () => {
|
||||
it('should do nothing with same camera', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const baseView = new View({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
baseView: baseView,
|
||||
params: {
|
||||
view: 'media',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch clips with different camera', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const baseView = new View({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
baseView: baseView,
|
||||
params: {
|
||||
view: 'media',
|
||||
camera: 'camera.kitchen',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).toBeCalledWith({
|
||||
cameraID: 'camera.kitchen',
|
||||
eventsMediaType: 'clips',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an events-based view', () => {
|
||||
it.each([
|
||||
['clip' as const, 'clips' as const],
|
||||
['clips' as const, 'clips' as const],
|
||||
['snapshot' as const, 'snapshots' as const],
|
||||
['snapshots' as const, 'snapshots' as const],
|
||||
])(
|
||||
'%s',
|
||||
async (viewName: FrigateCardView, eventsMediaType: 'clips' | 'snapshots') => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
eventsMediaType: eventsMediaType,
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with an recordings-based view', () => {
|
||||
it.each([['recording' as const], ['recordings' as const]])(
|
||||
'%s',
|
||||
async (viewName: FrigateCardView) => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new RecordingMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultRecordingQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with an media viewer view', () => {
|
||||
it('hould not fetch anything if configured for no thumbnails', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(
|
||||
createPopulatedAPI({
|
||||
media_viewer: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'clip',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing to gallery from the media viewer', () => {
|
||||
it('should adopt query and results', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
|
||||
const baseView = new View({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
query: new EventMediaQueries(),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
});
|
||||
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
baseView: baseView,
|
||||
params: {
|
||||
view: 'clips',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(baseView.query);
|
||||
expect(view?.queryResults).toBe(baseView.queryResults);
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when set or remove seek time', () => {
|
||||
it('should set seek time when results are selected based on time', async () => {
|
||||
const now = new Date();
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'clips',
|
||||
},
|
||||
queryExecutorOptions: {
|
||||
selectResult: {
|
||||
time: {
|
||||
time: now,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.context).toEqual({
|
||||
mediaViewer: {
|
||||
seek: now,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove seek time when results are not selected based on time', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
baseView: new View({
|
||||
view: 'clips',
|
||||
camera: 'camera.office',
|
||||
context: {
|
||||
mediaViewer: {
|
||||
seek: new Date(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
params: {
|
||||
view: 'clips',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.context?.mediaViewer?.seek).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewDefaultWithNewQuery', () => {
|
||||
it('should fetch events', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewDefaultWithNewQuery();
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
eventsMediaType: 'all',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewByParametersWithExistingQuery', () => {
|
||||
it('should not execute anything when query is absent', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithExistingQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toBeCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should set query results', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
executor.execute.mockResolvedValue(queryResults);
|
||||
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const query = new RecordingMediaQueries();
|
||||
const view = await factory.getViewByParametersWithExistingQuery({
|
||||
params: {
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
query: query,
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { expect, it } from 'vitest';
|
||||
import { MergeContextViewModifier } from '../../../../src/card-controller/view/modifiers/merge-context';
|
||||
import { createView } from '../../../test-utils';
|
||||
|
||||
it('should merge context', () => {
|
||||
const context: ViewContext = {
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
},
|
||||
},
|
||||
};
|
||||
const modifier = new MergeContextViewModifier(context);
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
});
|
||||
|
||||
expect(view.context).toBeNull();
|
||||
|
||||
modifier.modify(view);
|
||||
|
||||
expect(view.context).toEqual(context);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { createView } from '../../../test-utils';
|
||||
import { RemoveContextPropertyViewModifier } from '../../../../src/card-controller/view/modifiers/remove-context-property';
|
||||
|
||||
it('should remove context property', () => {
|
||||
const modifier = new RemoveContextPropertyViewModifier('timeline', 'window');
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
context: {
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modifier.modify(view);
|
||||
|
||||
expect(view.context).toEqual({ timeline: {} });
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { createView } from '../../../test-utils';
|
||||
import { RemoveContextViewModifier } from '../../../../src/card-controller/view/modifiers/remove-context';
|
||||
|
||||
it('should remove context property', () => {
|
||||
const modifier = new RemoveContextViewModifier(['timeline']);
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
context: {
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modifier.modify(view);
|
||||
|
||||
expect(view.context).toEqual({});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { createView } from '../../../test-utils';
|
||||
import { SubstreamOffViewModifier } from '../../../../src/card-controller/view/modifiers/substream-off';
|
||||
import { hasSubstream, setSubstream } from '../../../../src/utils/substream';
|
||||
|
||||
it('should turn off substream', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
});
|
||||
|
||||
setSubstream(view, 'substream');
|
||||
expect(hasSubstream(view)).toBe(true);
|
||||
|
||||
const modifier = new SubstreamOffViewModifier();
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CardController } from '../../../../src/card-controller/controller';
|
||||
import { SubstreamOnViewModifier } from '../../../../src/card-controller/view/modifiers/substream-on';
|
||||
import { RawFrigateCardConfig } from '../../../../src/config/types';
|
||||
import { getStreamCameraID, hasSubstream, setSubstream } from '../../../../src/utils/substream';
|
||||
import {
|
||||
createCameraConfig,
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createStore,
|
||||
createView,
|
||||
} from '../../../test-utils';
|
||||
|
||||
const createAPIWithSubstreams = (config?: RawFrigateCardConfig): CardController => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
live: true,
|
||||
substream: true,
|
||||
}),
|
||||
config: createCameraConfig({
|
||||
dependencies: {
|
||||
all_cameras: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({
|
||||
substream: true,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
||||
return api;
|
||||
};
|
||||
|
||||
describe('should turn on substream', () => {
|
||||
it('substream available', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
|
||||
const api = createAPIWithSubstreams();
|
||||
|
||||
const modifier = new SubstreamOnViewModifier(api);
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(true);
|
||||
expect(getStreamCameraID(view)).toBe('camera.kitchen');
|
||||
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
expect(getStreamCameraID(view)).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('malformed substream', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const api = createAPIWithSubstreams();
|
||||
|
||||
setSubstream(view, 'NOT_A_REAL_CAMERA');
|
||||
|
||||
const modifier = new SubstreamOnViewModifier(api);
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
expect(getStreamCameraID(view)).toBe('camera.office');
|
||||
});
|
||||
|
||||
it('substream unavailable', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
|
||||
const modifier = new SubstreamOnViewModifier(api);
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { SubstreamSelectViewModifier } from '../../../../src/card-controller/view/modifiers/substream-select';
|
||||
import { getStreamCameraID, hasSubstream } from '../../../../src/utils/substream';
|
||||
import { createView } from '../../../test-utils';
|
||||
|
||||
it('should select substream', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
expect(hasSubstream(view)).toBe(false);
|
||||
|
||||
const modifier = new SubstreamSelectViewModifier('substream');
|
||||
modifier.modify(view);
|
||||
|
||||
expect(hasSubstream(view)).toBe(true);
|
||||
expect(getStreamCameraID(view)).toBe('substream');
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import { add } from 'date-fns';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { QueryType } from '../../../src/camera-manager/types';
|
||||
import { QueryExecutor } from '../../../src/card-controller/view/query-executor';
|
||||
import { ClipsOrSnapshotsOrAll } from '../../../src/types';
|
||||
import { EventMediaQueries } from '../../../src/view/media-queries';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createCameraManager,
|
||||
createCardAPI,
|
||||
createStore,
|
||||
generateViewMediaArray,
|
||||
} from '../../test-utils';
|
||||
import { createPopulatedAPI } from './test-utils';
|
||||
|
||||
describe('executeDefaultEventQuery', () => {
|
||||
it('should return null without cameras', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore());
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultEventQuery({
|
||||
cameraID: 'camera.office',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null without queries', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
vi.mocked(api.getCameraManager().generateDefaultEventQueries).mockReturnValue(null);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultEventQuery({
|
||||
cameraID: 'camera.office',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
describe('should return query results for specified camera', async () => {
|
||||
it.each([['all' as const], ['clips' as const], ['snapshots' as const], [undefined]])(
|
||||
'%s',
|
||||
async (mediaType?: ClipsOrSnapshotsOrAll) => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
const rawQueries = [
|
||||
{ type: QueryType.Event as const, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultEventQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
const results = await executor.executeDefaultEventQuery({
|
||||
cameraID: 'camera.office',
|
||||
eventsMediaType: mediaType,
|
||||
});
|
||||
|
||||
expect(results?.query.getQueries()).toEqual(rawQueries);
|
||||
expect(results?.queryResults.getResults()).toEqual(media);
|
||||
expect(api.getCameraManager().generateDefaultEventQueries).toBeCalledWith(
|
||||
new Set(['camera.office']),
|
||||
{
|
||||
limit: 50,
|
||||
...(mediaType === 'clips' && { hasClip: true }),
|
||||
...(mediaType === 'snapshots' && { hasSnapshot: true }),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return query results for all cameras', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
const rawQueries = [
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office', 'camera.kitchen']),
|
||||
},
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultEventQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
const results = await executor.executeDefaultEventQuery();
|
||||
|
||||
expect(results?.query.getQueries()).toEqual(rawQueries);
|
||||
expect(results?.queryResults.getResults()).toEqual(media);
|
||||
expect(api.getCameraManager().generateDefaultEventQueries).toBeCalledWith(
|
||||
new Set(['camera.office', 'camera.kitchen']),
|
||||
{
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when query returns null', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const rawQueries = [
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultEventQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(null);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultEventQuery({ cameraID: 'camera.office' }),
|
||||
).toBeNull();
|
||||
expect(api.getCameraManager().generateDefaultEventQueries).toBeCalledWith(
|
||||
new Set(['camera.office']),
|
||||
{
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeDefaultRecordingQuery', () => {
|
||||
it('should return null without cameras', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore());
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultRecordingQuery({
|
||||
cameraID: 'camera.office',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null without queries', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
vi.mocked(api.getCameraManager().generateDefaultRecordingQueries).mockReturnValue(
|
||||
null,
|
||||
);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultRecordingQuery({
|
||||
cameraID: 'camera.office',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return query results for specified camera', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
const rawQueries = [
|
||||
{ type: QueryType.Recording as const, cameraIDs: new Set(['camera.office']) },
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultRecordingQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
const results = await executor.executeDefaultRecordingQuery({
|
||||
cameraID: 'camera.office',
|
||||
});
|
||||
|
||||
expect(results?.query.getQueries()).toEqual(rawQueries);
|
||||
expect(results?.queryResults.getResults()).toEqual(media);
|
||||
expect(api.getCameraManager().generateDefaultRecordingQueries).toBeCalledWith(
|
||||
new Set(['camera.office']),
|
||||
{
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return query results for all cameras', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
const rawQueries = [
|
||||
{
|
||||
type: QueryType.Recording as const,
|
||||
cameraIDs: new Set(['camera.office', 'camera.kitchen']),
|
||||
},
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultRecordingQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
const results = await executor.executeDefaultRecordingQuery();
|
||||
|
||||
expect(results?.query.getQueries()).toEqual(rawQueries);
|
||||
expect(results?.queryResults.getResults()).toEqual(media);
|
||||
expect(api.getCameraManager().generateDefaultRecordingQueries).toBeCalledWith(
|
||||
new Set(['camera.office', 'camera.kitchen']),
|
||||
{
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when query returns null', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const rawQueries = [
|
||||
{
|
||||
type: QueryType.Recording as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
];
|
||||
vi.mocked(api.getCameraManager().generateDefaultRecordingQueries).mockReturnValue(
|
||||
rawQueries,
|
||||
);
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(null);
|
||||
|
||||
const executor = new QueryExecutor(api);
|
||||
expect(
|
||||
await executor.executeDefaultRecordingQuery({ cameraID: 'camera.office' }),
|
||||
).toBeNull();
|
||||
expect(api.getCameraManager().generateDefaultRecordingQueries).toBeCalledWith(
|
||||
new Set(['camera.office']),
|
||||
{
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return null when query is empty', async () => {
|
||||
const executor = new QueryExecutor(createCardAPI());
|
||||
expect(await executor.execute(new EventMediaQueries())).toBeNull();
|
||||
});
|
||||
|
||||
describe('should handle result rejections', () => {
|
||||
it('rejected', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const query = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
]);
|
||||
const executor = new QueryExecutor(api);
|
||||
|
||||
expect(await executor.execute(query, { rejectResults: (_) => true })).toBeNull();
|
||||
});
|
||||
|
||||
it('not rejected', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray();
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const query = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
]);
|
||||
const executor = new QueryExecutor(api);
|
||||
|
||||
expect(
|
||||
await executor.execute(query, { rejectResults: (_) => false }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should select', () => {
|
||||
it('by id', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray({
|
||||
cameraIDs: ['camera.office'],
|
||||
count: 100,
|
||||
});
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const query = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
]);
|
||||
const executor = new QueryExecutor(api);
|
||||
|
||||
const results = await executor.execute(query, {
|
||||
selectResult: { id: 'id-camera.office-42' },
|
||||
});
|
||||
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-42');
|
||||
});
|
||||
});
|
||||
|
||||
it('by func', async () => {
|
||||
const api = createPopulatedAPI();
|
||||
const media = generateViewMediaArray({
|
||||
cameraIDs: ['camera.office'],
|
||||
count: 100,
|
||||
});
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const query = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
]);
|
||||
const executor = new QueryExecutor(api);
|
||||
|
||||
const results = await executor.execute(query, {
|
||||
selectResult: { func: (media) => media.getID() === 'id-camera.office-43' },
|
||||
});
|
||||
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-43');
|
||||
});
|
||||
|
||||
it('by time', async () => {
|
||||
const now = new Date('2024-07-21T19:09:37Z');
|
||||
|
||||
const api = createPopulatedAPI();
|
||||
const media = [
|
||||
new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
id: 'id-camera.office-0',
|
||||
startTime: now,
|
||||
}),
|
||||
new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
id: 'id-camera.office-1',
|
||||
startTime: add(now, { seconds: 1 }),
|
||||
}),
|
||||
new TestViewMedia({
|
||||
cameraID: 'camera.office',
|
||||
id: 'id-camera.office-2',
|
||||
startTime: add(now, { seconds: 2 }),
|
||||
}),
|
||||
];
|
||||
vi.mocked(api.getCameraManager().executeMediaQueries).mockResolvedValue(media);
|
||||
|
||||
const query = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event as const,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
},
|
||||
]);
|
||||
const executor = new QueryExecutor(api);
|
||||
|
||||
const results = await executor.execute(query, {
|
||||
selectResult: { time: { time: add(now, { seconds: 1 }) } },
|
||||
});
|
||||
expect(results?.getSelectedResult()?.getID()).toBe('id-camera.office-1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { vi } from 'vitest';
|
||||
import { CardController } from '../../../src/card-controller/controller';
|
||||
import { RawFrigateCardConfig } from '../../../src/config/types';
|
||||
import {
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createStore,
|
||||
} from '../../test-utils';
|
||||
|
||||
export const createPopulatedAPI = (config?: RawFrigateCardConfig): CardController => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
live: true,
|
||||
snapshots: true,
|
||||
clips: true,
|
||||
recordings: true,
|
||||
substream: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({
|
||||
live: true,
|
||||
snapshots: true,
|
||||
clips: true,
|
||||
recordings: true,
|
||||
substream: true,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
||||
return api;
|
||||
};
|
||||
@@ -0,0 +1,363 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
||||
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
||||
import { FrigateCardView } from '../../../src/config/types';
|
||||
import {
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createCardAPI,
|
||||
createStore,
|
||||
createView,
|
||||
} from '../../test-utils';
|
||||
import { ViewMedia } from '../../../src/view/media';
|
||||
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
|
||||
|
||||
describe('should act correctly when view is set', () => {
|
||||
it('basic view', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
});
|
||||
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(view);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.getView()).toBe(view);
|
||||
expect(manager.hasView()).toBeTruthy();
|
||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
||||
expect(api.getMessageManager().reset).toBeCalled();
|
||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||
expect(api.getConditionsManager()?.setState).toBeCalledWith({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'grid',
|
||||
});
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
|
||||
it('view with minor changes without media clearing or scroll', () => {
|
||||
const view_1 = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
});
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(view_1);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
|
||||
manager.setViewDefault();
|
||||
|
||||
vi.mocked(api.getMediaLoadedInfoManager().clear).mockClear();
|
||||
vi.mocked(api.getCardElementManager().scrollReset).mockClear();
|
||||
|
||||
const view_2 = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
displayMode: 'single',
|
||||
});
|
||||
factory.getViewDefault.mockReturnValue(view_2);
|
||||
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.getView()).toBe(view_2);
|
||||
|
||||
// The new view is neither a major media change, nor a different view name,
|
||||
// so media clearing and scrolling should not happen.
|
||||
expect(api.getMediaLoadedInfoManager().clear).not.toBeCalled();
|
||||
expect(api.getCardElementManager().scrollReset).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('setViewWithMergedContext', () => {
|
||||
const api = createCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
|
||||
const manager = new ViewManager(api, factory);
|
||||
const context: ViewContext = { timeline: {} };
|
||||
|
||||
// Setting context with no existing view does nothing.
|
||||
manager.setViewWithMergedContext(context);
|
||||
expect(manager.getView()).toBeNull();
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
});
|
||||
factory.getViewDefault.mockReturnValue(view);
|
||||
manager.setViewDefault();
|
||||
manager.setViewWithMergedContext(context);
|
||||
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.context).toEqual(context);
|
||||
});
|
||||
|
||||
it('getEpoch', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
expect(manager.getEpoch()).toBeTruthy();
|
||||
expect(manager.getEpoch().manager).toBe(manager);
|
||||
});
|
||||
|
||||
it('reset', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
|
||||
manager.reset();
|
||||
expect(manager.getView()).toBeNull();
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
|
||||
factory.getViewDefault.mockReturnValue(createView());
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.getView()).not.toBeNull();
|
||||
expect(manager.hasView()).toBeTruthy();
|
||||
|
||||
manager.reset();
|
||||
|
||||
expect(manager.getView()).toBeNull();
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('setViewDefault', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
});
|
||||
|
||||
it('setViewByParameters', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParameters.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewByParameters();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
});
|
||||
|
||||
it('setViewDefaultWithNewQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefaultWithNewQuery.mockResolvedValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
});
|
||||
|
||||
it('setViewByParametersWithNewQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParametersWithNewQuery.mockResolvedValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
await manager.setViewByParametersWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
});
|
||||
|
||||
it('setViewByParametersWithExistingQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParametersWithExistingQuery.mockResolvedValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
await manager.setViewByParametersWithExistingQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
});
|
||||
|
||||
describe('should handle exceptions', () => {
|
||||
it('non-async', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const error = new Error();
|
||||
factory.getViewDefault.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('async', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const error = new Error();
|
||||
factory.getViewByParametersWithNewQuery.mockRejectedValue(error);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
await manager.setViewByParametersWithNewQuery();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isViewSupportedByCamera', () => {
|
||||
it.each([
|
||||
['live' as const, false],
|
||||
['image' as const, true],
|
||||
['diagnostics' as const, true],
|
||||
['clip' as const, false],
|
||||
['clips' as const, false],
|
||||
['snapshot' as const, false],
|
||||
['snapshots' as const, false],
|
||||
['recording' as const, false],
|
||||
['recordings' as const, false],
|
||||
['timeline' as const, false],
|
||||
['media' as const, false],
|
||||
])('%s', (viewName: FrigateCardView, expected: boolean) => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera.kitchen',
|
||||
capabilities: createCapabilities({
|
||||
live: false,
|
||||
'favorite-events': false,
|
||||
'favorite-recordings': false,
|
||||
seek: false,
|
||||
clips: false,
|
||||
recordings: false,
|
||||
snapshots: false,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const manager = new ViewManager(api);
|
||||
|
||||
expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMajorMediaChange', () => {
|
||||
it('should consider undefined views as major', () => {
|
||||
const manager = new ViewManager(createCardAPI());
|
||||
|
||||
expect(manager.hasMajorMediaChange(undefined)).toBeFalsy();
|
||||
expect(manager.hasMajorMediaChange(createView())).toBeTruthy();
|
||||
expect(manager.hasMajorMediaChange()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should consider view change as major', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasMajorMediaChange(createView({ view: 'clips' }))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider camera change as major', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ camera: 'camera-1' }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasMajorMediaChange(createView({ camera: 'camera-2' }))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider live substream change as major in live view', () => {
|
||||
const overrides_1: Map<string, string> = new Map();
|
||||
overrides_1.set('camera', 'camera-2');
|
||||
|
||||
const overrides_2: Map<string, string> = new Map();
|
||||
overrides_2.set('camera', 'camera-3');
|
||||
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(
|
||||
createView({ context: { live: { overrides: overrides_1 } } }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
manager.hasMajorMediaChange(
|
||||
createView({ context: { live: { overrides: overrides_2 } } }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not consider live substream change as major in other view', () => {
|
||||
const overrides_1: Map<string, string> = new Map();
|
||||
overrides_1.set('camera', 'camera-2');
|
||||
|
||||
const overrides_2: Map<string, string> = new Map();
|
||||
overrides_2.set('camera', 'camera-3');
|
||||
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(
|
||||
createView({ view: 'clips', context: { live: { overrides: overrides_1 } } }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
manager.hasMajorMediaChange(
|
||||
createView({ view: 'clips', context: { live: { overrides: overrides_2 } } }),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should consider result change as major in other view', () => {
|
||||
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(
|
||||
createView({ view: 'media', queryResults: queryResults_1 }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
manager.hasMajorMediaChange(
|
||||
createView({ view: 'media', queryResults: queryResults_2 }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not consider selected result change as major in live view', () => {
|
||||
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ queryResults: queryResults_1 }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
manager.hasMajorMediaChange(createView({ queryResults: queryResults_2 })),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,15 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { 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(() => {
|
||||
@@ -168,210 +157,4 @@ describe('LiveController', () => {
|
||||
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,8 +1,10 @@
|
||||
import { endOfDay, startOfDay, sub } from 'date-fns';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import { CameraManagerStore } from '../../src/camera-manager/store';
|
||||
import { QueryType } from '../../src/camera-manager/types';
|
||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||
import {
|
||||
MediaFilterController,
|
||||
MediaFilterCoreDefaults,
|
||||
@@ -10,7 +12,6 @@ import {
|
||||
MediaFilterCoreWhen,
|
||||
MediaFilterMediaType,
|
||||
} from '../../src/components-lib/media-filter-controller';
|
||||
import { executeMediaQueryForViewWithErrorDispatching } from '../../src/utils/media-to-view';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
MediaQueries,
|
||||
@@ -26,8 +27,6 @@ import {
|
||||
createView,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('../../src/utils/media-to-view');
|
||||
|
||||
const createCameraStore = (options?: {
|
||||
capabilities: Capabilities;
|
||||
}): CameraManagerStore => {
|
||||
@@ -295,59 +294,82 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should get correct controls to show', () => {
|
||||
it('view with events', () => {
|
||||
const view = createView({ query: new EventMediaQueries() });
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new EventMediaQueries() }),
|
||||
);
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
controller.setViewManager(viewManager);
|
||||
expect(controller.getControlsToShow(cameraManager)).toMatchObject({
|
||||
events: true,
|
||||
recordings: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('view with recordings', () => {
|
||||
const view = createView({ query: new RecordingMediaQueries() });
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new RecordingMediaQueries() }),
|
||||
);
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
controller.setViewManager(viewManager);
|
||||
expect(controller.getControlsToShow(cameraManager)).toMatchObject({
|
||||
events: false,
|
||||
recordings: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can favorite events', () => {
|
||||
const view = createView({ query: new EventMediaQueries() });
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new EventMediaQueries() }),
|
||||
);
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ 'favorite-events': true }),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
expect(controller.getControlsToShow(cameraManager)).toMatchObject({
|
||||
favorites: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can favorite recordings', () => {
|
||||
const view = createView({ query: new RecordingMediaQueries() });
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({ query: new RecordingMediaQueries() }),
|
||||
);
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ 'favorite-recordings': true }),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
expect(controller.getControlsToShow(cameraManager)).toMatchObject({
|
||||
favorites: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can not favorite without a query', () => {
|
||||
const view = createView();
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
expect(controller.getControlsToShow(cameraManager)).toMatchObject({
|
||||
favorites: false,
|
||||
});
|
||||
});
|
||||
@@ -355,40 +377,34 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should handle value change', () => {
|
||||
it('must have visible cameras', async () => {
|
||||
const host = createLitElement();
|
||||
const controller = new MediaFilterController(host);
|
||||
await controller.valueChangeHandler(
|
||||
createCameraManager(),
|
||||
createView(),
|
||||
{},
|
||||
{ when: {} },
|
||||
);
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
expect(host.requestUpdate).not.toBeCalled();
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
await controller.valueChangeHandler(createCameraManager(), {}, { when: {} });
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('with events media type', () => {
|
||||
it.each([['clips' as const], ['snapshots' as const]])(
|
||||
'%s',
|
||||
async (viewName: 'clips' | 'snapshots') => {
|
||||
const eventListener = vi.fn();
|
||||
const host = createLitElement();
|
||||
host.addEventListener('frigate-card:view:change', eventListener);
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(host);
|
||||
const cameraManager = createCameraManager();
|
||||
const view = createView();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
vi.mocked(executeMediaQueryForViewWithErrorDispatching).mockResolvedValueOnce(
|
||||
view,
|
||||
);
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
const cameraManager = createCameraManager(createCameraStore());
|
||||
|
||||
const from = new Date('2024-02-06T21:59');
|
||||
const to = new Date('2024-02-06T22:00');
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
view,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
@@ -397,6 +413,7 @@ describe('MediaFilterController', () => {
|
||||
}),
|
||||
},
|
||||
{
|
||||
camera: 'camera.kitchen',
|
||||
mediaType:
|
||||
viewName === 'clips'
|
||||
? MediaFilterMediaType.Clips
|
||||
@@ -412,20 +429,14 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(vi.mocked(executeMediaQueryForViewWithErrorDispatching)).toBeCalledWith(
|
||||
host,
|
||||
cameraManager,
|
||||
view,
|
||||
expect.anything(),
|
||||
{
|
||||
targetCameraID: 'camera.kitchen',
|
||||
targetView: viewName,
|
||||
},
|
||||
);
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
|
||||
params: expect.objectContaining({
|
||||
camera: 'camera.kitchen',
|
||||
view: viewName,
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeMediaQueryForViewWithErrorDispatching)
|
||||
.mock.calls[0][3].getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -442,31 +453,29 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(eventListener).toBeCalled();
|
||||
expect(host.requestUpdate).toBeCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('with recordings media type', async () => {
|
||||
const eventListener = vi.fn();
|
||||
const host = createLitElement();
|
||||
host.addEventListener('frigate-card:view:change', eventListener);
|
||||
|
||||
const cameraManager = createCameraManager(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const controller = new MediaFilterController(host);
|
||||
const cameraManager = createCameraManager();
|
||||
const view = createView();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
vi.mocked(executeMediaQueryForViewWithErrorDispatching).mockResolvedValueOnce(
|
||||
view,
|
||||
);
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
const from = new Date('2024-02-06T21:59');
|
||||
const to = new Date('2024-02-06T22:00');
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
view,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
@@ -484,20 +493,15 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(vi.mocked(executeMediaQueryForViewWithErrorDispatching)).toBeCalledWith(
|
||||
host,
|
||||
cameraManager,
|
||||
view,
|
||||
expect.anything(),
|
||||
{
|
||||
targetCameraID: 'camera.kitchen',
|
||||
targetView: 'recordings',
|
||||
},
|
||||
);
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
|
||||
params: expect.objectContaining({
|
||||
camera: 'camera.kitchen',
|
||||
view: 'recordings',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeMediaQueryForViewWithErrorDispatching)
|
||||
.mock.calls[0][3].getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -509,18 +513,21 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(eventListener).toBeCalled();
|
||||
expect(host.requestUpdate).toBeCalled();
|
||||
});
|
||||
|
||||
it('without favorites', async () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
createView(),
|
||||
{},
|
||||
{
|
||||
mediaType: MediaFilterMediaType.Recordings,
|
||||
@@ -528,10 +535,15 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalledWith({
|
||||
params: expect.objectContaining({
|
||||
camera: 'camera.kitchen',
|
||||
view: 'recordings',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeMediaQueryForViewWithErrorDispatching)
|
||||
.mock.calls[0][3].getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -575,13 +587,17 @@ describe('MediaFilterController', () => {
|
||||
new Date('2024-02-29T23:59:59.999'),
|
||||
],
|
||||
])('%s', async (value: MediaFilterCoreWhen | string, from: Date, to: Date) => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
createView(),
|
||||
{},
|
||||
{
|
||||
mediaType: MediaFilterMediaType.Recordings,
|
||||
@@ -590,11 +606,8 @@ describe('MediaFilterController', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeMediaQueryForViewWithErrorDispatching)
|
||||
.mock.calls[0][3].getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -606,13 +619,17 @@ describe('MediaFilterController', () => {
|
||||
});
|
||||
|
||||
it('custom without values', async () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
await controller.valueChangeHandler(
|
||||
cameraManager,
|
||||
createView(),
|
||||
{},
|
||||
{
|
||||
mediaType: MediaFilterMediaType.Recordings,
|
||||
@@ -623,9 +640,7 @@ describe('MediaFilterController', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
vi
|
||||
.mocked(executeMediaQueryForViewWithErrorDispatching)
|
||||
.mock.calls[0][3].getQueries(),
|
||||
viewManager.setViewByParametersWithExistingQuery.mock.calls[0][0]?.params?.query?.getQueries(),
|
||||
).toEqual([
|
||||
{
|
||||
cameraIDs: new Set(['camera.kitchen']),
|
||||
@@ -638,24 +653,25 @@ describe('MediaFilterController', () => {
|
||||
|
||||
describe('should calculate correct defaults', () => {
|
||||
it('with no queries', () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
controller.computeInitialDefaultsFromView(createCameraManager(), createView());
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
controller.computeInitialDefaultsFromView(createCameraManager());
|
||||
|
||||
expect(controller.getDefaults()).toBeNull();
|
||||
});
|
||||
|
||||
it('with no cameras', () => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(createView());
|
||||
|
||||
controller.computeInitialDefaultsFromView(
|
||||
createCameraManager(),
|
||||
createView({
|
||||
query: new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera.kitchen']) },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
controller.computeInitialDefaultsFromView(createCameraManager());
|
||||
|
||||
expect(controller.getDefaults()).toBeNull();
|
||||
});
|
||||
@@ -948,17 +964,21 @@ describe('MediaFilterController', () => {
|
||||
mediaQueries: MediaQueries,
|
||||
defaults: MediaFilterCoreDefaults | null,
|
||||
) => {
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
controller.computeInitialDefaultsFromView(
|
||||
cameraManager,
|
||||
const viewManager = mock<ViewManager>();
|
||||
viewManager.getView.mockReturnValue(
|
||||
createView({
|
||||
query: mediaQueries,
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new MediaFilterController(createLitElement());
|
||||
controller.setViewManager(viewManager);
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||
|
||||
controller.computeInitialDefaultsFromView(cameraManager);
|
||||
|
||||
expect(controller.getDefaults()).toEqual(defaults);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CameraManager } from '../../src/camera-manager/manager';
|
||||
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
|
||||
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||
import { ViewManager } from '../../src/card-controller/view-manager';
|
||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||
import {
|
||||
MenuButtonController,
|
||||
MenuButtonControllerOptions,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { MergeContextViewModifier } from '../../../src/card-controller/view/modifiers/merge-context';
|
||||
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
||||
import {
|
||||
generateViewContextForZoom,
|
||||
handleZoomSettingsObservedEvent,
|
||||
} from '../../../src/components-lib/zoom/zoom-view-context';
|
||||
|
||||
vi.mock('../../../src/card-controller/view/modifiers/merge-context');
|
||||
|
||||
describe('generateViewContextForZoom', () => {
|
||||
it('with observed', () => {
|
||||
expect(
|
||||
@@ -53,11 +58,9 @@ describe('generateViewContextForZoom', () => {
|
||||
|
||||
// @vitest-environment jsdom
|
||||
it('handleZoomSettingsObservedEvent', () => {
|
||||
const element = document.createElement('div');
|
||||
const callback = vi.fn();
|
||||
element.addEventListener('frigate-card:view:change-context', callback);
|
||||
const viewManager = mock<ViewManager>();
|
||||
|
||||
handleZoomSettingsObservedEvent(
|
||||
element,
|
||||
new CustomEvent('frigate-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
@@ -66,18 +69,21 @@ it('handleZoomSettingsObservedEvent', () => {
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
viewManager,
|
||||
'target',
|
||||
);
|
||||
expect(callback).toBeCalledWith(
|
||||
expect(viewManager.setViewByParameters).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
zoom: {
|
||||
target: {
|
||||
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },
|
||||
requested: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
modifiers: [expect.any(MergeContextViewModifier)],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(MergeContextViewModifier).toBeCalledWith({
|
||||
zoom: {
|
||||
target: {
|
||||
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },
|
||||
requested: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+14
-10
@@ -35,7 +35,7 @@ import { MicrophoneManager } from '../src/card-controller/microphone-manager';
|
||||
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
||||
import { StyleManager } from '../src/card-controller/style-manager';
|
||||
import { TriggersManager } from '../src/card-controller/triggers-manager';
|
||||
import { ViewManager } from '../src/card-controller/view-manager';
|
||||
import { ViewManager } from '../src/card-controller/view/view-manager';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateCardCondition,
|
||||
@@ -278,14 +278,6 @@ export const createMediaLoadedInfoEvent = (
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -297,7 +289,12 @@ export const generateViewMediaArray = (options?: {
|
||||
const media: ViewMedia[] = [];
|
||||
for (let i = 0; i < (options?.count ?? 100); ++i) {
|
||||
for (const cameraID of options?.cameraIDs ?? ['kitchen', 'office']) {
|
||||
media.push(new TestViewMedia({ cameraID: cameraID, id: `id-${cameraID}-${i}` }));
|
||||
media.push(
|
||||
new TestViewMedia({
|
||||
cameraID: cameraID,
|
||||
id: `id-${cameraID}-${i}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return media;
|
||||
@@ -469,3 +466,10 @@ export const callHASubscribeMessageHandler = (
|
||||
expect(mock.calls.length).greaterThan(n);
|
||||
mock.calls[n][0](ev);
|
||||
};
|
||||
|
||||
/**
|
||||
* Flush resolved promises.
|
||||
*/
|
||||
export const flushPromises = async (): Promise<void> => {
|
||||
await new Promise(process.nextTick);
|
||||
};
|
||||
|
||||
@@ -1,600 +0,0 @@
|
||||
import { add, sub } from 'date-fns';
|
||||
import { beforeEach, describe, expect, it, Mock, MockedFunction, vi } from 'vitest';
|
||||
import { QueryType } from '../../src/camera-manager/types';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
executeMediaQueryForView,
|
||||
executeMediaQueryForViewWithErrorDispatching,
|
||||
findBestMediaIndex,
|
||||
} from '../../src/utils/media-to-view';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { EventMediaQueries } from '../../src/view/media-queries';
|
||||
import {
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
createPerformanceConfig,
|
||||
createStore,
|
||||
createView,
|
||||
TestViewMedia,
|
||||
} from '../test-utils';
|
||||
|
||||
const createElementListenForView = (): {
|
||||
element: HTMLElement;
|
||||
viewHandler: EventListener;
|
||||
messageHandler: EventListener;
|
||||
} => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const viewHandler = vi.fn();
|
||||
element.addEventListener('frigate-card:view:change', viewHandler);
|
||||
|
||||
const messageHandler = vi.fn();
|
||||
element.addEventListener('frigate-card:message', messageHandler);
|
||||
|
||||
return {
|
||||
element: element,
|
||||
viewHandler: viewHandler,
|
||||
messageHandler: messageHandler,
|
||||
};
|
||||
};
|
||||
|
||||
const getMediaFromHandlerCall = (handler: Mock<any, any>): ViewMedia[] | null => {
|
||||
return handler.mock.calls[0][0].detail.queryResults.getResults();
|
||||
};
|
||||
|
||||
const generateViewMedia = (
|
||||
index: number,
|
||||
base: Date,
|
||||
durationSeconds: number,
|
||||
cameraID?: string,
|
||||
): ViewMedia => {
|
||||
return new TestViewMedia({
|
||||
id: `id-${index}`,
|
||||
startTime: base,
|
||||
endTime: add(base, { seconds: durationSeconds }),
|
||||
...(cameraID && { cameraID: cameraID }),
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('changeViewToRecentEventsForCameraAndDependents', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing without camera config for selected camera', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createCameraManager(),
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without camera configs for all cameras', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createCameraManager(),
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
allCameras: true,
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing unless queries can be created', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue(null);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
eventsMediaType: 'clips',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch new view on success', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ clips: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera']),
|
||||
},
|
||||
]);
|
||||
|
||||
const mediaArray = [new ViewMedia('clip', 'camera')];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
targetView: 'clips',
|
||||
select: 'latest',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).toBeCalled();
|
||||
expect(getMediaFromHandlerCall(vi.mocked(elementHandler.viewHandler))).toBe(
|
||||
mediaArray,
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch error message on fail', async () => {
|
||||
vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ clips: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera']),
|
||||
},
|
||||
]);
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
expect(elementHandler.messageHandler).toBeCalled();
|
||||
});
|
||||
|
||||
it('should respect media chunk size', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ clips: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
cameraManager,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
media_chunk_size: 1000,
|
||||
},
|
||||
}),
|
||||
},
|
||||
createView(),
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultEventQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
limit: 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect useCache', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ clips: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera']),
|
||||
},
|
||||
]);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
useCache: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(vi.mocked(cameraManager.executeMediaQueries)).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
useCache: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should respect request for media type', () => {
|
||||
it.each([
|
||||
['snapshots' as const, 'hasSnapshot'],
|
||||
['clips' as const, 'hasClip'],
|
||||
])('%s', async (mediaType, queryParameter) => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ [mediaType]: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
eventsMediaType: mediaType,
|
||||
},
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultEventQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
[queryParameter]: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('executeMediaQueryForView', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should not execute empty queries', async () => {
|
||||
expect(
|
||||
await executeMediaQueryForView(
|
||||
createCameraManager(),
|
||||
createView(),
|
||||
new EventMediaQueries(),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw on failure', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||
|
||||
await expect(
|
||||
executeMediaQueryForView(
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set('camera'),
|
||||
},
|
||||
]),
|
||||
),
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should select time-based result', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
const view = await executeMediaQueryForView(
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set('camera'),
|
||||
},
|
||||
]),
|
||||
{
|
||||
select: 'time',
|
||||
targetTime: add(now, { seconds: 30 }),
|
||||
},
|
||||
);
|
||||
|
||||
// Should select the longest event.
|
||||
expect(view?.queryResults?.getSelectedIndex()).toBe(1);
|
||||
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
||||
});
|
||||
|
||||
it('should select nothing when time-based selection does not match', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
|
||||
const view = await executeMediaQueryForView(
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set('camera'),
|
||||
},
|
||||
]),
|
||||
{
|
||||
select: 'time',
|
||||
targetTime: sub(now, { seconds: 30 }),
|
||||
},
|
||||
);
|
||||
|
||||
// Should leave selection untouched (last item will remain selected).
|
||||
expect(view?.queryResults?.getSelectedIndex()).toBe(2);
|
||||
expect(view?.queryResults?.getResults()).toBe(mediaArray);
|
||||
});
|
||||
|
||||
it('should select nothing when query returns null', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
|
||||
|
||||
expect(
|
||||
await executeMediaQueryForView(
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set('camera'),
|
||||
},
|
||||
]),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('changeViewToRecentRecordingForCameraAndDependents', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing without camera config for selected camera', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createCameraManager(),
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without camera configs for all cameras', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
createCameraManager(),
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
allCameras: true,
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing unless queries can be created', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue(null);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
);
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch new view on success', async () => {
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ recordings: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const mediaArray = [new ViewMedia('recording', 'camera')];
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: new Set(['camera']),
|
||||
},
|
||||
]);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
targetView: 'recordings',
|
||||
select: 'latest',
|
||||
},
|
||||
);
|
||||
expect(elementHandler.viewHandler).toBeCalled();
|
||||
expect(getMediaFromHandlerCall(vi.mocked(elementHandler.viewHandler))).toBe(
|
||||
mediaArray,
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect media chunk size', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ recordings: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
cameraManager,
|
||||
{
|
||||
performance: createPerformanceConfig({
|
||||
features: {
|
||||
media_chunk_size: 1000,
|
||||
},
|
||||
}),
|
||||
},
|
||||
createView(),
|
||||
);
|
||||
|
||||
expect(cameraManager.generateDefaultRecordingQueries).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
limit: 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should respect useCache', async () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera',
|
||||
capabilities: createCapabilities({ recordings: true }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: new Set(['camera']),
|
||||
},
|
||||
]);
|
||||
|
||||
await changeViewToRecentRecordingForCameraAndDependents(
|
||||
createElementListenForView().element,
|
||||
cameraManager,
|
||||
{},
|
||||
createView(),
|
||||
{
|
||||
targetView: 'recordings',
|
||||
select: 'latest',
|
||||
useCache: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(vi.mocked(cameraManager.executeMediaQueries)).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
useCache: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('executeMediaQueryForViewWithErrorDispatching', () => {
|
||||
it('should dispatch error message on fail', async () => {
|
||||
vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||
|
||||
const elementHandler = createElementListenForView();
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
|
||||
|
||||
await executeMediaQueryForViewWithErrorDispatching(
|
||||
elementHandler.element,
|
||||
cameraManager,
|
||||
createView(),
|
||||
new EventMediaQueries([{ type: QueryType.Event, cameraIDs: new Set(['camera']) }]),
|
||||
);
|
||||
|
||||
expect(elementHandler.viewHandler).not.toBeCalled();
|
||||
expect(elementHandler.messageHandler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('findBestMediaIndex', () => {
|
||||
it('should find best media index', async () => {
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60),
|
||||
generateViewMedia(1, now, 120),
|
||||
generateViewMedia(2, now, 10),
|
||||
];
|
||||
|
||||
expect(findBestMediaIndex(mediaArray, add(now, { seconds: 30 }))).toBe(1);
|
||||
});
|
||||
|
||||
it('should find best media index respecting favored cameraID', async () => {
|
||||
const now = new Date();
|
||||
const mediaArray = [
|
||||
generateViewMedia(0, now, 60, 'less-good-camera'),
|
||||
generateViewMedia(1, now, 120, 'less-good-camera'),
|
||||
generateViewMedia(2, now, 10, 'favored-camera'),
|
||||
generateViewMedia(3, now, 35, 'favored-camera'),
|
||||
generateViewMedia(4, now, 40, 'favored-camera'),
|
||||
generateViewMedia(5, now, 30, 'favored-camera'),
|
||||
generateViewMedia(6, now, 300, 'less-good-camera'),
|
||||
];
|
||||
|
||||
expect(
|
||||
findBestMediaIndex(mediaArray, add(now, { seconds: 30 }), 'favored-camera'),
|
||||
).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getStreamCameraID, hasSubstream } from '../../src/utils/substream';
|
||||
import {
|
||||
getStreamCameraID,
|
||||
hasSubstream,
|
||||
removeSubstream,
|
||||
} from '../../src/utils/substream';
|
||||
import { View } from '../../src/view/view';
|
||||
|
||||
describe('hasSubstream/getStreamCameraID', () => {
|
||||
@@ -54,3 +58,41 @@ describe('hasSubstream/getStreamCameraID', () => {
|
||||
expect(getStreamCameraID(view, 'camera3')).toBe('camera4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSubstream', () => {
|
||||
it('should remove substream that exists', () => {
|
||||
const view = new View({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
removeSubstream(view);
|
||||
expect(view.context).toEqual({
|
||||
live: {
|
||||
overrides: new Map(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not remove substream that does not exists', () => {
|
||||
const view = new View({
|
||||
view: 'live',
|
||||
camera: 'camera-has-no-overrides',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
removeSubstream(view);
|
||||
expect(view.context).toEqual({
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,9 @@ describe('getCameraIDsForViewName', () => {
|
||||
['media' as const, 'clips' as const],
|
||||
['media' as const, 'snapshots' as const],
|
||||
['media' as const, 'recordings' as const],
|
||||
['timeline' as const, 'clips' as const],
|
||||
['timeline' as const, 'snapshots' as const],
|
||||
['timeline' as const, 'recordings' as const],
|
||||
])('%s', (viewName: FrigateCardView, capabilityKey: CapabilityKey) => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
|
||||
+21
-344
@@ -1,12 +1,8 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { QueryType } from '../../src/camera-manager/types';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { EventMediaQueries } from '../../src/view/media-queries';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import { View, dispatchViewContextChangeEvent } from '../../src/view/view';
|
||||
import { createView } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('View Basics', () => {
|
||||
it('should construct from parameters', () => {
|
||||
const query = new EventMediaQueries();
|
||||
@@ -28,21 +24,28 @@ describe('View Basics', () => {
|
||||
expect(view.context).toBe(context);
|
||||
});
|
||||
|
||||
it('should clone', () => {
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
const context = {};
|
||||
describe('should clone', () => {
|
||||
it('with query and queryResults', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
query: new EventMediaQueries(),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
context: {},
|
||||
});
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
context: context,
|
||||
expect(view.clone()).toEqual(view);
|
||||
});
|
||||
|
||||
const clone = view.clone();
|
||||
expect(clone).toEqual(view);
|
||||
it('without query and queryResults', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
context: {},
|
||||
});
|
||||
|
||||
expect(view.clone()).toEqual(view);
|
||||
});
|
||||
});
|
||||
|
||||
it('should evolve with everything set', () => {
|
||||
@@ -201,316 +204,6 @@ describe('View Basics', () => {
|
||||
expect(createView({ view: 'timeline' }).getDefaultMediaType()).toBeNull();
|
||||
});
|
||||
|
||||
it('should dispatch view', () => {
|
||||
const element = document.createElement('div');
|
||||
const view = createView();
|
||||
const handler = vi.fn((ev) => {
|
||||
expect(ev.detail).toBe(view);
|
||||
});
|
||||
|
||||
element.addEventListener('frigate-card:view:change', handler);
|
||||
view.dispatchChangeEvent(element);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View.isMajorMediaChange', () => {
|
||||
it('should consider undefined views as major', () => {
|
||||
expect(View.isMajorMediaChange(createView(), undefined)).toBeTruthy();
|
||||
expect(View.isMajorMediaChange(undefined, createView())).toBeTruthy();
|
||||
expect(View.isMajorMediaChange()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider view change as major', () => {
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ view: 'live' }),
|
||||
createView({ view: 'snapshots' }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider camera change as major', () => {
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ camera: 'camera-1' }),
|
||||
createView({ camera: 'camera-2' }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider live substream change as major in live view', () => {
|
||||
const overrides_1: Map<string, string> = new Map();
|
||||
overrides_1.set('camera', 'camera-2');
|
||||
|
||||
const overrides_2: Map<string, string> = new Map();
|
||||
overrides_2.set('camera', 'camera-3');
|
||||
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ context: { live: { overrides: overrides_1 } } }),
|
||||
createView({ context: { live: { overrides: overrides_2 } } }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not consider live substream change as major in other view', () => {
|
||||
const overrides_1: Map<string, string> = new Map();
|
||||
overrides_1.set('camera', 'camera-2');
|
||||
|
||||
const overrides_2: Map<string, string> = new Map();
|
||||
overrides_2.set('camera', 'camera-3');
|
||||
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ view: 'clips', context: { live: { overrides: overrides_1 } } }),
|
||||
createView({ view: 'clips', context: { live: { overrides: overrides_2 } } }),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should consider result change as major in other view', () => {
|
||||
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ view: 'media', queryResults: queryResults_1 }),
|
||||
createView({ view: 'media', queryResults: queryResults_2 }),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not consider selected result change as major in live view', () => {
|
||||
const media = [new ViewMedia('clip', 'camera-1'), new ViewMedia('clip', 'camera-2')];
|
||||
const queryResults_1 = new MediaQueriesResults({ results: media, selectedIndex: 0 });
|
||||
const queryResults_2 = new MediaQueriesResults({ results: media, selectedIndex: 1 });
|
||||
expect(
|
||||
View.isMajorMediaChange(
|
||||
createView({ queryResults: queryResults_1 }),
|
||||
createView({ queryResults: queryResults_2 }),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View.adoptFromViewIfAppropriate', () => {
|
||||
it('should adopt for gallery case', () => {
|
||||
const query = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
]);
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const current = createView({
|
||||
view: 'clip',
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
const next = createView({ view: 'clips' });
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.view).toBe('clips');
|
||||
expect(next.query).toBe(query);
|
||||
expect(next.queryResults).toBe(queryResults);
|
||||
});
|
||||
|
||||
it('should not adopt for gallery case if neither query nor results in current view', () => {
|
||||
const current = createView({
|
||||
view: 'clip',
|
||||
query: null,
|
||||
queryResults: null,
|
||||
});
|
||||
|
||||
const nextQuery = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
]);
|
||||
const nextResults = new MediaQueriesResults();
|
||||
const next = createView({
|
||||
view: 'clips',
|
||||
query: nextQuery,
|
||||
queryResults: nextResults,
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.view).toBe('clips');
|
||||
expect(next.query).toBe(nextQuery);
|
||||
expect(next.queryResults).toBe(nextResults);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
]),
|
||||
'clip',
|
||||
],
|
||||
[
|
||||
new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
|
||||
]),
|
||||
'snapshot',
|
||||
],
|
||||
[
|
||||
new RecordingMediaQueries([
|
||||
{ type: QueryType.Recording, cameraIDs: new Set(['camera']) },
|
||||
]),
|
||||
'recording',
|
||||
],
|
||||
])('should adopt in media case', (mediaQueries, expectedView) => {
|
||||
const current = createView({
|
||||
view: 'media',
|
||||
query: mediaQueries,
|
||||
queryResults: new MediaQueriesResults(),
|
||||
});
|
||||
const next = createView({ view: 'media' });
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.view).toBe(expectedView);
|
||||
expect(next.query).toBeFalsy();
|
||||
expect(next.queryResults).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not adopt for mixed queries in media case', () => {
|
||||
const query = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true },
|
||||
]);
|
||||
const results = new MediaQueriesResults();
|
||||
const current = createView({
|
||||
view: 'media',
|
||||
query: query,
|
||||
queryResults: results,
|
||||
});
|
||||
const next = createView({ view: 'media' });
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
|
||||
expect(next.view).toBe('media');
|
||||
expect(next.query).toBeNull();
|
||||
expect(next.queryResults).toBeNull();
|
||||
});
|
||||
|
||||
it('should not adopt when queries and results present in next view in media case', () => {
|
||||
const currentQuery = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']), hasClip: true },
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']), hasSnapshot: true },
|
||||
]);
|
||||
const currentResults = new MediaQueriesResults();
|
||||
const current = createView({
|
||||
view: 'media',
|
||||
query: currentQuery,
|
||||
queryResults: currentResults,
|
||||
});
|
||||
|
||||
const nextQuery = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera-2']), hasClip: true },
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera-2']), hasSnapshot: true },
|
||||
]);
|
||||
const nextResults = new MediaQueriesResults();
|
||||
const next = createView({
|
||||
view: 'media',
|
||||
query: nextQuery,
|
||||
queryResults: nextResults,
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
|
||||
expect(next.view).toBe('media');
|
||||
expect(next.query).toBe(nextQuery);
|
||||
expect(next.queryResults).toBe(nextResults);
|
||||
});
|
||||
|
||||
it('should not adopt for other case', () => {
|
||||
const query = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
]);
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const current = createView({
|
||||
view: 'media',
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
const next = createView({ view: 'live' });
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.view).toBe('live');
|
||||
expect(next.query).toBeFalsy();
|
||||
expect(next.queryResults).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should do nothing with undefined next view', () => {
|
||||
const view_1 = createView();
|
||||
const view_2 = view_1.clone();
|
||||
View.adoptFromViewIfAppropriate(view_1);
|
||||
expect(view_1).toEqual(view_2);
|
||||
});
|
||||
|
||||
it('should adopt with query but no queryResults', () => {
|
||||
const query = new EventMediaQueries([
|
||||
{ type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true },
|
||||
]);
|
||||
|
||||
const current = createView({
|
||||
view: 'media',
|
||||
query: query,
|
||||
});
|
||||
const next = createView({
|
||||
view: 'media',
|
||||
query: query,
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.view).toBe('clip');
|
||||
});
|
||||
|
||||
it('should adopt live context overrides for substreams', () => {
|
||||
const current = createView({
|
||||
view: 'live',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
const next = createView({
|
||||
view: 'live',
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.context?.live).toEqual(current.context?.live);
|
||||
});
|
||||
|
||||
it('should not adopt live context overrides if there are new overrides', () => {
|
||||
const current = createView({
|
||||
view: 'live',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
const next = createView({
|
||||
view: 'live',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera3']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.context?.live?.overrides).toEqual(new Map([['camera', 'camera3']]));
|
||||
});
|
||||
|
||||
it('should adopt live context overrides even if there is new context', () => {
|
||||
const current = createView({
|
||||
view: 'live',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
const next = createView({
|
||||
view: 'live',
|
||||
context: {},
|
||||
});
|
||||
View.adoptFromViewIfAppropriate(next, current);
|
||||
expect(next.context?.live).toEqual(current.context?.live);
|
||||
});
|
||||
|
||||
it('should determine if display mode is grid', () => {
|
||||
expect(createView({ displayMode: 'grid' }).isGrid()).toBeTruthy();
|
||||
expect(createView({ displayMode: 'single' }).isGrid()).toBeFalsy();
|
||||
@@ -535,19 +228,3 @@ describe('View.adoptFromViewIfAppropriate', () => {
|
||||
expect(createView({ view: 'timeline' }).supportsMultipleDisplayModes()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchViewContextChangeEvent', () => {
|
||||
it('should dispatch event', () => {
|
||||
const context = {};
|
||||
const handler = vi.fn((ev) => {
|
||||
expect(ev.detail).toBe(context);
|
||||
});
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('frigate-card:view:change-context', handler);
|
||||
|
||||
dispatchViewContextChangeEvent(element, context);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user