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.
357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
|
import { ViewModifier } from '../../../src/card-controller/view/types';
|
|
import { FrigateCardView, ViewDisplayMode } from '../../../src/config/types';
|
|
import { View } from '../../../src/view/view';
|
|
import {
|
|
createCameraManager,
|
|
createCapabilities,
|
|
createCardAPI,
|
|
createConfig,
|
|
createStore,
|
|
createView,
|
|
} from '../../test-utils';
|
|
import { createPopulatedAPI } from './test-utils';
|
|
|
|
describe('getViewDefault', () => {
|
|
it('should return null without config', () => {
|
|
const factory = new ViewFactory(createCardAPI());
|
|
expect(factory.getViewDefault()).toBeNull();
|
|
});
|
|
|
|
it('should return null if no cameras support view', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
|
|
// No cameras support live.
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.office',
|
|
capabilities: createCapabilities({
|
|
live: false,
|
|
}),
|
|
},
|
|
{
|
|
cameraID: 'camera.kitchen',
|
|
capabilities: createCapabilities({
|
|
live: false,
|
|
}),
|
|
},
|
|
]),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
expect(factory.getViewDefault()).toBeNull();
|
|
});
|
|
|
|
it('should create view', () => {
|
|
const factory = new ViewFactory(createPopulatedAPI());
|
|
const view = factory.getViewDefault();
|
|
|
|
expect(view?.is('live')).toBeTruthy();
|
|
expect(view?.camera).toBe('camera.office');
|
|
});
|
|
|
|
it('should cycle camera when configured', () => {
|
|
const api = createPopulatedAPI();
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
view: {
|
|
default_cycle_camera: true,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
|
|
let view = factory.getViewDefault();
|
|
expect(view?.camera).toBe('camera.office');
|
|
|
|
view = factory.getViewDefault({ baseView: view });
|
|
expect(view?.camera).toBe('camera.kitchen');
|
|
|
|
view = factory.getViewDefault({ baseView: view });
|
|
expect(view?.camera).toBe('camera.office');
|
|
|
|
// When a parameter is specified, it will not cycle.
|
|
view = factory.getViewDefault({
|
|
params: { camera: 'camera.office' },
|
|
baseView: view,
|
|
});
|
|
expect(view?.camera).toBe('camera.office');
|
|
});
|
|
|
|
it('should use default camera when camera unspecified', () => {
|
|
// Even though baseView has a camera, since default is called it should
|
|
// use that camera.
|
|
|
|
const factory = new ViewFactory(createPopulatedAPI());
|
|
const baseView = createView({ camera: 'camera.kitchen' });
|
|
const view = factory.getViewDefault({
|
|
baseView: baseView,
|
|
});
|
|
|
|
expect(view?.is('live')).toBeTruthy();
|
|
expect(view?.camera).toBe('camera.office');
|
|
});
|
|
|
|
it('should respect parameters', () => {
|
|
const factory = new ViewFactory(createPopulatedAPI());
|
|
const view = factory.getViewDefault({
|
|
params: {
|
|
camera: 'camera.kitchen',
|
|
},
|
|
});
|
|
|
|
expect(view?.is('live')).toBeTruthy();
|
|
expect(view?.camera).toBe('camera.kitchen');
|
|
});
|
|
});
|
|
|
|
describe('getViewByParameters', () => {
|
|
it('should get view by parameters specifying camera and view', () => {
|
|
const api = createPopulatedAPI();
|
|
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
|
createCapabilities({
|
|
clips: true,
|
|
}),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
const view = factory.getViewByParameters({
|
|
params: {
|
|
camera: 'camera.kitchen',
|
|
view: 'clips',
|
|
},
|
|
});
|
|
|
|
expect(view?.is('clips')).toBeTruthy();
|
|
expect(view?.camera).toBe('camera.kitchen');
|
|
});
|
|
|
|
it('should get view by parameters using base view if unspecified', () => {
|
|
const api = createPopulatedAPI();
|
|
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
|
createCapabilities({
|
|
clips: true,
|
|
}),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
const baseView = new View({
|
|
camera: 'camera.kitchen',
|
|
view: 'clips',
|
|
});
|
|
|
|
const view = factory.getViewByParameters({
|
|
baseView: baseView,
|
|
params: {
|
|
camera: 'camera.office',
|
|
},
|
|
});
|
|
|
|
expect(view?.view).toBe('clips');
|
|
expect(view?.camera).toBe('camera.office');
|
|
});
|
|
|
|
it('should set view by parameters using config as fallback', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.kitchen',
|
|
capabilities: createCapabilities({ live: true }),
|
|
},
|
|
]),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
|
|
const factory = new ViewFactory(api);
|
|
const view = factory.getViewByParameters({
|
|
params: {
|
|
camera: 'camera.kitchen',
|
|
|
|
// No prior view, and no specified view. This could happen during query
|
|
// string based initialization.
|
|
},
|
|
});
|
|
|
|
expect(view?.is('live')).toBeTruthy();
|
|
expect(view?.camera).toBe('camera.kitchen');
|
|
});
|
|
|
|
it('should not set view by parameters without config', () => {
|
|
const factory = new ViewFactory(createCardAPI());
|
|
|
|
const view = factory.getViewByParameters({
|
|
params: {
|
|
view: 'live',
|
|
},
|
|
});
|
|
|
|
expect(view).toBeNull();
|
|
});
|
|
|
|
it('should throw without camera and without failsafe', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.kitchen',
|
|
capabilities: createCapabilities({ snapshots: false }),
|
|
},
|
|
{
|
|
cameraID: 'camera.office',
|
|
// No capabilities.
|
|
capabilities: null,
|
|
},
|
|
]),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
|
|
const factory = new ViewFactory(api);
|
|
expect(() =>
|
|
factory.getViewByParameters({
|
|
// Since no camera is specified, and no camera supports the capabilities
|
|
// necessary for this view, the view will be null.
|
|
params: {
|
|
view: 'snapshots',
|
|
},
|
|
}),
|
|
).toThrowError(/No cameras support this view/);
|
|
});
|
|
|
|
describe('should handle unsupported view', () => {
|
|
it('should throw without failsafe', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.kitchen',
|
|
capabilities: createCapabilities({ snapshots: false }),
|
|
},
|
|
]),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
|
createCapabilities({
|
|
snapshots: false,
|
|
}),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
expect(() =>
|
|
factory.getViewByParameters({
|
|
params: {
|
|
camera: 'camera.kitchen',
|
|
view: 'snapshots',
|
|
},
|
|
}),
|
|
).toThrowError(/The selected camera does not support this view/);
|
|
});
|
|
|
|
it('should choose live view with failsafe', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.kitchen',
|
|
capabilities: createCapabilities({ live: true, snapshots: false }),
|
|
},
|
|
]),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
|
|
createCapabilities({
|
|
live: true,
|
|
snapshots: false,
|
|
}),
|
|
);
|
|
|
|
const factory = new ViewFactory(api);
|
|
const view = factory.getViewByParameters({
|
|
params: {
|
|
camera: 'camera.kitchen',
|
|
view: 'snapshots',
|
|
},
|
|
failSafe: true,
|
|
});
|
|
expect(view?.is('live')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('should call modifiers', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([
|
|
{
|
|
cameraID: 'camera.office',
|
|
capabilities: createCapabilities({ live: true }),
|
|
},
|
|
]),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
|
|
const modifyCallback = vi.fn();
|
|
class TestViewModifier implements ViewModifier {
|
|
modify = modifyCallback;
|
|
}
|
|
|
|
const view = new View({
|
|
view: 'live',
|
|
camera: 'camera.office',
|
|
});
|
|
const factory = new ViewFactory(api);
|
|
const modifiedView = factory.getViewByParameters({
|
|
baseView: view,
|
|
modifiers: [new TestViewModifier()],
|
|
});
|
|
|
|
expect(modifiedView?.is('live')).toBeTruthy();
|
|
expect(modifiedView?.camera).toBe('camera.office');
|
|
expect(view).not.toBe(modifiedView);
|
|
expect(modifyCallback).toHaveBeenCalledWith(modifiedView);
|
|
});
|
|
|
|
describe('should get correct default display mode', () => {
|
|
describe.each([
|
|
['single' as const, { 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: {
|
|
display: displayConfig,
|
|
},
|
|
live: {
|
|
display: displayConfig,
|
|
},
|
|
});
|
|
|
|
const factory = new ViewFactory(api);
|
|
expect(
|
|
factory.getViewByParameters({
|
|
params: {
|
|
view: viewName,
|
|
},
|
|
})?.displayMode,
|
|
).toBe(expectedDisplayMode);
|
|
});
|
|
});
|
|
});
|
|
});
|