Files
advanced-camera-card/tests/view/view.test.ts
T
Dermot Duffy e830db1a77 Complete carousel refactor.
Reduces one layer of DOM nesting for simplication, uses the latest Embla
version, unittests for everything.
2023-09-04 16:25:32 -07:00

548 lines
18 KiB
TypeScript

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 { 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();
const queryResults = new MediaQueriesResults();
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);
});
it('should clone', () => {
const query = new EventMediaQueries();
const queryResults = new MediaQueriesResults();
const context = {};
const view = createView({
view: 'live',
camera: 'camera',
query: query,
queryResults: queryResults,
context: context,
});
const clone = view.clone();
expect(clone).toEqual(view);
});
it('should evolve with everything set', () => {
const view = createView({
view: 'live',
camera: 'camera-1',
query: new EventMediaQueries(),
queryResults: new MediaQueriesResults(),
context: {},
displayMode: 'single',
});
const evolved = view.evolve({
view: 'clips',
camera: 'camera-2',
query: new EventMediaQueries(),
queryResults: new MediaQueriesResults(),
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 EventMediaQueries(),
queryResults: new MediaQueriesResults(),
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);
});
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({});
});
it('should detect gallery views', () => {
expect(createView({ view: 'clips' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isGalleryView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isGalleryView()).toBeTruthy();
});
it('should not detect gallery view', () => {
expect(createView({ view: 'live' }).isGalleryView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isGalleryView()).toBeFalsy();
});
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();
});
it('should not detect any media view', () => {
expect(createView({ view: 'timeline' }).isAnyMediaView()).toBeFalsy();
});
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();
});
it('should not detect viewer views', () => {
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isViewerView()).toBeFalsy();
});
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');
});
it('should not get default media type', () => {
expect(createView({ view: 'live' }).getDefaultMediaType()).toBeNull();
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();
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();
});
});
// @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();
});
});