Add out-of-the-box Frigate PTZ support.
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user