Add out-of-the-box Frigate PTZ support.
This commit is contained in:
1 parent
fa43d2e439
commit
e12912fa85
74 files changed
+3981
-1514
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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']),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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/',
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user