Files
advanced-camera-card/tests/utils/menu-controller.test.ts
T

1234 lines
37 KiB
TypeScript

import { HomeAssistant } from 'custom-card-helpers';
import screenfull from 'screenfull';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManager } from '../../src/camera-manager/manager';
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
import {
FrigateCardConfig,
FrigateCardMediaPlayer,
MediaLoadedInfo,
MenuButton,
ViewDisplayMode,
} from '../../src/types';
import { createFrigateCardCustomAction } from '../../src/utils/action';
import { MenuButtonController } from '../../src/utils/menu-controller';
import { MicrophoneController } from '../../src/utils/microphone';
import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
import {
createCameraCapabilities,
createCameraConfig,
createCameraManager,
createConfig,
createHASS,
createMediaCapabilities,
createMediaLoadedInfo,
createStateEntity,
createView,
} from '../test-utils';
vi.mock('../../src/camera-manager/manager.js');
vi.mock('../../src/utils/microphone');
vi.mock('screenfull');
const calculateButtons = (
controller: MenuButtonController,
options?: {
hass?: HomeAssistant;
config?: FrigateCardConfig;
cameraManager?: CameraManager;
view?: View;
expanded?: boolean;
currentMediaLoadedInfo?: MediaLoadedInfo | null;
mediaPlayers?: string[];
cameraURL?: string | null;
microphoneController?: MicrophoneController;
},
): MenuButton[] => {
return controller.calculateButtons(
options?.hass ?? createHASS(),
options?.config ?? createConfig(),
options?.cameraManager ??
createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
}),
options?.view ?? createView({ camera: 'camera-1' }),
options?.expanded ?? false,
{
currentMediaLoadedInfo: options?.currentMediaLoadedInfo,
mediaPlayers: options?.mediaPlayers,
cameraURL: options?.cameraURL,
microphoneController: options?.microphoneController,
},
);
};
// @vitest-environment jsdom
describe('MenuButtonController', () => {
let controller: MenuButtonController;
const dynamicButton: MenuButton = {
type: 'custom:frigate-card-menu-icon',
icon: 'mdi:alpha-a-circle',
title: 'Dynamic button',
};
beforeEach(() => {
vi.resetAllMocks();
controller = new MenuButtonController();
});
it('should have frigate menu button with hidden menu style', () => {
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'frigate',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('menu_toggle'),
hold_action: createFrigateCardCustomAction('diagnostics'),
});
});
it('should have frigate menu button without hidden menu style', () => {
const buttons = calculateButtons(controller, {
config: createConfig({ menu: { style: 'overlay' } }),
});
expect(buttons).toContainEqual({
icon: 'frigate',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Frigate menu / Default view',
tap_action: createFrigateCardCustomAction('default'),
hold_action: createFrigateCardCustomAction('diagnostics'),
});
});
it('should have cameras menu', () => {
const cameraManager = createCameraManager({
configs: new Map([
['camera-1', createCameraConfig()],
['camera-2', createCameraConfig()],
]),
});
mock<CameraManager>(cameraManager).getCameraMetadata.mockReturnValue({
title: 'title',
icon: 'icon',
});
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:video-switch',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-submenu',
title: 'Cameras',
items: [
{
enabled: true,
icon: 'icon',
entity: undefined,
state_color: true,
title: 'title',
selected: true,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera-1',
},
},
{
enabled: true,
icon: 'icon',
entity: undefined,
state_color: true,
title: 'title',
selected: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera-2',
},
},
],
});
});
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 buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:video-input-component',
style: {},
title: 'Substream(s)',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_on',
},
});
});
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 view = createView({
camera: 'camera-1',
context: {
live: {
overrides: new Map([['camera-1', 'camera-2']]),
},
},
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).toContainEqual({
icon: 'mdi:video-input-component',
style: { color: 'var(--primary-color, white)' },
title: 'Substream(s)',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_off',
},
});
});
it('should have substream menu without substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
camera_entity: 'camera.3',
}),
],
]),
});
// Return different metadata depending on the camera to test multiple code
// paths.
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
(_hass: unknown, cameraID: string): CameraManagerCameraMetadata | null => {
return cameraID === 'camera-1'
? {
title: 'title',
icon: 'icon',
}
: null;
},
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:video-input-component',
title: 'Substream(s)',
style: {},
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-submenu',
items: [
{
enabled: true,
icon: 'icon',
entity: 'camera.1',
state_color: true,
title: 'title',
selected: true,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-1',
},
},
{
enabled: true,
icon: undefined,
entity: 'camera.2',
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-2',
},
},
{
enabled: true,
icon: undefined,
entity: 'camera.3',
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-3',
},
},
],
});
});
it('should have substream menu with substream on with multiple dependencies', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
],
[
'camera-2',
createCameraConfig({
camera_entity: 'camera.2',
}),
],
[
'camera-3',
createCameraConfig({
camera_entity: 'camera.3',
}),
],
]),
});
const view = createView({
camera: 'camera-1',
context: {
live: {
overrides: new Map([['camera-1', 'camera-2']]),
},
},
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).toContainEqual({
icon: 'mdi:video-input-component',
title: 'Substream(s)',
style: { color: 'var(--primary-color, white)' },
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-submenu',
items: [
{
enabled: true,
icon: undefined,
entity: 'camera.1',
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-1',
},
},
{
enabled: true,
icon: undefined,
entity: 'camera.2',
state_color: true,
title: undefined,
// camera-2 is selected in this test scenario because of the view
// override.
selected: true,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-2',
},
},
{
enabled: true,
icon: undefined,
entity: 'camera.3',
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'camera-3',
},
},
],
});
});
it('should have styled live menu button in live view', () => {
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'mdi:cctv',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Live view',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' },
});
});
it('should have unstyled live menu button in non-live views', () => {
const view = createView({ view: 'clips' });
const buttons = calculateButtons(controller, { view: view });
expect(buttons).toContainEqual({
icon: 'mdi:cctv',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Live view',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' },
});
});
it('should have styled clips menu button in clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'clips' }),
});
expect(buttons).toContainEqual({
icon: 'mdi:filmstrip',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Clips gallery',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'clip' },
});
});
it('should have unstyled clips menu button in non-clips view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsClips: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:filmstrip',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Clips gallery',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'clip' },
});
});
it('should have styled snapshots menu button in snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'snapshots' }),
});
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Snapshots gallery',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'snapshots' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'snapshot' },
});
});
it('should have unstyled snapshots menu button in non-snapshots view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Snapshots gallery',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'snapshots' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'snapshot' },
});
});
it('should have styled recordings menu button in recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'recordings' }),
});
expect(buttons).toContainEqual({
icon: 'mdi:album',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Recordings gallery',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'recordings' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'recording' },
});
});
it('should have unstyled recordings menu button in non-recordings view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:album',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Recordings gallery',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'recordings' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'recording' },
});
});
it('should have styled image menu button in image view', () => {
const buttons = calculateButtons(controller, {
view: createView({ view: 'image' }),
});
expect(buttons).toContainEqual({
icon: 'mdi:image',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Static image',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' },
});
});
it('should have unstyled image menu button in non-image view', () => {
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'mdi:image',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Static image',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' },
});
});
it('should have styled timeline menu button in timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'timeline' }),
});
expect(buttons).toContainEqual({
icon: 'mdi:chart-gantt',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Timeline view',
style: { color: 'var(--primary-color, white)' },
tap_action: { action: 'fire-dom-event', frigate_card_action: 'timeline' },
});
});
it('should have unstyled timeline menu button in non-timeline view', () => {
const cameraManager = createCameraManager();
mock<CameraManager>(cameraManager).getAggregateCameraCapabilities.mockReturnValue(
createCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:chart-gantt',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Timeline view',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'timeline' },
});
});
it('should have download menu button', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
const cameraManager = createCameraManager();
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,
});
expect(buttons).toContainEqual({
icon: 'mdi:download',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Download',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'download' },
});
});
it('should not have download menu button when being casted', () => {
vi.stubGlobal('navigator', {
userAgent:
'Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/114.0.0.0 Safari/537.36 CrKey/1.56.500000',
});
const cameraManager = createCameraManager();
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,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Download' })]),
);
});
it('should have camera UI button', () => {
const buttons = calculateButtons(controller, {
cameraURL: 'http://frigate.domain',
});
expect(buttons).toContainEqual({
icon: 'mdi:web',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Camera user interface',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'camera_ui' },
});
});
it('should have microphone button', () => {
const microphoneController = new MicrophoneController();
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
},
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:microphone',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Microphone',
style: {
animation: 'pulse 3s infinite',
color: 'var(--error-color, white)',
},
start_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_unmute',
},
end_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_mute',
},
});
});
it('should not have microphone button when media does not support it', () => {
const microphoneController = new MicrophoneController();
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: false,
},
}),
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Microphone' })]),
);
});
it('should have microphone button when microphone forbidden', () => {
const microphoneController = new MicrophoneController();
mock<MicrophoneController>(microphoneController).isForbidden.mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
},
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:microphone-message-off',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Microphone',
style: {},
});
});
it('should have microphone button when microphone muted', () => {
const microphoneController = new MicrophoneController();
mock<MicrophoneController>(microphoneController).isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
},
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:microphone-off',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Microphone',
style: {},
start_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_unmute',
},
end_tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_mute',
},
});
});
it('should have microphone button when microphone muted with toggle type', () => {
const microphoneController = new MicrophoneController();
mock<MicrophoneController>(microphoneController).isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
},
}),
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:microphone-off',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Microphone',
style: {},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_unmute',
},
});
});
it('should have microphone button when microphone unmuted with toggle type', () => {
const microphoneController = new MicrophoneController();
mock<MicrophoneController>(microphoneController).isMuted.mockReturnValue(false);
const buttons = calculateButtons(controller, {
microphoneController: microphoneController,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
},
}),
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:microphone',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Microphone',
style: {
animation: 'pulse 3s infinite',
color: 'var(--error-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'microphone_mute',
},
});
});
it('should have fullscreen button', () => {
// Need to write a readonly property.
Object.defineProperty(screenfull, 'isEnabled', { value: true });
vi.stubGlobal('navigator', { userAgent: 'foo' });
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'mdi:fullscreen',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Fullscreen',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
style: {},
});
});
it('should have unfullscreen', () => {
// Need to write a readonly property.
Object.defineProperty(screenfull, 'isEnabled', { value: true });
Object.defineProperty(screenfull, 'isFullscreen', { value: true });
vi.stubGlobal('navigator', { userAgent: 'foo' });
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'mdi:fullscreen-exit',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Fullscreen',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
style: { color: 'var(--primary-color, white)' },
});
});
it('should have expand button', () => {
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
icon: 'mdi:arrow-expand-all',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Expand',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'expand' },
style: {},
});
});
it('should have unexpand button', () => {
const buttons = calculateButtons(controller, { expanded: true });
expect(buttons).toContainEqual({
icon: 'mdi:arrow-collapse-all',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Expand',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'expand' },
style: { color: 'var(--primary-color, white)' },
});
});
it('should have media players button', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
camera_entity: 'camera.1',
}),
],
]),
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
mediaPlayers: ['media_player.tv'],
hass: createHASS({
'media_player.tv': createStateEntity({ entity_id: 'media_player.tv' }),
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:cast',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-submenu',
title: 'Send to media player',
items: [
{
enabled: true,
selected: false,
icon: 'mdi:cast',
entity: 'media_player.tv',
state_color: false,
title: 'media_player.tv',
disabled: false,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player: 'media_player.tv',
media_player_action: 'play',
},
hold_action: {
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player: 'media_player.tv',
media_player_action: 'stop',
},
},
],
});
});
it('should disable media players button when entity not found', () => {
const cameraManager = createCameraManager({
configs: new Map([
[
'camera-1',
createCameraConfig({
camera_entity: 'camera.1',
}),
],
]),
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
mediaPlayers: ['player'],
hass: createHASS(),
});
expect(buttons).toContainEqual({
icon: 'mdi:cast',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-submenu',
title: 'Send to media player',
items: [
{
enabled: true,
selected: false,
icon: 'mdi:bookmark',
entity: 'player',
state_color: false,
title: 'player',
disabled: true,
},
],
});
});
it('should have pause button', () => {
const player = mock<FrigateCardMediaPlayer>();
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supportsPause: true,
},
player: player,
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:pause',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Play / Pause',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'pause' },
});
});
it('should have play button', () => {
const player = mock<FrigateCardMediaPlayer>();
player.isPaused.mockReturnValue(true);
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supportsPause: true,
},
player: player,
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:play',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Play / Pause',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'play' },
});
});
it('should have mute button', () => {
const player = mock<FrigateCardMediaPlayer>();
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
hasAudio: true,
},
player: player,
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:volume-high',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Mute / Unmute',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'mute' },
});
});
it('should have unmute button', () => {
const player = mock<FrigateCardMediaPlayer>();
player.isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
hasAudio: true,
},
player: player,
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:volume-off',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Mute / Unmute',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'unmute' },
});
});
it('should have screenshot button', () => {
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
player: mock<FrigateCardMediaPlayer>(),
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:monitor-screenshot',
enabled: false,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Screenshot',
tap_action: { action: 'fire-dom-event', frigate_card_action: 'screenshot' },
});
});
describe('should have grid button when display mode is', () => {
it.each([['single' as const], ['grid' as const]])(
'%s',
(displayMode: ViewDisplayMode) => {
const view = createView({ view: 'live', displayMode: displayMode });
expect(calculateButtons(controller, { view: view })).toContainEqual({
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Display mode',
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'display_mode_select',
display_mode: displayMode === 'single' ? 'grid' : 'single',
},
});
},
);
});
it('should handle dynamic buttons', () => {
const button: MenuButton = {
...dynamicButton,
style: {},
};
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual(button);
controller.removeDynamicMenuButton(button);
expect(calculateButtons(controller)).not.toContainEqual(button);
});
it('should not set style for dynamic button with stock action', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: { action: 'navigate', navigation_path: 'foo' },
};
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: {},
});
});
it('should not set style for dynamic button with non-Frigate fire-dom-event action', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: { action: 'fire-dom-event' },
};
controller.addDynamicMenuButton(button);
controller.addDynamicMenuButton(dynamicButton);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: {},
});
});
it('should set style for dynamic button with Frigate view action', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' },
};
const view = createView({ view: 'clips' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: { color: 'var(--primary-color, white)' },
});
});
it('should set style for dynamic button with Frigate default action', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'default' },
};
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: { color: 'var(--primary-color, white)' },
});
});
it('should set style for dynamic button with fullscreen action', () => {
// Need to write a readonly property.
Object.defineProperty(screenfull, 'isEnabled', { value: true });
Object.defineProperty(screenfull, 'isFullscreen', { value: true });
const button: MenuButton = {
...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
};
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: { color: 'var(--primary-color, white)' },
});
});
it('should set style for dynamic button with camera_select action', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'foo',
},
};
const view = createView({ camera: 'foo' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: { color: 'var(--primary-color, white)' },
});
});
it('should set style for dynamic button with array of actions', () => {
const button: MenuButton = {
...dynamicButton,
tap_action: [
{ action: 'fire-dom-event' },
{ action: 'fire-dom-event', frigate_card_action: 'clips' },
],
};
const view = createView({ camera: 'clips' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: {},
});
});
});