Add out-of-the-box Frigate PTZ support.

This commit is contained in:
Dermot Duffy committed 2023-10-29 14:10:41 -07:00
1 parent fa43d2e439
commit e12912fa85
74 files changed
+3981 -1514

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { Camera } from '../../src/camera-manager/camera.js';
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
import { createCameraCapabilities, createCameraConfig } from '../test-utils.js';
describe('Camera', () => {
it('should get config', async () => {
const config = createCameraConfig();
const camera = new Camera(
config,
new GenericCameraManagerEngine(),
createCameraCapabilities(),
);
expect(camera.getConfig()).toBe(config);
});
it('should get capabilities', async () => {
const capabilities = createCameraCapabilities();
const camera = new Camera(
createCameraConfig(),
new GenericCameraManagerEngine(),
capabilities,
);
expect(camera.getCapabilities()).toBe(capabilities);
});
it('should get engine', async () => {
const engine = new GenericCameraManagerEngine();
const camera = new Camera(createCameraConfig(), engine, createCameraCapabilities());
expect(camera.getEngine()).toBe(engine);
});
it('should set and get id', async () => {
const config = createCameraConfig();
const camera = new Camera(
config,
new GenericCameraManagerEngine(),
createCameraCapabilities(),
);
camera.setID('foo');
expect(camera.getID()).toBe('foo');
expect(camera.getConfig().id).toBe('foo');
});
it('should throw without id', async () => {
const config = createCameraConfig();
const camera = new Camera(
config,
new GenericCameraManagerEngine(),
createCameraCapabilities(),
);
expect(() => camera.getID()).toThrowError(
'Could not determine camera id for the following ' +
"camera, may need to set 'id' parameter manually",
);
});
});
@@ -9,6 +9,7 @@ import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/t
import {
CameraConfig,
FrigateCardView,
PTZAction,
RawFrigateCardConfig,
} from '../../../src/config/types';
import { ViewMedia } from '../../../src/view/media';
@@ -392,3 +393,101 @@ describe('getCameraEndpoints', () => {
});
});
});
describe('executePTZAction', () => {
describe('preset', () => {
it('should reject without preset argument', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, 'preset');
expect(hass.callService).not.toBeCalled();
});
it('should succeed', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, 'preset', {
preset: 'preset-foo',
});
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
entity_id: 'camera.office',
action: 'preset',
argument: 'preset-foo',
});
});
});
describe('zoom', () => {
describe.each([['zoom_in' as const], ['zoom_out' as const]])(
'%s',
(actionName: PTZAction) => {
it('start', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, actionName);
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
entity_id: 'camera.office',
action: 'zoom',
argument: actionName === 'zoom_in' ? 'in' : 'out',
});
});
it('stop', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, actionName, {
phase: 'stop',
});
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
entity_id: 'camera.office',
action: 'stop',
});
});
},
);
});
describe('move', () => {
describe.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
])('%s', (actionName: PTZAction) => {
it('start', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, actionName);
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
entity_id: 'camera.office',
action: 'move',
argument: actionName,
});
});
it('stop', () => {
const hass = createHASS();
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
createEngine().executePTZAction(hass, cameraConfig, actionName, {
phase: 'stop',
});
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
entity_id: 'camera.office',
action: 'stop',
});
});
});
});
});
@@ -0,0 +1,218 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
getEvents,
getEventSummary,
getPTZInfo,
getRecordingSegments,
getRecordingsSummary,
retainEvent,
} from '../../../src/camera-manager/frigate/requests';
import {
EventSummary,
eventSummarySchema,
FrigateEvent,
frigateEventsSchema,
ptzInfoSchema,
recordingSegmentsSchema,
recordingSummarySchema,
retainResultSchema,
} from '../../../src/camera-manager/frigate/types';
import { RecordingSegment } from '../../../src/camera-manager/types';
import { homeAssistantWSRequest } from '../../../src/utils/ha';
import { createFrigateEvent, createHASS } from '../../test-utils';
vi.mock('../../../src/utils/ha');
describe('frigate requests', () => {
afterEach(() => {
vi.resetAllMocks();
});
it('should get recordings summary', async () => {
const recordingSummary = {
events: 0,
hours: [],
day: new Date(),
};
const hass = createHASS();
vi.mocked(homeAssistantWSRequest).mockResolvedValue(recordingSummary);
expect(await getRecordingsSummary(hass, 'clientID', 'camera.office')).toBe(
recordingSummary,
);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
recordingSummarySchema,
expect.objectContaining({
type: 'frigate/recordings/summary',
instance_id: 'clientID',
camera: 'camera.office',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}),
true,
);
});
it('should get recordings segments', async () => {
const recordingSegments: RecordingSegment[] = [
{
start_time: 0,
end_time: 1,
id: 'foo',
},
];
const hass = createHASS();
vi.mocked(homeAssistantWSRequest).mockResolvedValue(recordingSegments);
expect(
await getRecordingSegments(hass, {
instance_id: 'clientID',
camera: 'camera.office',
after: 1,
before: 0,
}),
).toBe(recordingSegments);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
recordingSegmentsSchema,
expect.objectContaining({
type: 'frigate/recordings/get',
instance_id: 'clientID',
camera: 'camera.office',
after: 1,
before: 0,
}),
true,
);
});
describe('should retain event', async () => {
it('successfully', async () => {
vi.mocked(homeAssistantWSRequest).mockResolvedValue({
success: true,
message: 'success',
});
const hass = createHASS();
retainEvent(hass, 'clientID', 'eventID', true);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
retainResultSchema,
expect.objectContaining({
type: 'frigate/event/retain',
instance_id: 'clientID',
event_id: 'eventID',
retain: true,
}),
true,
);
});
it('unsuccessfully', async () => {
vi.mocked(homeAssistantWSRequest).mockResolvedValue({
success: false,
message: 'failed',
});
const hass = createHASS();
await expect(retainEvent(hass, 'clientID', 'eventID', true)).rejects.toThrowError(
/Could not retain event/,
);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
retainResultSchema,
expect.objectContaining({
type: 'frigate/event/retain',
instance_id: 'clientID',
event_id: 'eventID',
retain: true,
}),
true,
);
});
});
it('should get events', async () => {
const events: FrigateEvent[] = [createFrigateEvent()];
const hass = createHASS();
vi.mocked(homeAssistantWSRequest).mockResolvedValue(events);
expect(
await getEvents(hass, {
instance_id: 'clientID',
cameras: ['camera.office'],
labels: ['person'],
zones: ['zone'],
after: 0,
before: 1,
limit: 10,
has_clip: true,
has_snapshot: true,
favorites: true,
}),
).toBe(events);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
frigateEventsSchema,
expect.objectContaining({
type: 'frigate/events/get',
instance_id: 'clientID',
cameras: ['camera.office'],
labels: ['person'],
zones: ['zone'],
after: 0,
before: 1,
limit: 10,
has_clip: true,
has_snapshot: true,
favorites: true,
}),
true,
);
});
it('should get event summary', async () => {
const eventSummary: EventSummary = [
{
camera: 'camera.office',
day: '2023-10-29',
label: 'person',
sub_label: null,
zones: ['door'],
},
];
const hass = createHASS();
vi.mocked(homeAssistantWSRequest).mockResolvedValue(eventSummary);
expect(await getEventSummary(hass, 'clientID')).toBe(eventSummary);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
eventSummarySchema,
expect.objectContaining({
type: 'frigate/events/summary',
instance_id: 'clientID',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}),
true,
);
});
it('should get PTZ info', async () => {
const ptzInfo = [
{
name: 'camera.office',
features: ['zoom', 'zoom-r'],
presets: ['preset01', 'preset02'],
},
];
const hass = createHASS();
vi.mocked(homeAssistantWSRequest).mockResolvedValue(ptzInfo);
expect(await getPTZInfo(hass, 'clientID', 'camera.office')).toBe(ptzInfo);
expect(homeAssistantWSRequest).toBeCalledWith(
hass,
ptzInfoSchema,
expect.objectContaining({
type: 'frigate/ptz/info',
instance_id: 'clientID',
camera: 'camera.office',
}),
true,
);
});
});
@@ -9,6 +9,7 @@ import {
createCameraConfig,
createHASS,
createStateEntity,
createStore,
} from '../../test-utils';
const createEngine = (): GenericCameraManagerEngine => {
@@ -26,19 +27,29 @@ describe('GenericCameraManagerEngine', () => {
it('should initialize camera', async () => {
const config = createGenericCameraConfig();
expect(
await createEngine().initializeCamera(
createHASS(),
mock<EntityRegistryManager>(),
config,
),
).toEqual(config);
const camera = await createEngine().initializeCamera(
createHASS(),
mock<EntityRegistryManager>(),
config,
);
expect(camera.getConfig()).toEqual(config);
expect(camera.getCapabilities()).toEqual({
canFavoriteEvents: false,
canFavoriteRecordings: false,
canSeek: false,
supportsClips: false,
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
});
});
it('should generate default event query', () => {
const engine = createEngine();
expect(
createEngine().generateDefaultEventQuery(
new Map([['camera-1', createGenericCameraConfig()]]),
engine.generateDefaultEventQuery(
createStore([{ cameraID: 'camera-1', engine: engine }]),
new Set(['camera-1']),
{},
),
@@ -46,9 +57,10 @@ describe('GenericCameraManagerEngine', () => {
});
it('should generate default recording query', () => {
const engine = createEngine();
expect(
createEngine().generateDefaultRecordingQuery(
new Map([['camera-1', createGenericCameraConfig()]]),
engine.generateDefaultRecordingQuery(
createStore([{ cameraID: 'camera-1', engine: engine }]),
new Set(['camera-1']),
{},
),
@@ -56,9 +68,10 @@ describe('GenericCameraManagerEngine', () => {
});
it('should generate default recording segments query', () => {
const engine = createEngine();
expect(
createEngine().generateDefaultRecordingSegmentsQuery(
new Map([['camera-1', createGenericCameraConfig()]]),
engine.generateDefaultRecordingSegmentsQuery(
createStore([{ cameraID: 'camera-1', engine: engine }]),
new Set(['camera-1']),
{},
),
@@ -66,30 +79,33 @@ describe('GenericCameraManagerEngine', () => {
});
it('should get events', async () => {
const engine = createEngine();
expect(
await createEngine().getEvents(
await engine.getEvents(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{ type: QueryType.Event, cameraIDs: new Set(['camera-1']) },
),
).toBeNull();
});
it('should get recordings', async () => {
const engine = createEngine();
expect(
await createEngine().getRecordings(
await engine.getRecordings(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{ type: QueryType.Recording, cameraIDs: new Set(['camera-1']) },
),
).toBeNull();
});
it('should get recording segments', async () => {
const engine = createEngine();
expect(
await createEngine().getRecordingSegments(
await engine.getRecordingSegments(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
type: QueryType.RecordingSegments,
cameraIDs: new Set(['camera-1']),
@@ -101,10 +117,11 @@ describe('GenericCameraManagerEngine', () => {
});
it('should generate media from events', async () => {
const engine = createEngine();
expect(
createEngine().generateMediaFromEvents(
engine.generateMediaFromEvents(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
type: QueryType.Event,
cameraIDs: new Set(['camera-1']),
@@ -118,10 +135,11 @@ describe('GenericCameraManagerEngine', () => {
});
it('should generate media from recordings', async () => {
const engine = createEngine();
expect(
createEngine().generateMediaFromRecordings(
engine.generateMediaFromRecordings(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{
type: QueryType.Recording,
cameraIDs: new Set(['camera-1']),
@@ -167,10 +185,11 @@ describe('GenericCameraManagerEngine', () => {
});
it('should get media seek time', async () => {
const engine = createEngine();
expect(
await createEngine().getMediaSeekTime(
await engine.getMediaSeekTime(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
new TestViewMedia(),
new Date(),
),
@@ -178,10 +197,11 @@ describe('GenericCameraManagerEngine', () => {
});
it('should get media metadata', async () => {
const engine = createEngine();
expect(
await createEngine().getMediaMetadata(
await engine.getMediaMetadata(
createHASS(),
new Map([['camera-1', createGenericCameraConfig()]]),
createStore([{ cameraID: 'camera-1', engine: engine }]),
{ type: QueryType.MediaMetadata, cameraIDs: new Set(['camera-1']) },
),
).toBeNull();
@@ -264,18 +284,6 @@ describe('GenericCameraManagerEngine', () => {
});
});
it('should get camera capabilities metadata', async () => {
expect(createEngine().getCameraCapabilities(createGenericCameraConfig())).toEqual({
canFavoriteEvents: false,
canFavoriteRecordings: false,
canSeek: false,
supportsClips: false,
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
});
});
it('should get media capabilities', () => {
expect(createEngine().getMediaCapabilities(new TestViewMedia())).toBeNull();
});
@@ -303,4 +311,10 @@ describe('GenericCameraManagerEngine', () => {
});
});
});
it('should execute PTZ action', () => {
const hass = createHASS();
createEngine().executePTZAction(hass, createCameraConfig(), 'left');
expect(hass.callService).not.toBeCalled();
});
});
+140 -45
View File
@@ -1,15 +1,23 @@
import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Camera } from '../../src/camera-manager/camera.js';
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
import { CameraManagerStore } from '../../src/camera-manager/store.js';
import { Engine } from '../../src/camera-manager/types.js';
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry/index.js';
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
import {
TestViewMedia,
createCameraCapabilities,
createCameraConfig,
} from '../test-utils.js';
describe('CameraManagerStore', async () => {
const config_visible = createCameraConfig();
const config_hidden = createCameraConfig({
const configVisible = createCameraConfig({
id: 'camera-visible',
});
const configHidden = createCameraConfig({
id: 'camera-hidden',
hide: true,
});
@@ -21,71 +29,100 @@ describe('CameraManagerStore', async () => {
const engineGeneric = await engineFactory.createEngine(Engine.Generic);
const engineFrigate = await engineFactory.createEngine(Engine.Frigate);
const setupStore = async (): Promise<CameraManagerStore> => {
const setupStore = (): CameraManagerStore => {
const store = new CameraManagerStore();
store.addCamera('camera-visible', config_visible, engineGeneric);
store.addCamera('camera-hidden', config_hidden, engineFrigate);
store.addCamera(
new Camera(configVisible, engineGeneric, createCameraCapabilities()),
);
store.addCamera(new Camera(configHidden, engineFrigate, createCameraCapabilities()));
return store;
};
it('getCameraConfig', async () => {
const store = await setupStore();
expect(store.getCameraConfig('camera-visible')).toBe(config_visible);
expect(store.getCameraConfig('camera-hidden')).toBe(config_hidden);
const store = setupStore();
expect(store.getCameraConfig('camera-visible')).toBe(configVisible);
expect(store.getCameraConfig('camera-hidden')).toBe(configHidden);
expect(store.getCameraConfig('camera-not-exist')).toBeNull();
});
it('hasCameraID', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.hasCameraID('camera-visible')).toBeTruthy();
expect(store.hasCameraID('camera-hidden')).toBeTruthy();
});
it('hasVisibleCameraID', async () => {
const store = await setupStore();
expect(store.hasVisibleCameraID('camera-visible')).toBeTruthy();
expect(store.hasVisibleCameraID('camera-hidden')).toBeFalsy();
});
it('getCameraCount', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getCameraCount()).toBe(2);
});
it('getVisibleCameraCount', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getVisibleCameraCount()).toBe(1);
});
it('getCameras', async () => {
const store = await setupStore();
expect(store.getCameras()).toEqual(
new Map([
['camera-visible', config_visible],
['camera-hidden', config_hidden],
]),
);
describe('getCamera', async () => {
it('present', async () => {
const store = setupStore();
expect(store.getCamera('camera-visible')?.getConfig()).toEqual(configVisible);
});
it('absent', async () => {
const store = setupStore();
expect(store.getCamera('not-a-camera')).toBeNull();
});
});
it('getVisibleCameras', async () => {
const store = await setupStore();
expect(store.getVisibleCameras()).toEqual(
new Map([['camera-visible', config_visible]]),
);
describe('getCameraConfigs', async () => {
it('all', async () => {
const store = setupStore();
expect([...store.getCameraConfigs()]).toEqual([configVisible, configHidden]);
});
it('named', async () => {
const store = setupStore();
expect([...store.getCameraConfigs(['camera-visible', 'not-a-camera'])]).toEqual([
configVisible,
]);
});
});
describe('getCameraConfigEntries', async () => {
it('all', async () => {
const store = setupStore();
expect([...store.getCameraConfigEntries()]).toEqual([
['camera-visible', configVisible],
['camera-hidden', configHidden],
]);
});
it('named', async () => {
const store = setupStore();
expect([
...store.getCameraConfigEntries(['camera-visible', 'not-a-camera']),
]).toEqual([['camera-visible', configVisible]]);
});
});
it('getCameras', async () => {
const store = setupStore();
expect([...store.getCameras().keys()]).toEqual(['camera-visible', 'camera-hidden']);
expect(store.getCameras().get('camera-visible')?.getConfig()).toEqual(configVisible);
expect(store.getCameras().get('camera-hidden')?.getConfig()).toEqual(configHidden);
});
it('getCameraIDs', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getCameraIDs()).toEqual(new Set(['camera-visible', 'camera-hidden']));
});
it('getVisibleCameraIDs', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getVisibleCameraIDs()).toEqual(new Set(['camera-visible']));
});
it('reset', async () => {
const store = await setupStore();
const store = setupStore();
store.reset();
@@ -94,24 +131,24 @@ describe('CameraManagerStore', async () => {
});
it('getCameraConfigForMedia', async () => {
const store = await setupStore();
const store = setupStore();
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getCameraConfigForMedia(media_1)).toBe(config_visible);
expect(store.getCameraConfigForMedia(media_1)).toBe(configVisible);
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
});
it('getEngineOfType', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getEngineOfType(Engine.Generic)).toBe(engineGeneric);
expect(store.getEngineOfType(Engine.Frigate)).toBe(engineFrigate);
expect(store.getEngineOfType(Engine.MotionEye)).toBeNull();
});
it('getEngineForCameraID', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getEngineForCameraID('camera-visible')).toBe(engineGeneric);
expect(store.getEngineForCameraID('camera-hidden')).toBe(engineFrigate);
expect(store.getEngineForCameraID('camera-not-exist')).toBeNull();
@@ -119,13 +156,23 @@ describe('CameraManagerStore', async () => {
describe('getEnginesForCameraIDs', async () => {
it('empty input', async () => {
const store = await setupStore();
const store = setupStore();
expect(store.getEnginesForCameraIDs(new Set())).toBeNull();
});
it('multiple cameras', async () => {
const store = await setupStore();
store.addCamera('camera-visible2', config_visible, engineGeneric);
const store = setupStore();
store.addCamera(
new Camera(
{
...configVisible,
id: 'camera-visible2',
},
engineGeneric,
createCameraCapabilities(),
),
);
expect(
store.getEnginesForCameraIDs(
new Set([
@@ -145,13 +192,61 @@ describe('CameraManagerStore', async () => {
});
it('getEngineForMedia', async () => {
const store = await setupStore();
const store = setupStore();
const media = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
});
it('getAllEngines', async () => {
const store = await setupStore();
expect(store.getAllEngines()).toEqual([engineGeneric, engineFrigate]);
describe('getAllDependentCameras', () => {
it('should return dependent cameras', () => {
const store = new CameraManagerStore();
store.addCamera(
new Camera(
createCameraConfig({
id: 'one',
dependencies: {
cameras: ['two', 'three'],
},
}),
engineGeneric,
createCameraCapabilities(),
),
);
store.addCamera(
new Camera(
createCameraConfig({
id: 'two',
}),
engineGeneric,
createCameraCapabilities(),
),
);
expect(store.getAllDependentCameras('one')).toEqual(new Set(['one', 'two']));
});
it('should return all cameras', () => {
const store = new CameraManagerStore();
store.addCamera(
new Camera(
createCameraConfig({
id: 'one',
dependencies: {
all_cameras: true,
},
}),
engineGeneric,
createCameraCapabilities(),
),
);
store.addCamera(
new Camera(
createCameraConfig({
id: 'two',
}),
engineGeneric,
createCameraCapabilities(),
),
);
expect(store.getAllDependentCameras('one')).toEqual(new Set(['one', 'two']));
});
});
});
+56 -1
View File
@@ -23,7 +23,6 @@ import {
} from '../test-utils';
vi.mock('../../src/utils/action.js');
vi.mock('../../src/camera-manager/manager.js');
const createAction = (
action: Record<string, unknown>,
@@ -708,6 +707,62 @@ describe('ActionsManager.executeAction', () => {
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
});
describe('should handle ptz action', () => {
it('with selected camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
await manager.executeAction(
createAction({
frigate_card_action: 'ptz',
ptz_action: 'left',
})!,
);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
preset: undefined,
},
);
});
it('without selected camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeAction(
createAction({
frigate_card_action: 'ptz',
ptz_action: 'left',
})!,
);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
});
});
it('should handle show_ptz action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeAction(
createAction({
frigate_card_action: 'show_ptz',
show_ptz: true,
})!,
);
expect(api.getViewManager().setViewWithNewContext).toBeCalledWith(
expect.objectContaining({ live: { ptzVisible: true } }),
);
});
it('should handle unknown action', async () => {
const manager = new ActionsManager(createCardAPI());
+21 -22
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CardController } from '../../src/card-controller/controller';
import { HASSManager } from '../../src/card-controller/hass-manager';
import { CardHASSAPI } from '../../src/card-controller/types';
import {
createCameraConfig,
createCameraManager,
@@ -8,12 +8,11 @@ import {
createConfig,
createHASS,
createStateEntity,
createStore,
createView,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
const createAPIWithoutMediaPlayers = (): CardHASSAPI => {
const createAPIWithoutMediaPlayers = (): CardController => {
const api = createCardAPI();
vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([]);
return api;
@@ -158,20 +157,20 @@ describe('HASSManager', () => {
describe('should set default view when', () => {
it('selected camera trigger entity changes', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera.foo',
createCameraConfig({
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
triggers: {
entities: ['binary_sensor.motion'],
},
}),
],
},
]),
});
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.foo',
@@ -189,20 +188,20 @@ describe('HASSManager', () => {
});
it('selected camera is unknown', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera.foo',
createCameraConfig({
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
triggers: {
entities: ['binary_sensor.motion'],
},
}),
],
},
]),
});
const api = createAPIWithoutMediaPlayers();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.UNKNOWN',
@@ -18,9 +18,37 @@ describe('InitializationManager', () => {
vi.resetAllMocks();
});
it('should not be initialized', () => {
const manager = new InitializationManager(createCardAPI());
expect(manager.isInitializedMandatory()).toBeFalsy();
describe('should correctly determine when mandatory initialization is required', () => {
it('without aspects', () => {
const api = createCardAPI();
const initializer = mock<Initializer>();
const manager = new InitializationManager(api, initializer);
initializer.isInitializedMultiple.mockReturnValue(false);
expect(manager.isInitializedMandatory()).toBeFalsy();
});
it('without view', () => {
const api = createCardAPI();
const initializer = mock<Initializer>();
const manager = new InitializationManager(api, initializer);
initializer.isInitializedMultiple.mockReturnValue(true);
expect(manager.isInitializedMandatory()).toBeFalsy();
});
it('with aspects and view', () => {
const api = createCardAPI();
const initializer = mock<Initializer>();
const manager = new InitializationManager(api, initializer);
initializer.isInitializedMultiple.mockReturnValue(true);
vi.mocked(api.getViewManager().hasView).mockReturnValue(true);
expect(manager.isInitializedMandatory()).toBeTruthy();
});
});
describe('should initialize mandatory', () => {
@@ -41,7 +69,9 @@ describe('InitializationManager', () => {
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
false,
);
const manager = new InitializationManager(api);
expect(await manager.initializeMandatory()).toBeTruthy();
@@ -50,7 +80,6 @@ describe('InitializationManager', () => {
expect(sideLoadHomeAssistantElements).toBeCalled();
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
expect(api.getViewManager().setViewDefault).toBeCalled();
expect(api.getCardElementManager().update).toBeCalled();
});
it('successfully with querystring view', async () => {
@@ -71,7 +100,9 @@ describe('InitializationManager', () => {
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
false,
);
const manager = new InitializationManager(api);
expect(await manager.initializeMandatory()).toBeTruthy();
@@ -134,7 +165,6 @@ describe('InitializationManager', () => {
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
expect(api.getMicrophoneManager().connect).not.toBeCalled();
expect(api.getCardElementManager().update).not.toBeCalled();
});
it('successfully with all inititalizers', async () => {
@@ -1,20 +1,19 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const';
import { ExtendedHomeAssistant } from '../../src/types';
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
import {
TestViewMedia,
createCameraConfig,
createCameraManager,
createCardAPI,
createHASS,
createRegistryEntity,
createStateEntity,
createStore,
TestViewMedia,
} from '../test-utils';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
import { ExtendedHomeAssistant } from '../../src/types';
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const';
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
import { mock } from 'vitest-mock-extended';
vi.mock('../../src/camera-manager/manager.js');
const createHASSWithMediaPlayers = (): ExtendedHomeAssistant => {
const attributesSupported = {
@@ -145,9 +144,7 @@ describe('MediaPlayerManager', () => {
describe('live', () => {
it('without camera config', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(null);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
@@ -159,28 +156,33 @@ describe('MediaPlayerManager', () => {
describe('using standard method', () => {
it('successfully', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
}),
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
camera_entity: 'camera.foo',
}),
},
]),
);
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
vi.mocked(api.getCameraManager().getCameraMetadata).mockReturnValue({
title: 'camera title',
icon: 'icon',
});
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const hass = createHASS({
'camera.foo': createStateEntity({
attributes: {
entity_picture: 'http://thumbnail',
},
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS({
'camera.foo': createStateEntity({
attributes: {
entity_picture: 'http://thumbnail',
},
}),
}),
});
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
);
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
@@ -199,32 +201,41 @@ describe('MediaPlayerManager', () => {
it('without camera_entity', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({}),
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig(),
},
]),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
});
it('without title and thumbnail', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
}),
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
camera_entity: 'camera.foo',
}),
},
]),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
@@ -242,25 +253,29 @@ describe('MediaPlayerManager', () => {
describe('using dashboard method', () => {
it('successfully', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
},
}),
},
}),
]),
);
vi.mocked(api.getQueryStringManager().generateQueryString).mockReturnValue('');
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'cast',
@@ -275,24 +290,28 @@ describe('MediaPlayerManager', () => {
it('without hass', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
},
}),
},
}),
]),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
// No actual test can be performed here as nothing observable happens.
// This test serves only as code-coverage long-tail.
@@ -301,21 +320,25 @@ describe('MediaPlayerManager', () => {
it('without required configuration', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.foo',
config: createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
},
}),
},
}),
]),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
await manager.playLive('media_player.foo', 'camera.foo');
expect(
vi.mocked(api.getMessageManager().setMessageIfHigherPriority),
@@ -63,7 +63,7 @@ describe('MicrophoneManager', () => {
const manager = new MicrophoneManager(api);
navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error());
await manager.connect();
expect(await manager.connect()).toBeFalsy();
expect(manager.isConnected()).toBeFalsy();
expect(manager.isForbidden()).toBeTruthy();
+16 -17
View File
@@ -1,8 +1,9 @@
import add from 'date-fns/add';
import { HassEntities } from 'home-assistant-js-websocket';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ScanOptions } from '../../src/config/types';
import { CardController } from '../../src/card-controller/controller';
import { TriggersManager } from '../../src/card-controller/triggers-manager';
import { ScanOptions } from '../../src/config/types';
import {
createCameraConfig,
createCameraManager,
@@ -10,18 +11,17 @@ import {
createConfig,
createHASS,
createStateEntity,
createStore,
createView,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
// function reduces it.
const createTriggerAPI = (options?: {
config?: Partial<ScanOptions>;
hassStates?: HassEntities;
interaction?: boolean;
}) => {
}): CardController => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
@@ -37,19 +37,18 @@ const createTriggerAPI = (options?: {
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS(options?.hassStates),
);
vi.mocked(api.getCameraManager).mockReturnValue(
createCameraManager({
configs: new Map([
[
'camera_1',
createCameraConfig({
triggers: {
entities: ['binary_sensor.motion'],
},
}),
],
]),
}),
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera_1',
config: createCameraConfig({
triggers: {
entities: ['binary_sensor.motion'],
},
}),
},
]),
);
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(
options?.interaction ?? false,
+305 -149
View File
@@ -1,20 +1,24 @@
import { describe, expect, it, vi } from 'vitest';
import { QueryType } from '../../src/camera-manager/types';
import { FrigateCardView } from '../../src/config/types';
import { getAllDependentCameras } from '../../src/utils/camera';
import { ViewManager } from '../../src/card-controller/view-manager';
import { FrigateCardView } from '../../src/config/types';
import { executeMediaQueryForView } from '../../src/utils/media-to-view';
import { EventMediaQueries } from '../../src/view/media-queries';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
import {
createCameraCapabilities,
createCameraConfig,
createCameraManager,
createCardAPI,
createConfig,
createHASS,
createStore,
createView,
generateViewMediaArray,
generateViewMediaArray
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/camera');
vi.mock('../../src/utils/media-to-view');
describe('ViewManager.setView', () => {
it('should set view', () => {
@@ -29,6 +33,7 @@ describe('ViewManager.setView', () => {
manager.setView(view);
expect(manager.getView()).toBe(view);
expect(manager.hasView()).toBeTruthy();
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
expect(api.getCardElementManager().scrollReset).toBeCalled();
expect(api.getMessageManager().reset).toBeCalled();
@@ -101,6 +106,7 @@ describe('ViewManager.reset', () => {
manager.reset();
expect(manager.getView()).toBeNull();
expect(manager.hasView()).toBeFalsy();
});
});
@@ -109,6 +115,13 @@ describe('ViewManager.setViewDefault', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
const manager = new ViewManager(api);
manager.setViewDefault();
@@ -130,12 +143,18 @@ describe('ViewManager.setViewDefault', () => {
});
it('should cycle camera when configured', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera_1',
},
{
cameraID: 'camera_2',
},
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
@@ -160,13 +179,19 @@ describe('ViewManager.setViewDefault', () => {
});
it('should respect parameters', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera.kitchen', 'camera.office']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
{
cameraID: 'camera.office',
},
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const manager = new ViewManager(api);
manager.setViewDefault({
@@ -186,60 +211,84 @@ describe('ViewManager.setViewByParameters', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
supportsClips: true,
}),
);
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera',
cameraID: 'camera.kitchen',
viewName: 'clips',
});
expect(manager.getView()?.view).toBe('clips');
expect(manager.getView()?.camera).toBe('camera');
expect(manager.getView()?.camera).toBe('camera.kitchen');
});
it('should set view by parameters using existing view if unspecified', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
{
cameraID: 'camera.office',
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
supportsClips: true,
}),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera_1',
cameraID: 'camera.kitchen',
viewName: 'clips',
});
manager.setViewByParameters({
cameraID: 'camera_2',
cameraID: 'camera.office',
});
expect(manager.getView()?.view).toBe('clips');
expect(manager.getView()?.camera).toBe('camera_2');
expect(manager.getView()?.camera).toBe('camera.office');
});
it('should set view by parameters using config as fallback', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera_1',
cameraID: 'camera.kitchen',
// No prior view, and no specified view. This could happen during query
// string based initialization.
});
expect(manager.getView()?.view).toBe('live');
expect(manager.getView()?.camera).toBe('camera_1');
expect(manager.getView()?.camera).toBe('camera.kitchen');
});
it('should not set view by parameters without config', () => {
@@ -253,12 +302,19 @@ describe('ViewManager.setViewByParameters', () => {
});
it('should not set view by parameters without visible cameras', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(new Set());
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
config: createCameraConfig({
hide: true,
}),
},
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const manager = new ViewManager(api);
manager.setViewByParameters({
@@ -278,6 +334,21 @@ describe('ViewManager.setViewByParameters', () => {
])('%s', (viewName: FrigateCardView) => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
);
vi.mocked(api.getConfigManager()).getConfig.mockReturnValue(
createConfig({
media_viewer: {
@@ -295,7 +366,7 @@ describe('ViewManager.setViewByParameters', () => {
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera',
cameraID: 'camera.kitchen',
viewName: viewName,
});
@@ -314,43 +385,31 @@ describe('ViewManager.setViewByParameters', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
);
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera',
cameraID: 'camera.kitchen',
viewName: viewName,
});
expect(manager.getView()?.displayMode).toBe('single');
});
});
it('should set view by parameters using config as fallback', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(getAllDependentCameras).mockReturnValue(
new Set(['camera_1', 'camera_1_hd']),
);
const manager = new ViewManager(api);
manager.setViewByParameters({
cameraID: 'camera_1',
viewName: 'live',
substream: 'camera_1_hd',
});
expect(manager.getView()?.view).toBe('live');
expect(manager.getView()?.camera).toBe('camera_1');
expect(manager.getView()?.context?.live?.overrides).toEqual(
new Map([['camera_1', 'camera_1_hd']]),
);
});
});
// @vitest-environment jsdom
@@ -360,6 +419,7 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
const manager = new ViewManager(api);
manager.setView(createView());
@@ -380,20 +440,40 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
});
it('should set display mode to grid and create new query', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
{
cameraID: 'camera.office',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
]),
);
const hass = createHASS();
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
const media = generateViewMediaArray({ count: 5 });
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
const mediaArray = generateViewMediaArray({ count: 5 });
vi.mocked(executeMediaQueryForView).mockResolvedValue(
new View({
view: 'clip',
camera: 'camera.kitchen',
queryResults: new MediaQueriesResults({ results: mediaArray }),
}),
);
const manager = new ViewManager(api);
const query = new EventMediaQueries([
@@ -410,39 +490,50 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
await manager.setViewWithNewDisplayMode('grid');
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
expect(cameraManager.executeMediaQueries).toBeCalledWith(
expect.arrayContaining([
expect.objectContaining({
type: 'event-query',
cameraIDs: new Set(['camera_1', 'camera_2']),
hasClip: true,
}),
]),
);
expect(manager.getView()?.queryResults?.getResults()).toBe(mediaArray);
});
it('should set display mode to single and create new query', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
{
cameraID: 'camera.office',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
]),
);
const hass = createHASS();
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
const media = generateViewMediaArray({ count: 5 });
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
const mediaArray = generateViewMediaArray({ count: 5 });
vi.mocked(executeMediaQueryForView).mockResolvedValue(
new View({
view: 'clip',
camera: 'camera.kitchen',
queryResults: new MediaQueriesResults({ results: mediaArray }),
}),
);
const manager = new ViewManager(api);
const query = new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set(['camera_1', 'camera_2']),
cameraIDs: new Set(['camera.kitchen', 'camera.office']),
hasClip: true,
},
]);
@@ -450,53 +541,58 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
manager.setView(
createView({
view: 'clip',
camera: 'camera_2',
camera: 'camera.office',
query: query,
}),
);
await manager.setViewWithNewDisplayMode('single');
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
expect(cameraManager.executeMediaQueries).toBeCalledWith(
expect.arrayContaining([
expect.objectContaining({
type: 'event-query',
cameraIDs: new Set(['camera_2']),
hasClip: true,
}),
]),
);
expect(manager.getView()?.queryResults?.getResults()).toBe(mediaArray);
});
it('should set display mode to single and handle failed new query', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
{
cameraID: 'camera.office',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
]),
);
const manager = new ViewManager(api);
const query = new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set(['camera_1', 'camera_2']),
cameraIDs: new Set(['camera.kitchen', 'camera.office']),
hasClip: true,
},
]);
const originalView = createView({
view: 'clip',
camera: 'camera_2',
camera: 'camera.office',
query: query,
});
manager.setView(originalView);
// Query execution fails / returns null.
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(null);
vi.mocked(executeMediaQueryForView).mockRejectedValue(null);
await manager.setViewWithNewDisplayMode('single');
@@ -504,14 +600,28 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
});
it('should set display mode and handle empty new query results', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
new Set(['camera_1', 'camera_2']),
);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
{
cameraID: 'camera.office',
capabilities: createCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
}),
},
]),
);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new ViewManager(api);
@@ -519,20 +629,20 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
const query = new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set(['camera_1']),
cameraIDs: new Set(['camera.kitchen']),
hasClip: true,
},
]);
const originalView = createView({
view: 'clip',
camera: 'camera_2',
camera: 'camera.office',
query: query,
});
manager.setView(originalView);
await manager.setViewWithNewDisplayMode('grid');
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
vi.mocked(executeMediaQueryForView).mockResolvedValue(null);
// Empty queries will not be executed, so view will not be changed.
expect(manager.getView()?.displayMode).toBeNull();
@@ -541,13 +651,21 @@ describe('ViewManager.setViewWithNewDisplayMode', () => {
describe('ViewManager.setViewWithSubstream', () => {
it('should set new equal view with no dependencies', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
},
]),
);
const manager = new ViewManager(api);
const view = createView({
view: 'live',
camera: 'camera',
});
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera']));
const manager = new ViewManager(createCardAPI());
manager.setView(view);
manager.setViewWithSubstream();
@@ -557,39 +675,71 @@ describe('ViewManager.setViewWithSubstream', () => {
});
it('should set new view with next substream', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
config: createCameraConfig({
dependencies: {
cameras: ['camera.kitchen_hd'],
},
}),
},
{
cameraID: 'camera.kitchen_hd',
},
]),
);
const manager = new ViewManager(api);
const view = createView({
view: 'live',
camera: 'camera',
camera: 'camera.kitchen',
});
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
const manager = new ViewManager(createCardAPI());
manager.setView(view);
manager.setViewWithSubstream();
expect(manager.getView()?.context?.live?.overrides).toEqual(
new Map([['camera', 'camera2']]),
new Map([['camera.kitchen', 'camera.kitchen_hd']]),
);
});
it('should set new view with next substream when view has invalid substream', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
config: createCameraConfig({
dependencies: {
cameras: ['camera.kitchen_hd'],
},
}),
},
{
cameraID: 'camera.kitchen_hd',
},
]),
);
const manager = new ViewManager(api);
const view = createView({
view: 'live',
camera: 'camera',
camera: 'camera.kitchen',
context: {
live: {
overrides: new Map([['camera', 'camera-that-does-not-exist']]),
overrides: new Map([['camera.kitchen', 'camera-that-does-not-exist']]),
},
},
});
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
const manager = new ViewManager(createCardAPI());
manager.setView(view);
manager.setViewWithSubstream();
expect(manager.getView()?.context?.live?.overrides).toEqual(
new Map([['camera', 'camera']]),
new Map([['camera.kitchen', 'camera.kitchen']]),
);
});
@@ -680,17 +830,23 @@ describe('ViewManager.isViewSupportedByCamera', () => {
['media' as const, false],
])('%s', (viewName: FrigateCardView, expected: boolean) => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({
canFavoriteEvents: false,
canFavoriteRecordings: false,
canSeek: false,
supportsClips: false,
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
});
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.kitchen',
capabilities: {
canFavoriteEvents: false,
canFavoriteRecordings: false,
canSeek: false,
supportsClips: false,
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
},
},
]),
);
const manager = new ViewManager(api);
expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected);
+201 -104
View File
@@ -12,11 +12,12 @@ import {
} from '../../src/components-lib/menu-controller';
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
import { FrigateCardMediaPlayer } from '../../src/types';
import { createFrigateCardCustomAction } from '../../src/utils/action';
import { createFrigateCardSimpleAction } from '../../src/utils/action';
import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
import {
createAggregateCameraCapabilities,
createCameraCapabilities,
createCameraConfig,
createCameraManager,
@@ -26,10 +27,10 @@ import {
createMediaCapabilities,
createMediaLoadedInfo,
createStateEntity,
createStore,
createView,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/media-player-controller.js');
vi.mock('../../src/card-controller/microphone-manager.js');
@@ -42,16 +43,15 @@ const calculateButtons = (
view?: View;
},
): MenuItem[] => {
let cameraManager: CameraManager | null = options?.cameraManager ?? null;
if (!cameraManager) {
cameraManager = createCameraManager();
}
return controller.calculateButtons(
options?.hass ?? createHASS(),
options?.config ?? createConfig(),
options?.cameraManager ??
createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
}),
cameraManager,
options?.view ?? createView({ camera: 'camera-1' }),
options,
);
@@ -79,8 +79,8 @@ describe('MenuButtonController', () => {
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('menu_toggle'),
hold_action: createFrigateCardCustomAction('diagnostics'),
tap_action: createFrigateCardSimpleAction('menu_toggle'),
hold_action: createFrigateCardSimpleAction('diagnostics'),
});
});
@@ -94,19 +94,17 @@ describe('MenuButtonController', () => {
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('default'),
hold_action: createFrigateCardCustomAction('diagnostics'),
tap_action: createFrigateCardSimpleAction('default'),
hold_action: createFrigateCardSimpleAction('diagnostics'),
});
});
it('should have cameras menu', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
});
mock<CameraManager>(cameraManager).getCameraMetadata.mockReturnValue({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
);
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
title: 'title',
icon: 'icon',
});
@@ -150,14 +148,12 @@ describe('MenuButtonController', () => {
});
it('should not have a cameras menu without a visible camera', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{ cameraID: 'camera-1', config: createCameraConfig({ hide: true }) },
]),
});
vi.mocked(cameraManager.getStore()).getVisibleCameras.mockReturnValue(new Map());
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toEqual(
@@ -170,12 +166,16 @@ describe('MenuButtonController', () => {
});
it('should have substream button with single dependency', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
});
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -193,12 +193,17 @@ describe('MenuButtonController', () => {
});
it('should have substream button selected with single dependency', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
});
);
const view = createView({
camera: 'camera-1',
context: {
@@ -227,29 +232,31 @@ describe('MenuButtonController', () => {
});
it('should have substream menu without substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
],
},
]),
});
);
// Return different metadata depending on the camera to test multiple code
// paths.
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
@@ -316,29 +323,30 @@ describe('MenuButtonController', () => {
});
it('should have substream menu with substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
],
},
]),
});
);
const view = createView({
camera: 'camera-1',
@@ -436,8 +444,8 @@ describe('MenuButtonController', () => {
it('should have styled clips menu button in clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -457,8 +465,8 @@ describe('MenuButtonController', () => {
it('should have unstyled clips menu button in non-clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -475,8 +483,8 @@ describe('MenuButtonController', () => {
it('should have styled snapshots menu button in snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -496,8 +504,8 @@ describe('MenuButtonController', () => {
it('should have unstyled snapshots menu button in non-snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -514,8 +522,8 @@ describe('MenuButtonController', () => {
it('should have styled recordings menu button in recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -535,8 +543,8 @@ describe('MenuButtonController', () => {
it('should have unstyled recordings menu button in non-recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -581,8 +589,8 @@ describe('MenuButtonController', () => {
it('should have styled timeline menu button in timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -601,8 +609,8 @@ describe('MenuButtonController', () => {
it('should have unstyled timeline menu button in non-timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -622,15 +630,15 @@ describe('MenuButtonController', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
selectedIndex: 0,
}),
});
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
@@ -653,15 +661,15 @@ describe('MenuButtonController', () => {
});
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
selectedIndex: 0,
}),
});
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
@@ -906,16 +914,18 @@ describe('MenuButtonController', () => {
});
it('should have media players button', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
}),
],
},
]),
});
);
const mediaPlayerController = mock<MediaPlayerManager>();
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
mediaPlayerController.getMediaPlayers.mockReturnValue(['media_player.tv']);
@@ -961,16 +971,17 @@ describe('MenuButtonController', () => {
});
it('should disable media players button when entity not found', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
}),
],
},
]),
});
);
const mediaPlayerController = mock<MediaPlayerManager>();
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
mediaPlayerController.getMediaPlayers.mockReturnValue(['not_a_real_player']);
@@ -1109,7 +1120,13 @@ describe('MenuButtonController', () => {
'%s',
(displayMode: ViewDisplayMode) => {
const view = createView({ view: 'live', displayMode: displayMode });
expect(calculateButtons(controller, { view: view })).toContainEqual({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
);
expect(
calculateButtons(controller, { cameraManager: cameraManager, view: view }),
).toContainEqual({
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
enabled: true,
priority: 50,
@@ -1118,6 +1135,7 @@ describe('MenuButtonController', () => {
displayMode === 'grid'
? 'Show single media viewer'
: 'Show media viewer for each camera in a grid',
style: displayMode === 'grid' ? { color: 'var(--primary-color, white)' } : {},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'display_mode_select',
@@ -1128,6 +1146,85 @@ describe('MenuButtonController', () => {
);
});
describe('should have show ptz button', () => {
it('when the selected camera is not PTZ enabled', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities(),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
it('when the selected camera is PTZ enabled', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities({ ptz: {} }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
it('when the context has PTZ visiblity turned off', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities({ ptz: {} }),
);
const view = createView({
camera: 'camera-1',
context: { live: { ptzVisible: false } },
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: true,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
});
it('should handle dynamic buttons', () => {
const button: MenuItem = {
...dynamicButton,
+316
View File
@@ -0,0 +1,316 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PTZController } from '../../src/components-lib/ptz-controller';
import {
FrigateCardPTZConfig,
frigateCardPTZSchema,
PTZControlAction,
} from '../../src/config/types';
import {
frigateCardHandleActionConfig,
getActionConfigGivenAction,
} from '../../src/utils/action.js';
import {
createCameraCapabilities,
createCameraManager,
createHASS,
} from '../test-utils';
vi.mock('../../src/utils/action.js');
const createConfig = (config?: Partial<FrigateCardPTZConfig>): FrigateCardPTZConfig => {
return frigateCardPTZSchema.parse({
...config,
});
};
// @vitest-environment jsdom
describe('PTZController', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should be creatable', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller).toBeTruthy();
});
it('should get config creatable', () => {
const controller = new PTZController(document.createElement('div'));
const config = createConfig();
controller.setConfig(config);
expect(controller.getConfig()).toBe(config);
});
describe('should set element attributes', () => {
describe('orientation', () => {
describe('with config', () => {
it.each([['horizontal' as const], ['vertical' as const]])(
'%s',
(orientation: 'horizontal' | 'vertical') => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ orientation: orientation }));
expect(element.getAttribute('data-orientation')).toBe(orientation);
},
);
});
it('without config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig();
expect(element.getAttribute('data-orientation')).toBe('horizontal');
});
});
describe('position', () => {
describe('with config', () => {
it.each([
['top-left' as const],
['top-right' as const],
['bottom-left' as const],
['bottom-right' as const],
])(
'%s',
(position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ position: position }));
expect(element.getAttribute('data-position')).toBe(position);
},
);
});
it('without config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig();
expect(element.getAttribute('data-position')).toBe('bottom-right');
});
});
it('with style in config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ style: { transform: 'none', left: '50%' } }));
expect(element.getAttribute('style')).toBe('transform:none;left:50%');
});
});
describe('should respect mode', () => {
it('off', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller.shouldDisplay()).toBeFalsy();
});
it('off but forced on', () => {
const controller = new PTZController(document.createElement('div'));
controller.setForceVisibility(true);
// No actions, no rendering, no matter what.
expect(controller.shouldDisplay()).toBeFalsy();
});
it('on without actions', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on' }));
expect(controller.shouldDisplay()).toBeFalsy();
});
it('on with actions', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.shouldDisplay()).toBeTruthy();
});
it('on with actions but forced off', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
controller.setCamera(createCameraManager(), 'camera.office');
controller.setForceVisibility(false);
expect(controller.shouldDisplay()).toBeFalsy();
});
});
describe('should get PTZ actions', () => {
it('without config', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller.getPTZActions('left')).toBeNull();
});
describe('using defaults', () => {
describe('with continous PTZ', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
panTilt: ['continuous'],
zoom: ['continuous'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
start_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
ptz_phase: 'start',
},
end_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
ptz_phase: 'stop',
},
});
});
});
describe('with relative PTZ', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
panTilt: ['relative'],
zoom: ['relative'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
},
});
});
});
it('home', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
presets: ['preset-foo'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions('home')).toEqual({
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'preset',
ptz_preset: 'preset-foo',
},
});
});
});
describe('using config', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(
createConfig({
[`actions_${actionName}`]: {
argument: actionName,
},
}),
);
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
argument: actionName,
});
});
it('home', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(
createConfig({
actions_home: {
argument: 'home',
},
}),
);
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.getPTZActions('home')).toEqual({
argument: 'home',
});
});
});
});
describe('should handle action', () => {
it('without hass or action', () => {
const controller = new PTZController(document.createElement('div'));
controller.setHASS();
controller.setCamera();
controller.handleAction(
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
);
expect(frigateCardHandleActionConfig).not.toBeCalled();
});
it('with action', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
const hass = createHASS();
controller.setHASS(hass);
controller.setConfig(
createConfig({
[`actions_left`]: {},
}),
);
const tapAction = {
action: 'none' as const,
};
const actionsConfig = {
tap_action: tapAction,
};
vi.mocked(getActionConfigGivenAction).mockReturnValue(tapAction);
controller.handleAction(
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
actionsConfig,
);
expect(frigateCardHandleActionConfig).toBeCalledWith(
element,
hass,
actionsConfig,
'tap',
actionsConfig.tap_action,
);
});
});
});
+429
View File
@@ -739,4 +739,433 @@ describe('should handle version specific upgrades', () => {
});
});
});
describe('should move PTZ elements to live', () => {
it('case with 1 element', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Hello 1',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:frigate-card',
});
});
it('case with >1 element', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
service: 'service',
service_data: {
message: "It's a trick",
},
title: 'title',
type: 'service-button',
},
],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Hello 1',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:frigate-card',
});
});
it('case with custom conditional element with 2 PTZ but nothing else', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: {
fullscreen: true,
media_loaded: true,
},
elements: [
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 2',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Hello 1',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:frigate-card',
});
});
it('case with custom conditional element with 1 PTZ and another element', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: {
fullscreen: true,
media_loaded: true,
},
elements: [
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Hello 1',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
elements: [
{
type: 'custom:frigate-card-conditional',
conditions: {
fullscreen: true,
media_loaded: true,
},
elements: [
{
type: 'service-button',
title: 'title',
service: 'service',
service_data: {
message: "It's a trick",
},
},
],
},
],
type: 'custom:frigate-card',
});
});
it('case with stock conditional element with 1 PTZ', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
elements: [
{
type: 'conditional',
conditions: [{ entity: 'light.office', state: 'on' }],
elements: [
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Hello 1',
},
},
},
},
],
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Hello 1',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:frigate-card',
});
});
it('case when live.controls.ptz already exists', () => {
const config = {
type: 'custom:frigate-card',
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Original',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
elements: [
{
type: 'custom:frigate-card-ptz',
orientation: 'vertical',
style: {
right: '20px',
top: '20px',
color: 'white',
},
actions_up: {
tap_action: {
action: 'call-service',
service: 'notify.persistent_notification',
service_data: {
message: 'Replacement that should be ignored',
},
},
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config).toEqual({
cameras: [{ camera_entity: 'camera.office' }],
live: {
controls: {
ptz: {
actions_up: {
tap_action: {
action: 'call-service',
data: {
message: 'Original',
},
service: 'notify.persistent_notification',
},
},
orientation: 'vertical',
style: {
color: 'white',
right: '20px',
top: '20px',
},
},
},
},
type: 'custom:frigate-card',
});
});
});
});
+19 -1
View File
@@ -68,6 +68,14 @@ describe('config defaults', () => {
size: 48,
style: 'chevrons',
},
ptz: {
hide_home: false,
hide_pan_tilt: false,
hide_zoom: false,
mode: 'on',
orientation: 'horizontal',
position: 'bottom-right',
},
thumbnails: {
media: 'all',
mode: 'right',
@@ -208,6 +216,10 @@ describe('config defaults', () => {
enabled: false,
priority: 50,
},
ptz: {
enabled: false,
priority: 50,
},
recordings: {
enabled: false,
priority: 50,
@@ -343,7 +355,13 @@ describe('should convert webrtc card PTZ to Frigate card PTZ', () => {
},
},
service: 'foo',
type: 'custom:frigate-card-ptz',
hide_home: false,
hide_pan_tilt: false,
hide_zoom: false,
mode: 'on',
orientation: 'horizontal',
position: 'bottom-right',
});
});
});
+55 -57
View File
@@ -1,27 +1,17 @@
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
import { vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Camera } from '../src/camera-manager/camera';
import { CameraManagerEngine } from '../src/camera-manager/engine';
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
import { GenericCameraManagerEngine } from '../src/camera-manager/generic/engine-generic';
import { CameraManager } from '../src/camera-manager/manager';
import { CameraManagerStore } from '../src/camera-manager/store';
import {
CameraConfigs,
CameraManagerCameraCapabilities,
CameraManagerCapabilities,
CameraManagerMediaCapabilities,
QueryType,
} from '../src/camera-manager/types';
import {
CameraConfig,
FrigateCardCondition,
FrigateCardConfig,
PerformanceConfig,
RawFrigateCardConfig,
cameraConfigSchema,
frigateCardConditionSchema,
frigateCardConfigSchema,
performanceConfigSchema,
} from '../src/config/types';
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
import { ActionsManager } from '../src/card-controller/actions-manager';
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
import { AutomationsManager } from '../src/card-controller/automations-manager';
@@ -44,6 +34,18 @@ import { QueryStringManager } from '../src/card-controller/query-string-manager'
import { StyleManager } from '../src/card-controller/style-manager';
import { TriggersManager } from '../src/card-controller/triggers-manager';
import { ViewManager } from '../src/card-controller/view-manager';
import {
CameraConfig,
cameraConfigSchema,
FrigateCardCondition,
frigateCardConditionSchema,
FrigateCardConfig,
frigateCardConfigSchema,
PerformanceConfig,
performanceConfigSchema,
RawFrigateCardConfig,
} from '../src/config/types';
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
import { Entity } from '../src/utils/ha/entity-registry/types';
import { ViewMedia, ViewMediaType } from '../src/view/media';
import { MediaQueriesResults } from '../src/view/media-queries-results';
@@ -150,55 +152,51 @@ export const createViewWithMedia = (options?: Partial<ViewParameters>): View =>
});
};
export const createCameraManager = (options?: {
store?: CameraManagerStore;
configs?: CameraConfigs;
}): CameraManager => {
const cameraManager = new CameraManager(createCardAPI());
let store: CameraManagerStore | undefined = options?.store;
if (!store) {
store = mock<CameraManagerStore>();
const configs = options?.configs ?? new Map([['camera', createCameraConfig()]]);
vi.mocked(store.getCameras).mockReturnValue(configs);
vi.mocked(store.getVisibleCameras).mockReturnValue(configs);
vi.mocked(store.getVisibleCameraIDs).mockReturnValue(new Set(configs.keys()));
vi.mocked(store.hasVisibleCameraID).mockImplementation((cameraID: string) =>
[...configs.keys()].includes(cameraID),
);
vi.mocked(store.getCameraConfig).mockImplementation(
(cameraID): CameraConfig | null => {
return configs.get(cameraID) ?? null;
},
export const createStore = (
cameras?: {
cameraID: string;
engine?: CameraManagerEngine;
config?: CameraConfig;
capabilities?: CameraManagerCameraCapabilities;
}[],
): CameraManagerStore => {
const store = new CameraManagerStore();
for (const cameraProps of cameras ?? []) {
const camera = new Camera(
cameraProps.config ?? createCameraConfig(),
cameraProps.engine ?? new GenericCameraManagerEngine(),
cameraProps.capabilities ?? createCameraCapabilities(),
);
camera.setID(cameraProps.cameraID);
store.addCamera(camera);
}
vi.mocked(cameraManager.getStore).mockReturnValue(store);
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
{
cameraIDs: new Set(['camera']),
type: QueryType.Event,
},
]);
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
{
cameraIDs: new Set(['camera']),
type: QueryType.Recording,
},
]);
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({
canFavoriteEvents: true,
canFavoriteRecordings: true,
canSeek: true,
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
supportsTimeline: true,
});
return store;
};
export const createCameraManager = (): CameraManager => {
const cameraManager = mock<CameraManager>();
vi.mocked(cameraManager.getStore).mockReturnValue(createStore());
return cameraManager;
};
export const createAggregateCameraCapabilities = (
capabilities?: Partial<CameraManagerCapabilities>,
): CameraManagerCapabilities => {
return {
canFavoriteEvents: false,
canFavoriteRecordings: false,
canSeek: false,
supportsClips: false,
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
supportsPTZ: false,
...capabilities,
};
};
export const createCameraCapabilities = (
options?: Partial<CameraManagerCameraCapabilities>,
capabilities?: Partial<CameraManagerCameraCapabilities>,
): CameraManagerCameraCapabilities => {
return {
canFavoriteEvents: false,
@@ -208,7 +206,7 @@ export const createCameraCapabilities = (
supportsRecordings: false,
supportsSnapshots: false,
supportsTimeline: false,
...options,
...capabilities,
};
};
+53 -50
View File
@@ -4,7 +4,11 @@ import { mock } from 'vitest-mock-extended';
import { actionSchema } from '../../src/config/types';
import {
convertActionToFrigateCardCustomAction,
createFrigateCardCustomAction,
createFrigateCardCameraAction,
createFrigateCardDisplayModeAction,
createFrigateCardMediaPlayerAction,
createFrigateCardShowPTZAction,
createFrigateCardSimpleAction,
frigateCardHandleAction,
frigateCardHandleActionConfig,
frigateCardHasAction,
@@ -37,13 +41,24 @@ describe('convertActionToFrigateCardCustomAction', () => {
});
});
describe('createFrigateCardCustomAction', () => {
it('should create camera_select', () => {
describe('createFrigateCardSimpleAction', () => {
it('should create general action', () => {
expect(
createFrigateCardCustomAction('camera_select', {
camera: 'camera',
createFrigateCardSimpleAction('clips', {
cardID: 'card_id',
}),
).toEqual({
action: 'fire-dom-event',
frigate_card_action: 'clips',
card_id: 'card_id',
});
});
});
describe('createFrigateCardCameraAction', () => {
it('should create camera_select', () => {
expect(
createFrigateCardCameraAction('camera_select', 'camera', { cardID: 'card_id' }),
).toEqual({
action: 'fire-dom-event',
camera: 'camera',
@@ -51,16 +66,12 @@ describe('createFrigateCardCustomAction', () => {
card_id: 'card_id',
});
});
});
it('should not create camera_select without camera', () => {
expect(createFrigateCardCustomAction('camera_select')).toBeNull();
});
describe('createFrigateCardMediaPlayerAction', () => {
it('should create media_player', () => {
expect(
createFrigateCardCustomAction('media_player', {
media_player: 'device',
media_player_action: 'play',
createFrigateCardMediaPlayerAction('device', 'play', {
cardID: 'card_id',
}),
).toEqual({
@@ -71,37 +82,12 @@ describe('createFrigateCardCustomAction', () => {
card_id: 'card_id',
});
});
});
it('should not create media_player without player or action', () => {
expect(
createFrigateCardCustomAction('media_player', {
media_player_action: 'play',
}),
).toBeNull();
expect(
createFrigateCardCustomAction('media_player', {
media_player: 'device',
}),
).toBeNull();
});
it('should create general action', () => {
expect(
createFrigateCardCustomAction('clips', {
cardID: 'card_id',
}),
).toEqual({
action: 'fire-dom-event',
frigate_card_action: 'clips',
card_id: 'card_id',
});
});
describe('createFrigateCardDisplayModeAction', () => {
it('should create display mode action', () => {
expect(
createFrigateCardCustomAction('display_mode_select', {
display_mode: 'grid',
createFrigateCardDisplayModeAction('grid', {
cardID: 'card_id',
}),
).toEqual({
@@ -111,9 +97,20 @@ describe('createFrigateCardCustomAction', () => {
card_id: 'card_id',
});
});
});
it('should not create display mode action without display mode', () => {
expect(createFrigateCardCustomAction('display_mode_select')).toBeNull();
describe('createFrigateCardShowPTZAction', () => {
it('should create show PTZ action', () => {
expect(
createFrigateCardShowPTZAction(true, {
cardID: 'card_id',
}),
).toEqual({
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: true,
card_id: 'card_id',
});
});
});
@@ -124,13 +121,13 @@ describe('getActionConfigGivenAction', () => {
});
it('should not handle undefined arguments', () => {
expect(getActionConfigGivenAction()).toBeUndefined();
expect(getActionConfigGivenAction()).toBeNull();
});
it('should not handle unknown interactions', () => {
expect(
getActionConfigGivenAction('triple_poke', { triple_poke_action: action }),
).toBeUndefined();
).toBeNull();
});
it('should handle tap actions', () => {
@@ -178,17 +175,23 @@ describe('frigateCardHandleActionConfig', () => {
});
it('should handle simple case', () => {
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', action);
const hass = createHASS();
frigateCardHandleActionConfig(element, hass, {}, 'tap', action);
expect(handleActionConfig).toBeCalled();
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
});
it('should handle array case', () => {
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', [
action,
action,
action,
]);
const hass = createHASS();
frigateCardHandleActionConfig(element, hass, {}, 'tap', [action, action, action]);
expect(handleActionConfig).toBeCalledTimes(3);
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
});
it('should handle null case', () => {
const hass = createHASS();
frigateCardHandleActionConfig(element, hass, {}, 'tap', null);
expect(handleActionConfig).toBeCalledWith(element, hass, {}, undefined);
});
});
+5 -53
View File
@@ -1,11 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager.js';
import { CameraConfigs } from '../../src/camera-manager/types.js';
import { getAllDependentCameras, getCameraID } from '../../src/utils/camera.js';
import { createCameraConfig, createCameraManager } from '../test-utils.js';
vi.mock('../../src/camera-manager/manager.js');
import { getCameraID } from '../../src/utils/camera.js';
import { createCameraConfig } from '../test-utils.js';
describe('getCameraID', () => {
it('should get camera id with id', () => {
@@ -25,7 +20,9 @@ describe('getCameraID', () => {
expect(getCameraID(config)).toBe('foo');
});
it('should get camera id with go2rtc url and stream', () => {
const config = createCameraConfig({ go2rtc: { url: 'https://foo', stream: 'office' } });
const config = createCameraConfig({
go2rtc: { url: 'https://foo', stream: 'office' },
});
expect(getCameraID(config)).toBe('https://foo#office');
});
it('should get camera id with frigate camera_name', () => {
@@ -39,48 +36,3 @@ describe('getCameraID', () => {
expect(getCameraID(config)).toBe('');
});
});
describe('getAllDependentCameras', () => {
it('should return null without cameraManager', () => {
expect(getAllDependentCameras()).toBeNull();
});
it('should return null without cameraID', () => {
expect(getAllDependentCameras(mock<CameraManager>())).toBeNull();
});
it('should return dependent cameras', () => {
const cameraConfigs: CameraConfigs = new Map([
[
'one',
createCameraConfig({
dependencies: {
cameras: ['two', 'three'],
},
}),
],
['two', createCameraConfig({})],
]);
const cameraManager = createCameraManager({ configs: cameraConfigs });
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
new Set(['one', 'two']),
);
});
it('should return all cameras', () => {
const cameraConfigs: CameraConfigs = new Map([
[
'one',
createCameraConfig({
dependencies: {
all_cameras: true,
},
}),
],
['two', createCameraConfig({})],
]);
const cameraManager = createCameraManager({ configs: cameraConfigs });
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
new Set(['one', 'two']),
);
});
});
-1
View File
@@ -13,7 +13,6 @@ vi.mock('../../package.json', () => ({
gitDate: 'Wed, 6 Sep 2023 21:27:28 -0700',
},
}));
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/ha');
vi.mock('../../src/localize/localize.js');
vi.mock('../../src/utils/ha/device-registry');
+4 -6
View File
@@ -1,12 +1,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager.js';
import { downloadMedia, downloadURL } from '../../src/utils/download';
import { homeAssistantSignPath } from '../../src/utils/ha';
import { ViewMedia } from '../../src/view/media';
import { createCameraManager, createHASS } from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/ha');
const media = new ViewMedia('clip', 'camera-1');
@@ -73,7 +71,7 @@ describe('downloadMedia', () => {
it('should throw error when no media', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue(null);
vi.mocked(cameraManager.getMediaDownloadPath).mockResolvedValue(null);
expect(downloadMedia(createHASS(), cameraManager, media)).rejects.toThrow(
/No media to download/,
@@ -84,7 +82,7 @@ describe('downloadMedia', () => {
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
sign: true,
endpoint: 'foo',
});
@@ -98,7 +96,7 @@ describe('downloadMedia', () => {
it('should download media', async () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
sign: true,
endpoint: 'foo',
});
@@ -111,7 +109,7 @@ describe('downloadMedia', () => {
it('should download media without signing', async () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getMediaDownloadPath.mockResolvedValue({
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
sign: false,
endpoint: 'https://foo/',
});
+107 -33
View File
@@ -1,7 +1,7 @@
import add from 'date-fns/add';
import sub from 'date-fns/sub';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { CameraConfigs } from '../../src/camera-manager/types';
import { QueryType } from '../../src/camera-manager/types';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
@@ -14,12 +14,11 @@ import { EventMediaQueries } from '../../src/view/media-queries';
import {
createCameraManager,
createPerformanceConfig,
createStore,
createView,
TestViewMedia,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
const createElementListenForView = (): {
element: HTMLElement;
viewHandler: Mock<any, any>;
@@ -66,11 +65,10 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
it('should do nothing without camera config for selected camera', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
cameraManager,
createCameraManager(),
{},
createView(),
);
@@ -79,11 +77,10 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
it('should do nothing without camera configs for all cameras', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentEventsForCameraAndDependents(
elementHandler.element,
cameraManager,
createCameraManager(),
{},
createView(),
{
@@ -113,6 +110,19 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
it('should dispatch new view on success', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
{
type: QueryType.Event,
cameraIDs: new Set(['camera']),
},
]);
const mediaArray = [new ViewMedia('clip', 'camera')];
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
@@ -136,6 +146,19 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
vi.mocked(cameraManager.generateDefaultEventQueries).mockReturnValue([
{
type: QueryType.Event,
cameraIDs: new Set(['camera']),
},
]);
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
await changeViewToRecentEventsForCameraAndDependents(
@@ -150,6 +173,13 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
it('should respect media chunk size', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
await changeViewToRecentEventsForCameraAndDependents(
createElementListenForView().element,
@@ -178,6 +208,13 @@ describe('changeViewToRecentEventsForCameraAndDependents', () => {
['clips' as const, 'hasClip'],
])('%s', async (mediaType, queryParameter) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
await changeViewToRecentEventsForCameraAndDependents(
createElementListenForView().element,
@@ -206,12 +243,9 @@ describe('executeMediaQueryForView', () => {
});
it('should not execute empty queries', async () => {
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
expect(
await executeMediaQueryForView(
cameraManager,
createCameraManager(),
createView(),
new EventMediaQueries(),
),
@@ -219,24 +253,25 @@ describe('executeMediaQueryForView', () => {
});
it('should throw on failure', async () => {
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
const cameraManager = createCameraManager();
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(new Error());
await expect(
executeMediaQueryForView(
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set('camera'),
},
]),
),
).rejects.toThrowError();
});
it('should select time-based result', async () => {
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
const cameraManager = createCameraManager();
const now = new Date();
const mediaArray = [
@@ -249,9 +284,12 @@ describe('executeMediaQueryForView', () => {
const view = await executeMediaQueryForView(
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set('camera'),
},
]),
{
select: 'time',
targetTime: add(now, { seconds: 30 }),
@@ -264,8 +302,7 @@ describe('executeMediaQueryForView', () => {
});
it('should select nothing when time-based selection does not match', async () => {
const cameraConfigs: CameraConfigs = new Map();
const cameraManager = createCameraManager({ configs: cameraConfigs });
const cameraManager = createCameraManager();
const now = new Date();
const mediaArray = [
@@ -279,9 +316,12 @@ describe('executeMediaQueryForView', () => {
const view = await executeMediaQueryForView(
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set('camera'),
},
]),
{
select: 'time',
targetTime: sub(now, { seconds: 30 }),
@@ -292,6 +332,24 @@ describe('executeMediaQueryForView', () => {
expect(view?.queryResults?.getSelectedIndex()).toBe(2);
expect(view?.queryResults?.getResults()).toBe(mediaArray);
});
it('should select nothing when query returns null', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
expect(
await executeMediaQueryForView(
cameraManager,
createView(),
new EventMediaQueries([
{
type: QueryType.Event,
cameraIDs: new Set('camera'),
},
]),
),
).toBeNull();
});
});
// @vitest-environment jsdom
@@ -302,11 +360,10 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
it('should do nothing without camera config for selected camera', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
cameraManager,
createCameraManager(),
{},
createView(),
);
@@ -315,11 +372,10 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
it('should do nothing without camera configs for all cameras', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager({ configs: new Map() });
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
cameraManager,
createCameraManager(),
{},
createView(),
{
@@ -346,9 +402,22 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
it('should dispatch new view on success', async () => {
const elementHandler = createElementListenForView();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
const mediaArray = [new ViewMedia('recording', 'camera')];
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(mediaArray);
vi.mocked(cameraManager.generateDefaultRecordingQueries).mockReturnValue([
{
type: QueryType.Recording,
cameraIDs: new Set(['camera']),
},
]);
await changeViewToRecentRecordingForCameraAndDependents(
elementHandler.element,
@@ -366,6 +435,13 @@ describe('changeViewToRecentRecordingForCameraAndDependents', () => {
it('should respect media chunk size', async () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera',
},
]),
);
await changeViewToRecentRecordingForCameraAndDependents(
createElementListenForView().element,
@@ -402,9 +478,7 @@ describe('executeMediaQueryForViewWithErrorDispatching', () => {
elementHandler.element,
cameraManager,
createView(),
new EventMediaQueries(
cameraManager.generateDefaultEventQueries('camera') ?? undefined,
),
new EventMediaQueries([{ type: QueryType.Event, cameraIDs: new Set(['camera']) }]),
);
expect(elementHandler.viewHandler).not.toBeCalled();
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { FrigateCardPTZConfig, frigateCardPTZSchema } from '../../src/config/types';
import { hasUsablePTZ } from '../../src/utils/ptz';
import { createCameraCapabilities } from '../test-utils';
const createPTZConfig = (
config?: Partial<FrigateCardPTZConfig>,
): FrigateCardPTZConfig => {
return frigateCardPTZSchema.parse(config ?? {});
};
describe('hasUsablePTZ', () => {
it('should return true with manual actions', () => {
expect(
hasUsablePTZ(
createCameraCapabilities(),
createPTZConfig({
actions_left: {},
}),
),
).toBeTruthy();
});
it('should return true with capabilities', () => {
expect(
hasUsablePTZ(
createCameraCapabilities({
ptz: {},
}),
createPTZConfig(),
),
).toBeTruthy();
});
it('should return false with manual actions or capabilities', () => {
expect(hasUsablePTZ(createCameraCapabilities(), createPTZConfig())).toBeFalsy();
});
});