Files
advanced-camera-card/tests/utils/media-to-view.test.ts
T

453 lines
12 KiB
TypeScript

import add from 'date-fns/add';
import sub from 'date-fns/sub';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import {
CameraConfigs,
PartialRecordingQuery,
QueryType,
} from '../../src/camera-manager/types';
import { setify } from '../../src/utils/basic';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
createQueriesForRecordingsView,
executeMediaQueryForView,
findBestMediaIndex,
} from '../../src/utils/media-to-view';
import { ViewMedia } from '../../src/view/media';
import { EventMediaQueries } from '../../src/view/media-queries';
import {
createCameraManager,
createHASS,
createPerformanceConfig,
createView,
TestViewMedia,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
const createElementListenForView = (): {
element: HTMLElement;
viewHandler: Mock<any, any>;
messageHandler: Mock<any, any>;
} => {
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,
): ViewMedia => {
return new TestViewMedia({
id: `id-${index}`,
startTime: base,
endTime: add(base, { seconds: durationSeconds }),
});
};
// @vitest-environment jsdom
describe('changeViewToRecentEventsForCameraAndDependents', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should do nothing without camera config for selected camera', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
createView(),
);
expect(elementHandler.viewHandler).not.toBeCalled();
});
it('should do nothing without camera configs for all cameras', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
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,
createHASS(),
cameraManager,
{},
createView(),
{
mediaType: 'clips',
},
);
expect(elementHandler.viewHandler).not.toBeCalled();
});
it('should dispatch new view on success', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager();
const mediaArray = [new ViewMedia('clip', 'camera')];
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
createView(),
{
targetView: 'clips',
select: 'latest',
},
);
expect(elementHandler.viewHandler).toBeCalled();
expect(getMediaFromHandlerCall(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.executeMediaQueries).mockRejectedValue(new Error());
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
createView(),
);
expect(elementHandler.viewHandler).not.toBeCalled();
expect(elementHandler.messageHandler).toBeCalled();
});
it('should respect media chunk size', async () => {
const cameraManager = createCameraManager();
await changeViewToRecentEventsForCameraAndDependents(
createElementListenForView().element,
createHASS(),
cameraManager,
{
performance: createPerformanceConfig({
features: {
media_chunk_size: 1000,
},
}),
},
createView(),
);
expect(cameraManager.generateDefaultEventQueries).toBeCalledWith(
expect.anything(),
expect.objectContaining({
limit: 1000,
}),
);
});
describe('should respect request for media type', () => {
it.each([
['snapshots' as const, 'hasSnapshot'],
['clips' as const, 'hasClip'],
])('%s', async (mediaType, queryParameter) => {
const cameraManager = createCameraManager();
await changeViewToRecentEventsForCameraAndDependents(
createElementListenForView().element,
createHASS(),
cameraManager,
{},
createView(),
{
mediaType: 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 () => {
const elementHandler = createElementListenForView();
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
expect(
await executeMediaQueryForView(
elementHandler.element,
createHASS(),
cameraManager,
createView(),
new EventMediaQueries(),
),
).toBeNull();
});
it('should select time-based result', async () => {
const elementHandler = createElementListenForView();
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
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(
elementHandler.element,
createHASS(),
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
{
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 elementHandler = createElementListenForView();
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
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(
elementHandler.element,
createHASS(),
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
{
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);
});
});
// @vitest-environment jsdom
describe('changeViewToRecentRecordingForCameraAndDependents', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should do nothing without camera config for selected camera', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
createView(),
);
expect(elementHandler.viewHandler).not.toBeCalled();
});
it('should do nothing without camera configs for all cameras', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
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,
createHASS(),
cameraManager,
{},
createView(),
);
expect(elementHandler.viewHandler).not.toBeCalled();
});
it('should dispatch new view on success', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager();
const mediaArray = [new ViewMedia('recording', 'camera')];
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
createHASS(),
cameraManager,
{},
createView(),
{
targetView: 'recordings',
select: 'latest',
},
);
expect(elementHandler.viewHandler).toBeCalled();
expect(getMediaFromHandlerCall(elementHandler.viewHandler)).toBe(mediaArray);
});
it('should respect media chunk size', async () => {
const cameraManager = createCameraManager();
await changeViewToRecentRecordingForCameraAndDependents(
createElementListenForView().element,
createHASS(),
cameraManager,
{
performance: createPerformanceConfig({
features: {
media_chunk_size: 1000,
},
}),
},
createView(),
);
expect(cameraManager.generateDefaultRecordingQueries).toBeCalledWith(
expect.anything(),
expect.objectContaining({
limit: 1000,
}),
);
});
});
// @vitest-environment jsdom
describe('createQueriesForRecordingsView', () => {
it('should respect start and end date in recording query', async () => {
const cameraManager = createCameraManager({ configs: new Map() });
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockImplementation(
(cameraIDs: string | Set<string>, partialQuery?: PartialRecordingQuery) => [
{
cameraIDs: setify(cameraIDs),
type: QueryType.Recording,
...partialQuery,
},
],
);
const start = new Date('2023-04-29T14:00:00');
const end = new Date('2023-04-29T14:59:59');
const queries = createQueriesForRecordingsView(
cameraManager,
{},
new Set(['camera']),
{
start: start,
end: end,
},
);
expect(queries?.getQueries()).toEqual(
expect.arrayContaining([
expect.objectContaining({
start: start,
end: end,
}),
]),
);
});
});
// @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);
});
});