Files
advanced-camera-card/tests/components-lib/menu-button-controller.test.ts
T

2517 lines
76 KiB
TypeScript

import { isEqual } from 'lodash-es';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Capabilities } from '../../src/camera-manager/capabilities.js';
import { CameraManager } from '../../src/camera-manager/manager.js';
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types.js';
import { FoldersManager } from '../../src/card-controller/folders/manager.js';
import { FolderQuery } from '../../src/card-controller/folders/types';
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager.js';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager.js';
import { MicrophoneManager } from '../../src/card-controller/microphone-manager.js';
import { ViewManager } from '../../src/card-controller/view/view-manager.js';
import {
MenuButtonController,
MenuButtonControllerOptions,
} from '../../src/components-lib/menu-button-controller.js';
import { AdvancedCameraCardView } from '../../src/config/schema/common/const.js';
import { ViewDisplayMode } from '../../src/config/schema/common/display.js';
import { MenuItem } from '../../src/config/schema/elements/custom/menu/types.js';
import { AdvancedCameraCardConfig } from '../../src/config/schema/types.js';
import { HomeAssistant } from '../../src/ha/types.js';
import { QuerySource } from '../../src/query-source';
import { MediaPlayerController, PTZMovementType } from '../../src/types.js';
import { createGeneralAction, createViewAction } from '../../src/utils/action.js';
import { ViewMedia, ViewMediaType } from '../../src/view/item.js';
import { QueryResults } from '../../src/view/query-results.js';
import { UnifiedQuery } from '../../src/view/unified-query.js';
import {
getCameraIDsWithCapabilityForView,
isViewSupported,
} from '../../src/view/view-support.js';
import { View } from '../../src/view/view.js';
import {
createCameraConfig,
createCameraManager,
createCapabilities,
createConfig,
createFolder,
createHASS,
createMediaCapabilities,
createMediaLoadedInfo,
createStateEntity,
createStore,
createView,
TestViewMedia,
} from '../test-utils.js';
vi.mock('../../src/view/view-support.js');
vi.mock('../../src/utils/media-player-controller.js');
vi.mock('../../src/card-controller/microphone-manager.js');
const calculateButtons = (
controller: MenuButtonController,
options?: MenuButtonControllerOptions & {
hass?: HomeAssistant;
config?: AdvancedCameraCardConfig;
cameraManager?: CameraManager;
foldersManager?: FoldersManager;
view?: View | null;
viewManager?: ViewManager;
},
): MenuItem[] => {
let cameraManager: CameraManager | null = options?.cameraManager ?? null;
if (!cameraManager) {
cameraManager = createCameraManager();
}
return controller.calculateButtons(
options?.hass ?? createHASS(),
options?.config ?? createConfig(),
cameraManager,
options?.foldersManager ?? mock<FoldersManager>(),
{
...options,
view:
options?.view === undefined ? createView({ camera: 'camera-1' }) : options.view,
},
);
};
// @vitest-environment jsdom
describe('MenuButtonController', () => {
let controller: MenuButtonController;
const dynamicButton: MenuItem = {
type: 'custom:advanced-camera-card-menu-icon',
icon: 'mdi:alpha-a-circle',
title: 'Dynamic button',
};
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(isViewSupported).mockReturnValue(true);
controller = new MenuButtonController();
});
describe('should have iris menu button', () => {
it('with hidden menu style', () => {
const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
icon: 'iris',
enabled: true,
permanent: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Iris / Default View / Unhide menu',
tap_action: createGeneralAction('menu_toggle'),
hold_action: createViewAction('diagnostics'),
});
});
it('without hidden menu style', () => {
const buttons = calculateButtons(controller, {
config: createConfig({ menu: { style: 'overlay' } }),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
icon: 'iris',
enabled: true,
permanent: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Iris / Default View / Unhide menu',
tap_action: createGeneralAction('default'),
hold_action: createViewAction('diagnostics'),
});
});
});
describe('should have cameras menu', () => {
it('should have cameras menu with multiple cameras', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{ cameraID: 'camera-1', capabilities: createCapabilities({ menu: true }) },
{ cameraID: 'camera-2', capabilities: createCapabilities({ menu: true }) },
{ cameraID: 'camera-3', capabilities: createCapabilities({ menu: false }) },
]),
);
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
title: 'title',
icon: {
icon: 'icon',
},
});
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:video-switch',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-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',
advanced_camera_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',
advanced_camera_card_action: 'camera_select',
camera: 'camera-2',
},
},
],
});
});
it('should not have cameras menu with <= 1 camera', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{ cameraID: 'camera-1', capabilities: createCapabilities({ menu: true }) },
{ cameraID: 'camera-3', capabilities: createCapabilities({ menu: false }) },
]),
);
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
title: 'title',
icon: {
icon: 'icon',
},
});
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
title: 'Cameras',
}),
]),
);
});
});
describe('should have substream button', () => {
it('with no view', () => {
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(),
view: null,
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Substream(s)',
}),
);
});
it('with no dependency', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ substream: true }),
},
]),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Substream(s)',
}),
);
});
it('with single dependency', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
capabilities: createCapabilities({ substream: true }),
},
]),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:video-input-component',
style: {},
title: 'Substream(s)',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_on',
},
});
});
it('with substream selected and single dependency', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
capabilities: createCapabilities({ substream: true }),
},
]),
);
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({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:video-input-component',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
title: 'Substream(s)',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_off',
},
});
});
it('with substream unselected and multiple dependencies', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
capabilities: createCapabilities({ substream: true }),
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
capabilities: createCapabilities({ substream: true }),
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
capabilities: createCapabilities({ substream: true }),
},
]),
);
// Return different metadata depending on the camera to test multiple code
// paths.
mock<CameraManager>(cameraManager).getCameraMetadata.mockImplementation(
(cameraID: string): CameraManagerCameraMetadata | null => {
return cameraID === 'camera-1'
? {
title: 'title',
icon: {
icon: 'icon',
entity: 'entity',
},
}
: null;
},
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:video-input-component',
title: 'Substream(s)',
style: {},
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
items: [
{
enabled: true,
icon: 'icon',
entity: 'entity',
state_color: true,
title: 'title',
selected: true,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-1',
},
},
{
enabled: true,
icon: undefined,
entity: undefined,
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-2',
},
},
{
enabled: true,
icon: undefined,
entity: undefined,
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-3',
},
},
],
});
});
it('with substream selected and with multiple dependencies', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({
camera_entity: 'camera.1',
dependencies: { cameras: ['camera-2', 'camera-3'] },
}),
capabilities: createCapabilities({ substream: true }),
},
{
cameraID: 'camera-2',
config: createCameraConfig({
camera_entity: 'camera.2',
}),
capabilities: createCapabilities({ substream: true }),
},
{
cameraID: 'camera-3',
config: createCameraConfig({
camera_entity: 'camera.3',
}),
capabilities: createCapabilities({ substream: true }),
},
]),
);
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({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:video-input-component',
title: 'Substream(s)',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
items: [
{
enabled: true,
icon: undefined,
entity: undefined,
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-1',
},
},
{
enabled: true,
icon: undefined,
entity: undefined,
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',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-2',
},
},
{
enabled: true,
icon: undefined,
entity: undefined,
state_color: true,
title: undefined,
selected: false,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'live_substream_select',
camera: 'camera-3',
},
},
],
});
});
});
describe('should have live menu button', () => {
it('when in live view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:cctv',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Live view',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'live' },
});
});
it('when not in live view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:cctv',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Live view',
style: {},
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'live' },
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Live view',
}),
);
});
});
describe('should have clips menu button', () => {
it('when in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:filmstrip',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Clips gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'clip' },
});
});
it('when not in clips view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:filmstrip',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Clips gallery',
style: {},
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'clip' },
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Clips gallery' })]),
);
});
});
describe('should have snapshots menu button', () => {
it('when in snapshots view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockImplementation((view) => view !== 'reviews');
const buttons = calculateButtons(controller, {
view: createView({ view: 'snapshots' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:camera',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Snapshots gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'snapshots',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'snapshot',
},
});
});
it('when not in snapshots view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:camera',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Snapshots gallery',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'snapshots',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'snapshot',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Snapshots gallery' }),
]),
);
});
});
describe('should have reviews menu button', () => {
it('when in reviews view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'reviews' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:play-box-edit-outline',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Reviews gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'reviews',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'review',
},
});
});
it('when not in reviews view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:play-box-edit-outline',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Reviews gallery',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'reviews',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'review',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Reviews gallery' })]),
);
});
});
describe('should have gallery menu button', () => {
it('when in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'gallery' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:play-box-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'gallery',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media',
},
});
});
it('when not in gallery view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:play-box-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Gallery',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'gallery',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Gallery' })]),
);
});
});
describe('should have recordings menu button', () => {
it('when in recordings view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'recordings' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:album',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Recordings gallery',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'recordings',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'recording',
},
});
});
it('when not in recordings view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:album',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Recordings gallery',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'recordings',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'recording',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Recordings gallery' }),
]),
);
});
});
describe('should have image menu button', () => {
it('when in image view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'image' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:image',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Static image',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'image' },
});
});
it('when not in image view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:image',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Static image',
style: {},
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'image' },
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Static image',
}),
);
});
});
describe('should have timeline button', () => {
it('when in timeline view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'timeline' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:chart-gantt',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Timeline view',
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'timeline',
},
});
});
it('when not in timeline view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:chart-gantt',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Timeline view',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'timeline',
},
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(isViewSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Timeline view' })]),
);
});
});
describe('should have download button', () => {
it('when media available', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
view: 'media',
queryResults: new QueryResults({
results: [
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-1',
}),
],
selectedIndex: 0,
}),
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:download',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Download',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'download',
},
});
});
it('not 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();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new QueryResults({
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
selectedIndex: 0,
}),
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Download' })]),
);
});
it('not in a non-media view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getMediaCapabilities).mockReturnValue(
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
view: 'live',
queryResults: new QueryResults({
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
selectedIndex: 0,
}),
});
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, {
showCameraUIButton: true,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:web',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Camera user interface',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'camera_ui' },
});
});
describe('should have microphone button', () => {
it('when camera has 2-way-audio capability', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {
animation: 'pulse 3s infinite',
color: 'var(--advanced-camera-card-menu-button-critical-color)',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_unmute',
},
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_mute',
},
});
});
it('when camera does not have 2-way-audio capability', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([{ cameraID: 'camera-1', capabilities: createCapabilities() }]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Microphone' })]),
);
});
it('with forbidden microphone', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone-message-off',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {},
});
});
it('with muted microphone', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone-off',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_unmute',
},
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_mute',
},
});
});
it('with unsupported microphone', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(false);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone-message-off',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {},
});
});
it('with muted toggle type microphone', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone-off',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_unmute',
},
});
});
it('with unmuted toggle type microphone', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:microphone',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Microphone',
style: {
animation: 'pulse 3s infinite',
color: 'var(--advanced-camera-card-menu-button-critical-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'microphone_mute',
},
});
});
});
describe('should have fullscreen button', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('when not in fullscreen mode', () => {
// Need to write a readonly property.
vi.stubGlobal('navigator', { userAgent: 'foo' });
const fullscreenManager = mock<FullscreenManager>();
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(false);
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, { fullscreenManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:fullscreen',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Fullscreen',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'fullscreen',
},
style: {},
});
});
it('when in fullscreen mode', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
const fullscreenManager = mock<FullscreenManager>();
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(true);
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, { fullscreenManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:fullscreen-exit',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Fullscreen',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'fullscreen',
},
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('when not supported', () => {
// Need to write a readonly property.
vi.stubGlobal('navigator', { userAgent: 'foo' });
const fullscreenManager = mock<FullscreenManager>();
vi.mocked(fullscreenManager.isSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, { fullscreenManager });
expect(buttons).not.toContainEqual(
expect.objectContaining({ title: 'Fullscreen' }),
);
});
});
describe('should have expand button', () => {
it('when not expanded', () => {
const buttons = calculateButtons(controller, { inExpandedMode: false });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:arrow-expand-all',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Expand',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'expand' },
style: {},
});
});
it('when expanded', () => {
const buttons = calculateButtons(controller, { inExpandedMode: true });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:arrow-collapse-all',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Expand',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'expand' },
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
});
describe('should have media players button', () => {
it('with media players', () => {
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']);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
mediaPlayerController: mediaPlayerController,
hass: createHASS({
'media_player.tv': createStateEntity({ entity_id: 'media_player.tv' }),
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:cast',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Send to media player',
items: [
{
enabled: true,
selected: false,
entity: 'media_player.tv',
state_color: false,
title: 'media_player.tv',
disabled: false,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media_player',
media_player: 'media_player.tv',
media_player_action: 'play',
},
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'media_player',
media_player: 'media_player.tv',
media_player_action: 'stop',
},
},
],
});
});
it('when entity not found', () => {
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']);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
mediaPlayerController: mediaPlayerController,
hass: createHASS(),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:cast',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Send to media player',
items: [
{
enabled: true,
selected: false,
entity: 'not_a_real_player',
state_color: false,
title: 'not_a_real_player',
disabled: true,
},
],
});
});
});
it('should have pause button', () => {
const mediaPlayerController = mock<MediaPlayerController>();
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supportsPause: true,
},
mediaPlayerController,
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:pause',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Play / Pause',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'pause' },
});
});
it('should have play button', () => {
const mediaPlayerController = mock<MediaPlayerController>();
mediaPlayerController.isPaused.mockReturnValue(true);
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supportsPause: true,
},
mediaPlayerController,
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:play',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Play / Pause',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'play' },
});
});
it('should have mute button', () => {
const mediaPlayerController = mock<MediaPlayerController>();
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
hasAudio: true,
},
mediaPlayerController,
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:volume-high',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Mute / Unmute',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'mute' },
});
});
it('should have unmute button', () => {
const mediaPlayerController = mock<MediaPlayerController>();
mediaPlayerController.isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
hasAudio: true,
},
mediaPlayerController,
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:volume-off',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Mute / Unmute',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'unmute' },
});
});
it('should have screenshot button', () => {
const buttons = calculateButtons(controller, {
currentMediaLoadedInfo: createMediaLoadedInfo({
mediaPlayerController: mock<MediaPlayerController>(),
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:monitor-screenshot',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Screenshot',
tap_action: {
action: 'fire-dom-event',
advanced_camera_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({
camera: 'camera-1',
view: 'live',
displayMode: displayMode,
});
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{ cameraID: 'camera-1', capabilities: createCapabilities({ live: true }) },
{ cameraID: 'camera-2', capabilities: createCapabilities({ live: true }) },
]),
);
vi.mocked(getCameraIDsWithCapabilityForView).mockReturnValue(
new Set(['camera-1', 'camera-2']),
);
expect(
calculateButtons(controller, { cameraManager: cameraManager, view: view }),
).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: displayMode === 'single' ? 'mdi:grid' : 'mdi:grid-off',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title:
displayMode === 'grid'
? 'Show single media viewer'
: 'Show media viewer for each camera in a grid',
style:
displayMode === 'grid'
? { color: 'var(--advanced-camera-card-menu-button-active-color)' }
: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'display_mode_select',
display_mode: displayMode === 'single' ? 'grid' : 'single',
},
});
},
);
});
describe('should have show ptz button', () => {
it('should not show when not in live view', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
view: createView({ view: 'clips' }),
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Show PTZ controls',
}),
);
});
it('should show when in live view', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
view: createView({ view: 'live' }),
});
expect(buttons).toContainEqual(
expect.objectContaining({
title: 'Show PTZ controls',
}),
);
});
it('should show when the context has PTZ enabled', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const view = createView({
camera: 'camera-1',
context: { ptzControls: { enabled: true } },
});
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
view: view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
enabled: false,
},
title: 'Show PTZ controls',
type: 'custom:advanced-camera-card-menu-icon',
});
});
it('should show when the context has PTZ disabled', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const view = createView({
camera: 'camera-1',
context: { ptzControls: { enabled: false } },
});
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
view: view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
enabled: true,
},
title: 'Show PTZ controls',
type: 'custom:advanced-camera-card-menu-icon',
});
});
it('should detect current status without context in auto mode', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const view = createView({
camera: 'camera-1',
});
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
config: createConfig({ live: { controls: { ptz: { mode: 'auto' } } } }),
view: view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
enabled: false,
},
title: 'Show PTZ controls',
type: 'custom:advanced-camera-card-menu-icon',
});
});
it('should show when a substream is PTZ enabled', () => {
const store = createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{
cameraID: 'camera-2',
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const view = createView({
camera: 'camera-1',
context: {
live: {
overrides: new Map([['camera-1', 'camera-2']]),
},
},
});
const buttons = calculateButtons(controller, {
cameraManager: createCameraManager(store),
view: view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
enabled: false,
},
title: 'Show PTZ controls',
type: 'custom:advanced-camera-card-menu-icon',
});
});
});
describe('should have ptz home button', () => {
it.each([
['live' as const, true, false],
['live' as const, undefined, false],
['live' as const, false, true],
['media' as const, true, false],
['media' as const, undefined, false],
['media' as const, false, true],
])(
'%s view with isDefault %s',
(
viewName: AdvancedCameraCardView,
isDefault: boolean | undefined,
expectedResult: boolean,
) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }]),
);
const targetID = viewName === 'live' ? 'camera-1' : 'media-1';
const view = createView({
view: viewName,
camera: 'camera-1',
queryResults: new QueryResults({
results: [new TestViewMedia({ id: 'media-1' })],
selectedIndex: 0,
}),
...(isDefault !== undefined && {
context: {
zoom: {
[targetID]: {
observed: {
isDefault: isDefault,
unzoomed: false,
zoom: 1,
pan: {
x: 50,
y: 50,
},
},
},
},
},
}),
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
});
if (expectedResult) {
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
enabled: false,
icon: 'mdi:home',
priority: 50,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
target_id: targetID,
},
title: 'PTZ Home',
type: 'custom:advanced-camera-card-menu-icon',
});
} else {
expect(buttons).not.toContainEqual(
expect.objectContaining({ title: 'PTZ Home' }),
);
}
},
);
});
describe('should have folders button', () => {
it('should have no folders button without folders', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(new Map().entries());
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
icon: 'mdi:folder',
}),
]),
);
});
it('should have a folders button for a single folder outside the folder view', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
);
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:folder',
enabled: true,
priority: 50,
style: {},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folders' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
it('should have a folders button for a single folder inside the folder view', () => {
const folder = createFolder({ id: 'folder-0' });
const foldersManager = mock<FoldersManager>();
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', folder]]).entries(),
);
const folderNode: FolderQuery = {
source: QuerySource.Folder,
folder: folder,
path: [{ ha: { id: 'one' } }],
};
const buttons = calculateButtons(controller, {
foldersManager,
view: createView({
view: 'folder',
query: new UnifiedQuery().addNode(folderNode),
}),
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:folder',
enabled: true,
priority: 50,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folders' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
it('should have a folders submenu for multiple folder with a selected folder', () => {
const foldersManager = mock<FoldersManager>();
const selectedFolder = createFolder({ id: 'folder-selected' });
const folders = new Map([
['folder-0', createFolder({ id: 'folder-0' })],
['folder-selected', selectedFolder],
]);
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(folders.entries());
const selectedFolderNode: FolderQuery = {
source: QuerySource.Folder,
folder: selectedFolder,
path: [{ ha: { id: 'id' } }],
};
const view = createView({
view: 'folder',
query: new UnifiedQuery().addNode(selectedFolderNode),
});
const buttons = calculateButtons(controller, {
foldersManager,
view,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:folder-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Folders',
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
items: [
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folders',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-selected',
},
icon: 'mdi:folder',
selected: true,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folders',
folder: 'folder-selected',
},
title: 'folder-selected',
},
],
});
});
it('should have a folders submenu for multiple folder without a selected folder', () => {
const foldersManager = mock<FoldersManager>();
const folders = new Map([
['folder-0', createFolder({ id: 'folder-0' })],
['folder-1', createFolder({ id: 'folder-1' })],
]);
foldersManager.hasFolders.mockReturnValue(true);
foldersManager.getFolders.mockReturnValue(folders.entries());
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:folder-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Folders',
style: {},
items: [
{
enabled: true,
icon: 'mdi:folder',
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folders',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-1',
},
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folders',
folder: 'folder-1',
},
title: 'folder-1',
},
],
});
});
});
describe('should handle dynamic buttons', () => {
it('successfully', () => {
const button: MenuItem = {
...dynamicButton,
style: {},
};
controller.addDynamicMenuButton(button);
expect(
calculateButtons(controller).filter((menuButton) => isEqual(button, menuButton))
.length,
).toBe(1);
// Adding it again will have no effect.
controller.addDynamicMenuButton(button);
expect(
calculateButtons(controller).filter((menuButton) => isEqual(button, menuButton))
.length,
).toBe(1);
controller.removeDynamicMenuButton(button);
expect(calculateButtons(controller)).not.toContainEqual(button);
});
it('with stock HA action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: { action: 'navigate', navigation_path: 'foo' },
};
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: {},
});
});
it('with non-advanced-camera-card fire-dom-event action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: { action: 'fire-dom-event' },
};
controller.addDynamicMenuButton(button);
controller.addDynamicMenuButton(dynamicButton);
expect(calculateButtons(controller)).toContainEqual({
...button,
style: {},
});
});
it('with advanced camera card view action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
};
const view = createView({ view: 'clips' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('with advanced camera card default action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'default' },
};
const cameraManager = createCameraManager(createStore([{ cameraID: 'camera-1' }]));
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { cameraManager })).toContainEqual({
...button,
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('with fullscreen action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'fullscreen',
},
};
controller.addDynamicMenuButton(button);
const fullscreenManager = mock<FullscreenManager>();
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(true);
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
expect(calculateButtons(controller, { fullscreenManager })).toContainEqual({
...button,
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('with camera_select action', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'foo',
},
};
const view = createView({ camera: 'foo' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('with array of actions', () => {
const button: MenuItem = {
...dynamicButton,
tap_action: [
{ action: 'fire-dom-event' },
{ action: 'fire-dom-event', advanced_camera_card_action: 'clips' },
],
};
const view = createView({ camera: 'clips' });
controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({
...button,
style: {},
});
});
});
describe('should have set review button', () => {
it('when unreviewed', () => {
const selectedItem = new TestViewMedia({
mediaType: ViewMediaType.Review,
reviewed: false,
});
const queryResults = mock<QueryResults>();
queryResults.getSelectedResult.mockReturnValue(selectedItem);
const view = createView({
view: 'media',
queryResults: queryResults,
});
const buttons = calculateButtons(controller, { view: view });
expect(buttons).toContainEqual(
expect.objectContaining({
icon: 'mdi:check-circle-outline',
title: 'Mark as reviewed',
}),
);
});
it('when already reviewed', () => {
const selectedItem = new TestViewMedia({
mediaType: ViewMediaType.Review,
reviewed: true,
});
const queryResults = mock<QueryResults>();
queryResults.getSelectedResult.mockReturnValue(selectedItem);
const view = createView({
view: 'media',
queryResults: queryResults,
});
const buttons = calculateButtons(controller, { view: view });
expect(buttons).toContainEqual(
expect.objectContaining({
icon: 'mdi:check-circle',
title: 'Mark as unreviewed',
}),
);
});
it('when isReviewed returns null', () => {
const selectedItem = new TestViewMedia({
mediaType: ViewMediaType.Review,
reviewed: null,
});
const queryResults = mock<QueryResults>();
queryResults.getSelectedResult.mockReturnValue(selectedItem);
const view = createView({
view: 'media',
queryResults: queryResults,
});
const buttons = calculateButtons(controller, { view: view });
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
icon: 'mdi:check-circle',
}),
]),
);
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
icon: 'mdi:check-circle-outline',
}),
]),
);
});
});
it('should handle view without camera', () => {
const buttons = calculateButtons(controller, {
view: createView({ camera: null }),
});
expect(buttons).toBeDefined();
});
});