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

This commit is contained in:
Dermot Duffy
2023-10-29 14:10:41 -07:00
parent fa43d2e439
commit e12912fa85
74 changed files with 3981 additions and 1514 deletions
+201 -104
View File
@@ -12,11 +12,12 @@ import {
} from '../../src/components-lib/menu-controller';
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
import { FrigateCardMediaPlayer } from '../../src/types';
import { createFrigateCardCustomAction } from '../../src/utils/action';
import { createFrigateCardSimpleAction } from '../../src/utils/action';
import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
import {
createAggregateCameraCapabilities,
createCameraCapabilities,
createCameraConfig,
createCameraManager,
@@ -26,10 +27,10 @@ import {
createMediaCapabilities,
createMediaLoadedInfo,
createStateEntity,
createStore,
createView,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/media-player-controller.js');
vi.mock('../../src/card-controller/microphone-manager.js');
@@ -42,16 +43,15 @@ const calculateButtons = (
view?: View;
},
): MenuItem[] => {
let cameraManager: CameraManager | null = options?.cameraManager ?? null;
if (!cameraManager) {
cameraManager = createCameraManager();
}
return controller.calculateButtons(
options?.hass ?? createHASS(),
options?.config ?? createConfig(),
options?.cameraManager ??
createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
}),
cameraManager,
options?.view ?? createView({ camera: 'camera-1' }),
options,
);
@@ -79,8 +79,8 @@ describe('MenuButtonController', () => {
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('menu_toggle'),
hold_action: createFrigateCardCustomAction('diagnostics'),
tap_action: createFrigateCardSimpleAction('menu_toggle'),
hold_action: createFrigateCardSimpleAction('diagnostics'),
});
});
@@ -94,19 +94,17 @@ describe('MenuButtonController', () => {
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('default'),
hold_action: createFrigateCardCustomAction('diagnostics'),
tap_action: createFrigateCardSimpleAction('default'),
hold_action: createFrigateCardSimpleAction('diagnostics'),
});
});
it('should have cameras menu', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
});
mock<CameraManager>(cameraManager).getCameraMetadata.mockReturnValue({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
);
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
title: 'title',
icon: 'icon',
});
@@ -150,14 +148,12 @@ describe('MenuButtonController', () => {
});
it('should not have a cameras menu without a visible camera', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{ cameraID: 'camera-1', config: createCameraConfig({ hide: true }) },
]),
});
vi.mocked(cameraManager.getStore()).getVisibleCameras.mockReturnValue(new Map());
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toEqual(
@@ -170,12 +166,16 @@ describe('MenuButtonController', () => {
});
it('should have substream button with single dependency', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
});
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -193,12 +193,17 @@ describe('MenuButtonController', () => {
});
it('should have substream button selected with single dependency', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig({ dependencies: { cameras: ['camera-2'] } })],
['camera-2', createCameraConfig()],
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
});
);
const view = createView({
camera: 'camera-1',
context: {
@@ -227,29 +232,31 @@ describe('MenuButtonController', () => {
});
it('should have substream menu without substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
],
},
]),
});
);
// Return different metadata depending on the camera to test multiple code
// paths.
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
@@ -316,29 +323,30 @@ describe('MenuButtonController', () => {
});
it('should have substream menu with substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
],
},
]),
});
);
const view = createView({
camera: 'camera-1',
@@ -436,8 +444,8 @@ describe('MenuButtonController', () => {
it('should have styled clips menu button in clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -457,8 +465,8 @@ describe('MenuButtonController', () => {
it('should have unstyled clips menu button in non-clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -475,8 +483,8 @@ describe('MenuButtonController', () => {
it('should have styled snapshots menu button in snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -496,8 +504,8 @@ describe('MenuButtonController', () => {
it('should have unstyled snapshots menu button in non-snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -514,8 +522,8 @@ describe('MenuButtonController', () => {
it('should have styled recordings menu button in recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -535,8 +543,8 @@ describe('MenuButtonController', () => {
it('should have unstyled recordings menu button in non-recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
@@ -581,8 +589,8 @@ describe('MenuButtonController', () => {
it('should have styled timeline menu button in timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -601,8 +609,8 @@ describe('MenuButtonController', () => {
it('should have unstyled timeline menu button in non-timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
@@ -622,15 +630,15 @@ describe('MenuButtonController', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
selectedIndex: 0,
}),
});
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
@@ -653,15 +661,15 @@ describe('MenuButtonController', () => {
});
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
selectedIndex: 0,
}),
});
mock<CameraManager>(cameraManager).getMediaCapabilities.mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
@@ -906,16 +914,18 @@ describe('MenuButtonController', () => {
});
it('should have media players button', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
}),
],
},
]),
});
);
const mediaPlayerController = mock<MediaPlayerManager>();
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
mediaPlayerController.getMediaPlayers.mockReturnValue(['media_player.tv']);
@@ -961,16 +971,17 @@ describe('MenuButtonController', () => {
});
it('should disable media players button when entity not found', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
}),
],
},
]),
});
);
const mediaPlayerController = mock<MediaPlayerManager>();
mediaPlayerController.hasMediaPlayers.mockReturnValue(true);
mediaPlayerController.getMediaPlayers.mockReturnValue(['not_a_real_player']);
@@ -1109,7 +1120,13 @@ describe('MenuButtonController', () => {
'%s',
(displayMode: ViewDisplayMode) => {
const view = createView({ view: 'live', displayMode: displayMode });
expect(calculateButtons(controller, { view: view })).toContainEqual({
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
);
expect(
calculateButtons(controller, { cameraManager: cameraManager, view: view }),
).toContainEqual({
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
enabled: true,
priority: 50,
@@ -1118,6 +1135,7 @@ describe('MenuButtonController', () => {
displayMode === 'grid'
? 'Show single media viewer'
: 'Show media viewer for each camera in a grid',
style: displayMode === 'grid' ? { color: 'var(--primary-color, white)' } : {},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'display_mode_select',
@@ -1128,6 +1146,85 @@ describe('MenuButtonController', () => {
);
});
describe('should have show ptz button', () => {
it('when the selected camera is not PTZ enabled', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities(),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
it('when the selected camera is PTZ enabled', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities({ ptz: {} }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
it('when the context has PTZ visiblity turned off', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities({ ptz: {} }),
);
const view = createView({
camera: 'camera-1',
context: { live: { ptzVisible: false } },
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: true,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
});
it('should handle dynamic buttons', () => {
const button: MenuItem = {
...dynamicButton,
+316
View File
@@ -0,0 +1,316 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PTZController } from '../../src/components-lib/ptz-controller';
import {
FrigateCardPTZConfig,
frigateCardPTZSchema,
PTZControlAction,
} from '../../src/config/types';
import {
frigateCardHandleActionConfig,
getActionConfigGivenAction,
} from '../../src/utils/action.js';
import {
createCameraCapabilities,
createCameraManager,
createHASS,
} from '../test-utils';
vi.mock('../../src/utils/action.js');
const createConfig = (config?: Partial<FrigateCardPTZConfig>): FrigateCardPTZConfig => {
return frigateCardPTZSchema.parse({
...config,
});
};
// @vitest-environment jsdom
describe('PTZController', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('should be creatable', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller).toBeTruthy();
});
it('should get config creatable', () => {
const controller = new PTZController(document.createElement('div'));
const config = createConfig();
controller.setConfig(config);
expect(controller.getConfig()).toBe(config);
});
describe('should set element attributes', () => {
describe('orientation', () => {
describe('with config', () => {
it.each([['horizontal' as const], ['vertical' as const]])(
'%s',
(orientation: 'horizontal' | 'vertical') => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ orientation: orientation }));
expect(element.getAttribute('data-orientation')).toBe(orientation);
},
);
});
it('without config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig();
expect(element.getAttribute('data-orientation')).toBe('horizontal');
});
});
describe('position', () => {
describe('with config', () => {
it.each([
['top-left' as const],
['top-right' as const],
['bottom-left' as const],
['bottom-right' as const],
])(
'%s',
(position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ position: position }));
expect(element.getAttribute('data-position')).toBe(position);
},
);
});
it('without config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig();
expect(element.getAttribute('data-position')).toBe('bottom-right');
});
});
it('with style in config', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
controller.setConfig(createConfig({ style: { transform: 'none', left: '50%' } }));
expect(element.getAttribute('style')).toBe('transform:none;left:50%');
});
});
describe('should respect mode', () => {
it('off', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller.shouldDisplay()).toBeFalsy();
});
it('off but forced on', () => {
const controller = new PTZController(document.createElement('div'));
controller.setForceVisibility(true);
// No actions, no rendering, no matter what.
expect(controller.shouldDisplay()).toBeFalsy();
});
it('on without actions', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on' }));
expect(controller.shouldDisplay()).toBeFalsy();
});
it('on with actions', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.shouldDisplay()).toBeTruthy();
});
it('on with actions but forced off', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
controller.setCamera(createCameraManager(), 'camera.office');
controller.setForceVisibility(false);
expect(controller.shouldDisplay()).toBeFalsy();
});
});
describe('should get PTZ actions', () => {
it('without config', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller.getPTZActions('left')).toBeNull();
});
describe('using defaults', () => {
describe('with continous PTZ', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
panTilt: ['continuous'],
zoom: ['continuous'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
start_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
ptz_phase: 'start',
},
end_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
ptz_phase: 'stop',
},
});
});
});
describe('with relative PTZ', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
panTilt: ['relative'],
zoom: ['relative'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: actionName,
},
});
});
});
it('home', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCameraCapabilities({
ptz: {
presets: ['preset-foo'],
},
}),
);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions('home')).toEqual({
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'preset',
ptz_preset: 'preset-foo',
},
});
});
});
describe('using config', () => {
it.each([
['left' as const],
['right' as const],
['up' as const],
['down' as const],
['zoom_in' as const],
['zoom_out' as const],
])('%s', (actionName: PTZControlAction) => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(
createConfig({
[`actions_${actionName}`]: {
argument: actionName,
},
}),
);
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.getPTZActions(actionName)).toEqual({
argument: actionName,
});
});
it('home', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(
createConfig({
actions_home: {
argument: 'home',
},
}),
);
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.getPTZActions('home')).toEqual({
argument: 'home',
});
});
});
});
describe('should handle action', () => {
it('without hass or action', () => {
const controller = new PTZController(document.createElement('div'));
controller.setHASS();
controller.setCamera();
controller.handleAction(
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
);
expect(frigateCardHandleActionConfig).not.toBeCalled();
});
it('with action', () => {
const element = document.createElement('div');
const controller = new PTZController(element);
const hass = createHASS();
controller.setHASS(hass);
controller.setConfig(
createConfig({
[`actions_left`]: {},
}),
);
const tapAction = {
action: 'none' as const,
};
const actionsConfig = {
tap_action: tapAction,
};
vi.mocked(getActionConfigGivenAction).mockReturnValue(tapAction);
controller.handleAction(
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
actionsConfig,
);
expect(frigateCardHandleActionConfig).toBeCalledWith(
element,
hass,
actionsConfig,
'tap',
actionsConfig.tap_action,
);
});
});
});