- Add support for Frigate reviews / detections. - Add support for GenAI metadata. - Significant internal refactor to more flexible "UnifiedQuery" to allow mixing cameras with simple metadata and review metadata (e.g. a timeline view of a Frigate camera with reviews, and a Reolink camera with simple metadata). - Add support for folder media as camera media. There are a few more PRs to commit prior to this going live, but commiting this for now due to the scale of the change. BREAKING CHANGE: `media_type` and `events_type` are retired under `live`, `viewer` and `timeline` configuration sections, instead media type is associated (once) with the camera under `media`.
261 lines
9.6 KiB
TypeScript
261 lines
9.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { UnifiedQuery } from '../../src/view/unified-query';
|
|
import { QueryResults } from '../../src/view/query-results';
|
|
import { createView } from '../test-utils';
|
|
|
|
describe('View Basics', () => {
|
|
it('should construct from parameters', () => {
|
|
const query = new UnifiedQuery();
|
|
const queryResults = new QueryResults();
|
|
const context = {};
|
|
|
|
const view = createView({
|
|
view: 'live',
|
|
camera: 'camera',
|
|
query: query,
|
|
queryResults: queryResults,
|
|
context: context,
|
|
});
|
|
expect(view.is('live')).toBeTruthy();
|
|
expect(view.view).toBe('live');
|
|
expect(view.camera).toBe('camera');
|
|
expect(view.query).toBe(query);
|
|
expect(view.queryResults).toBe(queryResults);
|
|
expect(view.context).toBe(context);
|
|
});
|
|
|
|
describe('should clone', () => {
|
|
it('with query and queryResults', () => {
|
|
const view = createView({
|
|
view: 'live',
|
|
camera: 'camera',
|
|
query: new UnifiedQuery(),
|
|
queryResults: new QueryResults(),
|
|
context: {},
|
|
});
|
|
|
|
expect(view.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', () => {
|
|
const view = createView({
|
|
view: 'live',
|
|
camera: 'camera-1',
|
|
query: new UnifiedQuery(),
|
|
queryResults: new QueryResults(),
|
|
context: {},
|
|
displayMode: 'single',
|
|
});
|
|
|
|
const evolved = view.evolve({
|
|
view: 'clips',
|
|
camera: 'camera-2',
|
|
query: new UnifiedQuery(),
|
|
queryResults: new QueryResults(),
|
|
context: {},
|
|
displayMode: 'grid',
|
|
});
|
|
expect(evolved.view).not.toBe(view.view);
|
|
expect(evolved.camera).not.toBe(view.camera);
|
|
expect(evolved.query).not.toBe(view.query);
|
|
expect(evolved.queryResults).not.toBe(view.queryResults);
|
|
expect(evolved.context).not.toBe(view.context);
|
|
expect(evolved.displayMode).not.toBe(view.displayMode);
|
|
});
|
|
|
|
it('should evolve with nothing set', () => {
|
|
const view = createView({
|
|
view: 'live',
|
|
camera: 'camera-1',
|
|
query: new UnifiedQuery(),
|
|
queryResults: new QueryResults(),
|
|
context: {},
|
|
});
|
|
|
|
const evolved = view.evolve({});
|
|
|
|
expect(evolved.view).toBe(view.view);
|
|
expect(evolved.camera).toBe(view.camera);
|
|
expect(evolved.context).toBe(view.context);
|
|
|
|
// Query and QueryResults are cloned if not set.
|
|
expect(evolved.query).not.toBe(view.query);
|
|
expect(evolved.query).toEqual(view.query);
|
|
expect(evolved.queryResults).not.toBe(view.queryResults);
|
|
expect(evolved.queryResults).toEqual(view.queryResults);
|
|
});
|
|
|
|
it('should not clone query and queryResults with nothing set', () => {
|
|
const view = createView();
|
|
const evolved = view.evolve({});
|
|
|
|
// Query and QueryResults are cloned if not set.
|
|
expect(evolved.query).toBeNull();
|
|
expect(evolved.queryResults).toBeNull();
|
|
});
|
|
|
|
it('should merge in context', () => {
|
|
const view = createView();
|
|
const liveContext = { overrides: new Map() };
|
|
const timelineContext = { window: { start: new Date(), end: new Date() } };
|
|
|
|
view.mergeInContext({ live: liveContext });
|
|
expect(view.context?.live).toEqual(liveContext);
|
|
|
|
view.mergeInContext({ timeline: timelineContext });
|
|
expect(view.context?.live).toEqual(liveContext);
|
|
expect(view.context?.timeline).toEqual(timelineContext);
|
|
|
|
// Verify that merging context creates a new context object, as some
|
|
// downstream users may check for context equality.
|
|
const oldContext = view.context;
|
|
view.mergeInContext({ live: liveContext });
|
|
expect(view.context).not.toBe(oldContext);
|
|
});
|
|
|
|
it('should remove context', () => {
|
|
const view = createView({ context: { live: { overrides: new Map() } } });
|
|
|
|
view.removeContext('live');
|
|
expect(view.context).toEqual({});
|
|
});
|
|
|
|
it('should not remove context when no context', () => {
|
|
const view = createView();
|
|
expect(view.context).toBeNull();
|
|
|
|
view.removeContext('live');
|
|
expect(view.context).toBeNull();
|
|
});
|
|
|
|
it('should remove context property', () => {
|
|
const view = createView({ context: { live: { overrides: new Map() } } });
|
|
|
|
view.removeContextProperty('live', 'overrides');
|
|
expect(view.context).toEqual({ live: {} });
|
|
});
|
|
|
|
it('should not remove context property that does not exist', () => {
|
|
const view = createView({ context: {} });
|
|
|
|
view.removeContextProperty('live', 'overrides');
|
|
expect(view.context).toEqual({});
|
|
});
|
|
|
|
describe('should detect folder views', () => {
|
|
it('should detect folder views', () => {
|
|
expect(createView({ view: 'folder' }).isAnyFolderView()).toBeTruthy();
|
|
expect(createView({ view: 'folders' }).isAnyFolderView()).toBeTruthy();
|
|
});
|
|
|
|
it('should not detect folder views', () => {
|
|
expect(createView({ view: 'live' }).isAnyFolderView()).toBeFalsy();
|
|
expect(createView({ view: 'clip' }).isAnyFolderView()).toBeFalsy();
|
|
expect(createView({ view: 'clips' }).isAnyFolderView()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should detect media views', () => {
|
|
it('should detect any media views', () => {
|
|
expect(createView({ view: 'clip' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'snapshot' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'media' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'recording' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'live' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'image' }).isAnyMediaView()).toBeTruthy();
|
|
expect(createView({ view: 'folder' }).isAnyMediaView()).toBeTruthy();
|
|
});
|
|
|
|
it('should not detect any media view', () => {
|
|
expect(createView({ view: 'timeline' }).isAnyMediaView()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should detect gallery views', () => {
|
|
it('should detect gallery views', () => {
|
|
expect(createView({ view: 'clips' }).isGalleryView()).toBeTruthy();
|
|
expect(createView({ view: 'folders' }).isGalleryView()).toBeTruthy();
|
|
expect(createView({ view: 'snapshots' }).isGalleryView()).toBeTruthy();
|
|
expect(createView({ view: 'recordings' }).isGalleryView()).toBeTruthy();
|
|
expect(createView({ view: 'reviews' }).isGalleryView()).toBeTruthy();
|
|
});
|
|
|
|
it('should not detect gallery view', () => {
|
|
expect(createView({ view: 'live' }).isGalleryView()).toBeFalsy();
|
|
expect(createView({ view: 'timeline' }).isGalleryView()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should detect viewer views', () => {
|
|
it('should detect viewer views', () => {
|
|
expect(createView({ view: 'clip' }).isViewerView()).toBeTruthy();
|
|
expect(createView({ view: 'snapshot' }).isViewerView()).toBeTruthy();
|
|
expect(createView({ view: 'media' }).isViewerView()).toBeTruthy();
|
|
expect(createView({ view: 'recording' }).isViewerView()).toBeTruthy();
|
|
expect(createView({ view: 'review' }).isViewerView()).toBeTruthy();
|
|
expect(createView({ view: 'folder' }).isAnyMediaView()).toBeTruthy();
|
|
});
|
|
|
|
it('should not detect viewer views', () => {
|
|
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
|
|
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
|
|
expect(createView({ view: 'timeline' }).isViewerView()).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('should get default media type', () => {
|
|
it('should get default media type', () => {
|
|
expect(createView({ view: 'clip' }).getDefaultMediaType()).toBe('clips');
|
|
expect(createView({ view: 'clips' }).getDefaultMediaType()).toBe('clips');
|
|
expect(createView({ view: 'snapshot' }).getDefaultMediaType()).toBe('snapshots');
|
|
expect(createView({ view: 'snapshots' }).getDefaultMediaType()).toBe('snapshots');
|
|
expect(createView({ view: 'recording' }).getDefaultMediaType()).toBe('recordings');
|
|
expect(createView({ view: 'recordings' }).getDefaultMediaType()).toBe(
|
|
'recordings',
|
|
);
|
|
expect(createView({ view: 'review' }).getDefaultMediaType()).toBe('reviews');
|
|
expect(createView({ view: 'reviews' }).getDefaultMediaType()).toBe('reviews');
|
|
});
|
|
|
|
it('should not get default media type', () => {
|
|
expect(createView({ view: 'live' }).getDefaultMediaType()).toBeNull();
|
|
expect(createView({ view: 'timeline' }).getDefaultMediaType()).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('should determine if display mode is grid', () => {
|
|
expect(createView({ displayMode: 'grid' }).isGrid()).toBeTruthy();
|
|
expect(createView({ displayMode: 'single' }).isGrid()).toBeFalsy();
|
|
expect(createView().isGrid()).toBeFalsy();
|
|
});
|
|
|
|
it('should determine if view supports multiple display modes', () => {
|
|
expect(createView({ view: 'live' }).supportsMultipleDisplayModes()).toBeTruthy();
|
|
expect(createView({ view: 'media' }).supportsMultipleDisplayModes()).toBeTruthy();
|
|
expect(createView({ view: 'clip' }).supportsMultipleDisplayModes()).toBeTruthy();
|
|
expect(createView({ view: 'snapshot' }).supportsMultipleDisplayModes()).toBeTruthy();
|
|
expect(
|
|
createView({ view: 'recording' }).supportsMultipleDisplayModes(),
|
|
).toBeTruthy();
|
|
|
|
expect(createView({ view: 'clips' }).supportsMultipleDisplayModes()).toBeFalsy();
|
|
expect(createView({ view: 'snapshots' }).supportsMultipleDisplayModes()).toBeFalsy();
|
|
expect(
|
|
createView({ view: 'recordings' }).supportsMultipleDisplayModes(),
|
|
).toBeFalsy();
|
|
expect(createView({ view: 'image' }).supportsMultipleDisplayModes()).toBeFalsy();
|
|
expect(createView({ view: 'timeline' }).supportsMultipleDisplayModes()).toBeFalsy();
|
|
});
|
|
});
|