perf: Change media fetches to be asynchronous to view render (#1702)
This is a fairly non-trivial change in terms of consequence, so a greater than average chance something breaks. This is necessary since some cameras (e.g. Reolink) are materially slower to fetch media, and this change substantially improves card responsiveness.
This commit is contained in:
@@ -1,15 +1,7 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { QueryType } from '../../../src/camera-manager/types';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
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,
|
||||
@@ -329,496 +321,36 @@ describe('getViewByParameters', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
describe('should set timeline window', async () => {
|
||||
it('should set timeline to now for live views', 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 unset timeline for non-live views', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const factory = new ViewFactory(createPopulatedAPI(), executor);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
baseView: createView({
|
||||
context: {
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date('2024-07-21T12:22:06.000Z'),
|
||||
end: new Date('2024-07-21T13:22:06.000Z'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
params: {
|
||||
view: 'clip',
|
||||
},
|
||||
});
|
||||
|
||||
expect(view?.context).toEqual({ timeline: {} });
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
describe.each([
|
||||
['single' as const, { mode: 'single' as const }],
|
||||
['grid' as const, { mode: 'grid' as const }],
|
||||
['single' as const, undefined],
|
||||
])('%s', (expectedDisplayMode: ViewDisplayMode, displayConfig?: unknown) => {
|
||||
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: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none' as const,
|
||||
},
|
||||
display: displayConfig,
|
||||
},
|
||||
live: {
|
||||
display: displayConfig,
|
||||
},
|
||||
});
|
||||
|
||||
const factory = new ViewFactory(api);
|
||||
expect(
|
||||
factory.getViewByParameters({
|
||||
params: {
|
||||
view: viewName,
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = await factory.getViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: 'clip',
|
||||
},
|
||||
})?.displayMode,
|
||||
).toBe(expectedDisplayMode);
|
||||
});
|
||||
|
||||
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([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
hasClip: true,
|
||||
},
|
||||
]),
|
||||
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,32 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { applyViewModifiers } from '../../../../src/card-controller/view/modifiers';
|
||||
import { MergeContextViewModifier } from '../../../../src/card-controller/view/modifiers/merge-context';
|
||||
import { SetQueryViewModifier } from '../../../../src/card-controller/view/modifiers/set-query';
|
||||
import { EventMediaQueries } from '../../../../src/view/media-queries';
|
||||
import { MediaQueriesResults } from '../../../../src/view/media-queries-results';
|
||||
import { createView } from '../../../test-utils';
|
||||
|
||||
it('should apply view modifiers', () => {
|
||||
const view = createView();
|
||||
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const context = {
|
||||
timeline: { window: { start: new Date(), end: new Date() } },
|
||||
};
|
||||
|
||||
const modifiers = [
|
||||
new SetQueryViewModifier({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
}),
|
||||
new MergeContextViewModifier(context),
|
||||
];
|
||||
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view.query).toBe(query);
|
||||
expect(view.queryResults).toBe(queryResults);
|
||||
expect(view.context).toEqual(context);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { SetQueryViewModifier } from '../../../../src/card-controller/view/modifiers/set-query';
|
||||
import { createView } from '../../../test-utils';
|
||||
import { EventMediaQueries } from '../../../../src/view/media-queries';
|
||||
import { MediaQueriesResults } from '../../../../src/view/media-queries-results';
|
||||
|
||||
it('should do nothing without arguments', () => {
|
||||
const view = createView();
|
||||
|
||||
const modifier = new SetQueryViewModifier();
|
||||
modifier.modify(view);
|
||||
|
||||
expect(view.query).toBeNull();
|
||||
expect(view.queryResults).toBeNull();
|
||||
});
|
||||
|
||||
it('should set query and results', () => {
|
||||
const view = createView();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const modifier = new SetQueryViewModifier({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
modifier.modify(view);
|
||||
|
||||
expect(view.query).toBe(query);
|
||||
expect(view.queryResults).toBe(queryResults);
|
||||
});
|
||||
@@ -13,6 +13,15 @@ import {
|
||||
createStore,
|
||||
createView,
|
||||
} from '../../test-utils';
|
||||
import { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor';
|
||||
import { EventMediaQueries } from '../../../src/view/media-queries';
|
||||
import { QueryType } from '../../../src/camera-manager/types';
|
||||
import { SetQueryViewModifier } from '../../../src/card-controller/view/modifiers/set-query';
|
||||
import { View } from '../../../src/view/view';
|
||||
import {
|
||||
QueryExecutorOptions,
|
||||
ViewModifier,
|
||||
} from '../../../src/card-controller/view/types';
|
||||
|
||||
describe('should act correctly when view is set', () => {
|
||||
it('basic view', () => {
|
||||
@@ -26,7 +35,7 @@ describe('should act correctly when view is set', () => {
|
||||
factory.getViewDefault.mockReturnValue(view);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
const manager = new ViewManager(api, { viewFactory: factory });
|
||||
|
||||
manager.setViewDefault();
|
||||
|
||||
@@ -53,7 +62,7 @@ describe('should act correctly when view is set', () => {
|
||||
factory.getViewDefault.mockReturnValue(view_1);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
const manager = new ViewManager(api, { viewFactory: factory });
|
||||
|
||||
manager.setViewDefault();
|
||||
|
||||
@@ -82,7 +91,7 @@ it('setViewWithMergedContext', () => {
|
||||
const api = createCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
|
||||
const manager = new ViewManager(api, factory);
|
||||
const manager = new ViewManager(api, { viewFactory: factory });
|
||||
const context: ViewContext = { timeline: {} };
|
||||
|
||||
// Setting context with no existing view does nothing.
|
||||
@@ -104,14 +113,14 @@ it('setViewWithMergedContext', () => {
|
||||
|
||||
it('getEpoch', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
expect(manager.getEpoch()).toBeTruthy();
|
||||
expect(manager.getEpoch().manager).toBe(manager);
|
||||
});
|
||||
|
||||
it('reset', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
|
||||
manager.reset();
|
||||
expect(manager.getView()).toBeNull();
|
||||
@@ -133,7 +142,7 @@ it('setViewDefault', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
@@ -144,7 +153,7 @@ it('setViewByParameters', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParameters.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewByParameters();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
@@ -152,10 +161,16 @@ it('setViewByParameters', () => {
|
||||
});
|
||||
|
||||
it('setViewDefaultWithNewQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefaultWithNewQuery.mockResolvedValue(createView());
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
@@ -163,10 +178,16 @@ it('setViewDefaultWithNewQuery', async () => {
|
||||
});
|
||||
|
||||
it('setViewByParametersWithNewQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParametersWithNewQuery.mockResolvedValue(createView());
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewByParameters.mockReturnValue(createView());
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
await manager.setViewByParametersWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
@@ -174,10 +195,17 @@ it('setViewByParametersWithNewQuery', async () => {
|
||||
});
|
||||
|
||||
it('setViewByParametersWithExistingQuery', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewByParametersWithExistingQuery.mockResolvedValue(createView());
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewByParameters.mockReturnValue(createView());
|
||||
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
await manager.setViewByParametersWithExistingQuery();
|
||||
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
@@ -185,33 +213,57 @@ it('setViewByParametersWithExistingQuery', async () => {
|
||||
});
|
||||
|
||||
describe('should handle exceptions', () => {
|
||||
it('non-async', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
it('should handle exceptions in sync calls', () => {
|
||||
const error = new Error();
|
||||
factory.getViewDefault.mockImplementation(() => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
const manager = new ViewManager(api, { viewFactory: viewFactory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('async', async () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
it('should handle viewFactory exceptions in async calls', async () => {
|
||||
const error = new Error();
|
||||
factory.getViewByParametersWithNewQuery.mockRejectedValue(error);
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, factory);
|
||||
await manager.setViewByParametersWithNewQuery();
|
||||
const manager = new ViewManager(api, { viewFactory: viewFactory });
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
||||
});
|
||||
|
||||
it('should handle viewQueryExecutor exceptions in async calls', async () => {
|
||||
const error = new Error();
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValue(createView());
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockRejectedValue(error);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
// The initial view will have been set.
|
||||
expect(manager.hasView()).toBeTruthy();
|
||||
|
||||
// But an error will also be generated.
|
||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isViewSupportedByCamera', () => {
|
||||
@@ -265,7 +317,7 @@ describe('hasMajorMediaChange', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasMajorMediaChange(createView({ view: 'clips' }))).toBeTruthy();
|
||||
@@ -275,7 +327,7 @@ describe('hasMajorMediaChange', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ camera: 'camera-1' }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(manager.hasMajorMediaChange(createView({ camera: 'camera-2' }))).toBeTruthy();
|
||||
@@ -293,7 +345,7 @@ describe('hasMajorMediaChange', () => {
|
||||
createView({ context: { live: { overrides: overrides_1 } } }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
@@ -315,7 +367,7 @@ describe('hasMajorMediaChange', () => {
|
||||
createView({ view: 'clips', context: { live: { overrides: overrides_1 } } }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
@@ -335,7 +387,7 @@ describe('hasMajorMediaChange', () => {
|
||||
createView({ view: 'media', queryResults: queryResults_1 }),
|
||||
);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
@@ -353,42 +405,317 @@ describe('hasMajorMediaChange', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
factory.getViewDefault.mockReturnValue(createView({ queryResults: queryResults_1 }));
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), factory);
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
manager.setViewDefault();
|
||||
|
||||
expect(
|
||||
manager.hasMajorMediaChange(createView({ queryResults: queryResults_2 })),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should initialize', () => {
|
||||
it('without querystring', async () => {
|
||||
const api = createCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(api, factory);
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
});
|
||||
factory.getViewDefaultWithNewQuery.mockResolvedValue(view);
|
||||
|
||||
expect(await manager.initialize()).toBeTruthy();
|
||||
|
||||
expect(manager.getView()).toBe(view);
|
||||
describe('should initialize', () => {
|
||||
it('without querystring', async () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
});
|
||||
|
||||
it('with querystring', async () => {
|
||||
const api = createCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(api, factory);
|
||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue(
|
||||
true,
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValue(view);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ViewManager(api, {
|
||||
viewFactory: viewFactory,
|
||||
});
|
||||
|
||||
expect(await manager.initialize()).toBeTruthy();
|
||||
|
||||
expect(manager.getView()).toBe(view);
|
||||
});
|
||||
|
||||
it('with querystring', async () => {
|
||||
const api = createCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(api, { viewFactory: factory });
|
||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActionsToRun).mockReturnValue(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(await manager.initialize()).toBeTruthy();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should adopt query and results when changing to gallery from viewer', async () => {
|
||||
const baseView = createView({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
query: new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
hasClip: true,
|
||||
},
|
||||
]),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
});
|
||||
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault
|
||||
.mockReturnValueOnce(baseView)
|
||||
.mockReturnValueOnce(createView({ view: 'clips' }));
|
||||
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
manager.setViewDefault();
|
||||
expect(manager.getView()?.is('media')).toBeTruthy();
|
||||
|
||||
await manager.setViewDefaultWithNewQuery({
|
||||
params: {
|
||||
view: 'clips',
|
||||
},
|
||||
});
|
||||
|
||||
expect(manager.getView()?.is('clips')).toBeTruthy();
|
||||
expect(manager.getView()?.query).toBe(baseView.query);
|
||||
expect(manager.getView()?.queryResults).toBe(baseView.queryResults);
|
||||
expect(viewQueryExecutor.getNewQueryModifiers).not.toHaveBeenCalled();
|
||||
expect(viewQueryExecutor.getExistingQueryModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('should apply async view modifications', () => {
|
||||
it('should apply modifications successfully', async () => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValue(createView({ view: 'live' }));
|
||||
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([
|
||||
new SetQueryViewModifier({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
}),
|
||||
]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.query).toBe(query);
|
||||
expect(manager.getView()?.queryResults).toBe(queryResults);
|
||||
expect(manager.getView()?.context?.loading?.query).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not apply modifications if there is a major media change', async () => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
||||
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
viewQueryExecutor.getNewQueryModifiers.mockImplementation(
|
||||
async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_view: View,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_executorqueryExecutorOptions?: QueryExecutorOptions,
|
||||
): Promise<ViewModifier[] | null> => {
|
||||
// Simulate a major media change while the async operation is running.
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(
|
||||
createView({ view: 'clips', context: { loading: { query: 1 } } }),
|
||||
);
|
||||
manager.setViewDefault();
|
||||
|
||||
// Now return the modifiers (which should be ignored since there has
|
||||
// been a major change in the meantime).
|
||||
return [
|
||||
new SetQueryViewModifier({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
}),
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
// View set during the async operation should not be touched.
|
||||
expect(manager.getView()?.is('clips')).toBeTruthy();
|
||||
expect(manager.getView()?.query).toBeNull();
|
||||
expect(manager.getView()?.queryResults).toBeNull();
|
||||
expect(manager.getView()?.context?.loading?.query).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('should manage loading state correctly', () => {
|
||||
it('should mark as not loading when with major media change', async () => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
viewQueryExecutor.getNewQueryModifiers.mockImplementation(
|
||||
async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_view: View,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_executorqueryExecutorOptions?: QueryExecutorOptions,
|
||||
): Promise<ViewModifier[] | null> => {
|
||||
expect(manager.getView()?.context?.loading?.query).not.toBeUndefined();
|
||||
|
||||
// Simulate a major media change while the async operation is running.
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(
|
||||
createView({ view: 'clips', context: { loading: { query: 1 } } }),
|
||||
);
|
||||
manager.setViewDefault();
|
||||
|
||||
// Now return the modifiers (which should be ignored since there has
|
||||
// been a major change in the meantime).
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
expect(await manager.initialize()).toBeTruthy();
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.hasView()).toBeFalsy();
|
||||
expect(manager.getView()?.context?.loading?.query).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not change loading status if something else is being loaded', async () => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
viewQueryExecutor.getNewQueryModifiers.mockImplementation(
|
||||
async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_view: View,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_executorqueryExecutorOptions?: QueryExecutorOptions,
|
||||
): Promise<ViewModifier[] | null> => {
|
||||
expect(manager.getView()?.context?.loading?.query).not.toBeUndefined();
|
||||
|
||||
// Simulate a major media change while the async operation is running.
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(
|
||||
createView({ view: 'clips', context: { loading: { query: 2 } } }),
|
||||
);
|
||||
manager.setViewDefault();
|
||||
|
||||
// Now return the modifiers (which should be ignored since there has
|
||||
// been a major change in the meantime).
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.context?.loading?.query).toBe(2);
|
||||
});
|
||||
|
||||
it('should not change loading status if it is unexpected', async () => {
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(createView({ view: 'live' }));
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
viewQueryExecutor.getNewQueryModifiers.mockImplementation(
|
||||
async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_view: View,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_executorqueryExecutorOptions?: QueryExecutorOptions,
|
||||
): Promise<ViewModifier[] | null> => {
|
||||
expect(manager.getView()?.context?.loading?.query).not.toBeUndefined();
|
||||
|
||||
// Simulate another view update (without a major media change)
|
||||
viewFactory.getViewDefault.mockReturnValueOnce(
|
||||
createView({ context: { loading: { query: 100 } } }),
|
||||
);
|
||||
manager.setViewDefault();
|
||||
|
||||
// Now return the modifiers (which should be ignored since there has
|
||||
// been a major change in the meantime).
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
await manager.setViewDefaultWithNewQuery();
|
||||
|
||||
expect(manager.getView()?.context?.loading?.query).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should adopt query and results when changing to gallery from viewer', async () => {
|
||||
const baseView = createView({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
query: new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: new Set(['camera.office']),
|
||||
hasClip: true,
|
||||
},
|
||||
]),
|
||||
queryResults: new MediaQueriesResults(),
|
||||
});
|
||||
|
||||
const viewFactory = mock<ViewFactory>();
|
||||
viewFactory.getViewDefault
|
||||
.mockReturnValueOnce(baseView)
|
||||
.mockReturnValueOnce(createView({ view: 'clips' }));
|
||||
|
||||
const viewQueryExecutor = mock<ViewQueryExecutor>();
|
||||
viewQueryExecutor.getNewQueryModifiers.mockResolvedValue([]);
|
||||
|
||||
const manager = new ViewManager(createCardAPI(), {
|
||||
viewFactory: viewFactory,
|
||||
viewQueryExecutor: viewQueryExecutor,
|
||||
});
|
||||
|
||||
manager.setViewDefault();
|
||||
expect(manager.getView()?.is('media')).toBeTruthy();
|
||||
|
||||
await manager.setViewDefaultWithNewQuery({
|
||||
params: {
|
||||
view: 'clips',
|
||||
},
|
||||
});
|
||||
|
||||
expect(manager.getView()?.is('clips')).toBeTruthy();
|
||||
expect(manager.getView()?.query).toBe(baseView.query);
|
||||
expect(manager.getView()?.queryResults).toBe(baseView.queryResults);
|
||||
expect(viewQueryExecutor.getNewQueryModifiers).not.toHaveBeenCalled();
|
||||
expect(viewQueryExecutor.getExistingQueryModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { applyViewModifiers } from '../../../src/card-controller/view/modifiers';
|
||||
import { QueryExecutor } from '../../../src/card-controller/view/query-executor';
|
||||
import { ViewQueryExecutor } from '../../../src/card-controller/view/view-query-executor';
|
||||
import { FrigateCardView } 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 { createCardAPI, createView } from '../../test-utils';
|
||||
import { createPopulatedAPI } from './test-utils';
|
||||
|
||||
describe('ViewQueryExecutor', () => {
|
||||
describe('getExistingQueryModifiers', () => {
|
||||
it('should return modifier with result when query present', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(createCardAPI(), executor);
|
||||
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
const view = createView({
|
||||
query: query,
|
||||
});
|
||||
|
||||
executor.execute.mockResolvedValue(queryResults);
|
||||
|
||||
const queryExecutorOptions = {};
|
||||
const modifiers = await viewQueryExecutor.getExistingQueryModifiers(
|
||||
view,
|
||||
queryExecutorOptions,
|
||||
);
|
||||
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view.query).toBe(query);
|
||||
expect(view.queryResults).toBe(queryResults);
|
||||
expect(executor.execute).toBeCalledWith(query, queryExecutorOptions);
|
||||
});
|
||||
|
||||
it('should not return modifier when query absent', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(createCardAPI(), executor);
|
||||
|
||||
const view = createView();
|
||||
|
||||
const queryExecutorOptions = {};
|
||||
const modifiers = await viewQueryExecutor.getExistingQueryModifiers(
|
||||
view,
|
||||
queryExecutorOptions,
|
||||
);
|
||||
|
||||
expect(modifiers?.length).toBe(0);
|
||||
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view.query).toBeNull();
|
||||
expect(view.queryResults).toBeNull();
|
||||
expect(executor.execute).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewQueryModifiers', () => {
|
||||
it('should return null without config', async () => {
|
||||
const factory = new ViewQueryExecutor(createCardAPI());
|
||||
expect(await factory.getNewQueryModifiers(createView())).toBeNull();
|
||||
});
|
||||
|
||||
describe('with a live view', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-07-21T13:22:06Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should set query and queryResults for events', async () => {
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const executor = mock<QueryExecutor>();
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
|
||||
const view = createView({ view: 'live', camera: 'camera.office' });
|
||||
const queryExecutorOptions = {};
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(
|
||||
view,
|
||||
queryExecutorOptions,
|
||||
);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
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 set query and queryResults for recordings', async () => {
|
||||
const query = new RecordingMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
const executor = mock<QueryExecutor>();
|
||||
executor.executeDefaultRecordingQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
media_type: 'recordings',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = createView({ view: 'live', camera: 'camera.office' });
|
||||
const queryExecutorOptions = {};
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(
|
||||
view,
|
||||
queryExecutorOptions,
|
||||
);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultRecordingQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
executorOptions: {
|
||||
useCache: false,
|
||||
},
|
||||
});
|
||||
expect(executor.executeDefaultEventQuery).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('should set timeline window', async () => {
|
||||
it('should set timeline to now for live views', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI(),
|
||||
executor,
|
||||
);
|
||||
const view = createView({ view: 'live', camera: 'camera.office' });
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
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 unset timeline for non-live views', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI(),
|
||||
executor,
|
||||
);
|
||||
const view = createView({
|
||||
view: 'clips',
|
||||
camera: 'camera.office',
|
||||
context: {
|
||||
timeline: {
|
||||
window: {
|
||||
start: new Date('2024-07-21T12:22:06.000Z'),
|
||||
end: new Date('2024-07-21T13:22:06.000Z'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.context).toEqual({ timeline: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch anything if configured for no thumbnails', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI({
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a media view', () => {
|
||||
it('should set query and queryResults for events', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
const query = new EventMediaQueries();
|
||||
const queryResults = new MediaQueriesResults();
|
||||
|
||||
executor.executeDefaultEventQuery.mockResolvedValue({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
const viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
|
||||
const view = new View({
|
||||
view: 'media',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.query).toBe(query);
|
||||
expect(view?.queryResults).toBe(queryResults);
|
||||
expect(executor.executeDefaultEventQuery).toBeCalledWith({
|
||||
cameraID: 'camera.office',
|
||||
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 viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI(),
|
||||
executor,
|
||||
);
|
||||
const view = new View({
|
||||
view: viewName,
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
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 a 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 viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI(),
|
||||
executor,
|
||||
);
|
||||
const view = new View({
|
||||
view: viewName,
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
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('should not fetch anything if configured for no thumbnails', async () => {
|
||||
const executor = mock<QueryExecutor>();
|
||||
|
||||
const viewQueryExecutor = new ViewQueryExecutor(
|
||||
createPopulatedAPI({
|
||||
media_viewer: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
executor,
|
||||
);
|
||||
const view = new View({
|
||||
view: 'clip',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.query).toBeNull();
|
||||
expect(view?.queryResults).toBeNull();
|
||||
expect(executor.executeDefaultEventQuery).not.toHaveBeenCalled();
|
||||
expect(executor.executeDefaultRecordingQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setting or removing seek time', () => {
|
||||
it('should set seek time when results are selected based on time', async () => {
|
||||
const now = new Date();
|
||||
const executor = mock<QueryExecutor>();
|
||||
const viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
|
||||
|
||||
const view = new View({
|
||||
view: 'clip',
|
||||
camera: 'camera.office',
|
||||
});
|
||||
|
||||
const queryExecutorOptions = {
|
||||
selectResult: {
|
||||
time: {
|
||||
time: now,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(
|
||||
view,
|
||||
queryExecutorOptions,
|
||||
);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
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 viewQueryExecutor = new ViewQueryExecutor(createPopulatedAPI(), executor);
|
||||
|
||||
const view = new View({
|
||||
view: 'clip',
|
||||
camera: 'camera.office',
|
||||
context: {
|
||||
mediaViewer: {
|
||||
seek: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
|
||||
applyViewModifiers(view, modifiers);
|
||||
|
||||
expect(view?.context?.mediaViewer?.seek).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user