2517 lines
76 KiB
TypeScript
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();
|
|
});
|
|
});
|