Ensure timeline menu buttons shows up when it should.

This commit is contained in:
Dermot Duffy
2024-03-09 19:57:14 -08:00
parent e4783681d6
commit 6ed1c90554
7 changed files with 1208 additions and 1070 deletions
+2 -7
View File
@@ -3,7 +3,6 @@ import {
ActionsConfig, ActionsConfig,
ActionType, ActionType,
FrigateCardCustomAction, FrigateCardCustomAction,
FRIGATE_CARD_VIEW_DEFAULT,
} from '../config/types.js'; } from '../config/types.js';
import { import {
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
@@ -177,14 +176,10 @@ export class ActionsManager {
const viewOnCameraSelect = config?.view.camera_select ?? 'current'; const viewOnCameraSelect = config?.view.camera_select ?? 'current';
const targetViewName = const targetViewName =
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect; viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
const verifiedViewName = this._api
.getViewManager()
.isViewSupportedByCamera(selectCameraID, targetViewName)
? targetViewName
: FRIGATE_CARD_VIEW_DEFAULT;
this._api.getViewManager().setViewByParameters({ this._api.getViewManager().setViewByParameters({
viewName: verifiedViewName, viewName: targetViewName,
cameraID: selectCameraID, cameraID: selectCameraID,
failSafe: true,
}); });
} }
break; break;
+20 -8
View File
@@ -190,7 +190,17 @@ export class ViewManager {
} }
public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean { public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
const capabilities = this._api.getCameraManager().getCameraCapabilities(cameraID); const dependentCamerasCapabilities = this._api
.getCameraManager()
.getAggregateCameraCapabilities(
this._api.getCameraManager().getStore().getAllDependentCameras(cameraID),
);
const allCamerasCapabilities = this._api
.getCameraManager()
.getAggregateCameraCapabilities(
this._api.getCameraManager().getStore().getCameraIDs(),
);
switch (view) { switch (view) {
case 'live': case 'live':
case 'image': case 'image':
@@ -198,20 +208,22 @@ export class ViewManager {
return true; return true;
case 'clip': case 'clip':
case 'clips': case 'clips':
return !!capabilities?.supportsClips; return !!dependentCamerasCapabilities?.supportsClips;
case 'snapshot': case 'snapshot':
case 'snapshots': case 'snapshots':
return !!capabilities?.supportsSnapshots; return !!dependentCamerasCapabilities?.supportsSnapshots;
case 'recording': case 'recording':
case 'recordings': case 'recordings':
return !!capabilities?.supportsRecordings; return !!dependentCamerasCapabilities?.supportsRecordings;
case 'timeline': case 'timeline':
return !!capabilities?.supportsTimeline; // Show the timeline if any camera supports it, even cameras unrelated
// to the currently selected camera.
return !!allCamerasCapabilities?.supportsTimeline;
case 'media': case 'media':
return ( return (
!!capabilities?.supportsClips || !!dependentCamerasCapabilities?.supportsClips ||
!!capabilities?.supportsSnapshots || !!dependentCamerasCapabilities?.supportsSnapshots ||
!!capabilities?.supportsRecordings !!dependentCamerasCapabilities?.supportsRecordings
); );
} }
} }
+1
View File
@@ -209,6 +209,7 @@ class FrigateCard extends LitElement {
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(), showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
mediaPlayerController: this._controller.getMediaPlayerManager(), mediaPlayerController: this._controller.getMediaPlayerManager(),
microphoneManager: this._controller.getMicrophoneManager(), microphoneManager: this._controller.getMicrophoneManager(),
viewManager: this._controller.getViewManager(),
}, },
)} )}
.entityRegistryManager=${this._controller.getEntityRegistryManager()} .entityRegistryManager=${this._controller.getEntityRegistryManager()}
+20 -16
View File
@@ -3,10 +3,11 @@ import { StyleInfo } from 'lit/directives/style-map';
import { CameraManager } from '../camera-manager/manager'; import { CameraManager } from '../camera-manager/manager';
import { MediaPlayerManager } from '../card-controller/media-player-manager'; import { MediaPlayerManager } from '../card-controller/media-player-manager';
import { MicrophoneManager } from '../card-controller/microphone-manager'; import { MicrophoneManager } from '../card-controller/microphone-manager';
import { ViewManager } from '../card-controller/view-manager';
import { import {
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig, FrigateCardConfig,
FrigateCardCustomAction, FrigateCardCustomAction,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MenuItem, MenuItem,
} from '../config/types'; } from '../config/types';
import { FRIGATE_BUTTON_MENU_ICON } from '../const'; import { FRIGATE_BUTTON_MENU_ICON } from '../const';
@@ -31,6 +32,7 @@ export interface MenuButtonControllerOptions {
inExpandedMode?: boolean; inExpandedMode?: boolean;
microphoneManager?: MicrophoneManager | null; microphoneManager?: MicrophoneManager | null;
mediaPlayerController?: MediaPlayerManager | null; mediaPlayerController?: MediaPlayerManager | null;
viewManager?: ViewManager | null;
} }
export class MenuButtonController { export class MenuButtonController {
@@ -62,18 +64,19 @@ export class MenuButtonController {
): MenuItem[] { ): MenuItem[] {
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs(); const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
const selectedCameraID = view.camera; const selectedCameraID = view.camera;
const substreamAwareCameraID =
view.context?.live?.overrides?.get(selectedCameraID) ?? selectedCameraID;
const selectedCameraConfig = cameraManager const selectedCameraConfig = cameraManager
.getStore() .getStore()
.getCameraConfig(selectedCameraID); .getCameraConfig(selectedCameraID);
const allSelectedCameraIDs = cameraManager const allSelectedCameraIDs = cameraManager
.getStore() .getStore()
.getAllDependentCameras(selectedCameraID); .getAllDependentCameras(selectedCameraID);
const selectedMedia = view.queryResults?.getSelectedResult();
const selectedCameraCapabilities = const substreamAwareCameraCapabilities =
cameraManager.getCameraCapabilities(selectedCameraID); cameraManager.getCameraCapabilities(substreamAwareCameraID);
const aggregateCapabilities =
cameraManager.getAggregateCameraCapabilities(allSelectedCameraIDs); const selectedMedia = view.queryResults?.getSelectedResult();
const mediaCapabilities = selectedMedia const mediaCapabilities = selectedMedia
? cameraManager?.getMediaCapabilities(selectedMedia) ? cameraManager?.getMediaCapabilities(selectedMedia)
: null; : null;
@@ -125,7 +128,6 @@ export class MenuButtonController {
if (selectedCameraID && allSelectedCameraIDs && view.is('live')) { if (selectedCameraID && allSelectedCameraIDs && view.is('live')) {
const dependencies = [...allSelectedCameraIDs]; const dependencies = [...allSelectedCameraIDs];
const override = view.context?.live?.overrides?.get(selectedCameraID);
if (dependencies.length === 2) { if (dependencies.length === 2) {
// If there are only two dependencies (the main camera, and 1 other) // If there are only two dependencies (the main camera, and 1 other)
@@ -133,7 +135,9 @@ export class MenuButtonController {
buttons.push({ buttons.push({
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
style: style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {}, substreamAwareCameraID !== selectedCameraID
? this._getEmphasizedStyle()
: {},
title: localize('config.menu.buttons.substreams'), title: localize('config.menu.buttons.substreams'),
...config.menu.buttons.substreams, ...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -166,7 +170,9 @@ export class MenuButtonController {
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
title: localize('config.menu.buttons.substreams'), title: localize('config.menu.buttons.substreams'),
style: style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {}, substreamAwareCameraID !== selectedCameraID
? this._getEmphasizedStyle()
: {},
...config.menu.buttons.substreams, ...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-submenu', type: 'custom:frigate-card-menu-submenu',
items: menuItems, items: menuItems,
@@ -183,7 +189,7 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
}); });
if (aggregateCapabilities?.supportsClips) { if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'clips')) {
buttons.push({ buttons.push({
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
...config.menu.buttons.clips, ...config.menu.buttons.clips,
@@ -195,7 +201,7 @@ export class MenuButtonController {
}); });
} }
if (aggregateCapabilities?.supportsSnapshots) { if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'snapshots')) {
buttons.push({ buttons.push({
icon: 'mdi:camera', icon: 'mdi:camera',
...config.menu.buttons.snapshots, ...config.menu.buttons.snapshots,
@@ -211,7 +217,7 @@ export class MenuButtonController {
}); });
} }
if (aggregateCapabilities?.supportsRecordings) { if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'recordings')) {
buttons.push({ buttons.push({
icon: 'mdi:album', icon: 'mdi:album',
...config.menu.buttons.recordings, ...config.menu.buttons.recordings,
@@ -236,9 +242,7 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
}); });
// Don't show the timeline button unless there's at least one non-birdseye if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'timeline')) {
// camera with a Frigate camera name.
if (aggregateCapabilities?.supportsTimeline) {
buttons.push({ buttons.push({
icon: 'mdi:chart-gantt', icon: 'mdi:chart-gantt',
...config.menu.buttons.timeline, ...config.menu.buttons.timeline,
@@ -419,7 +423,7 @@ export class MenuButtonController {
}); });
} }
if (hasUsablePTZ(selectedCameraCapabilities, config.live.controls.ptz)) { if (hasUsablePTZ(substreamAwareCameraCapabilities, config.live.controls.ptz)) {
const isOn = const isOn =
view.context?.live?.ptzVisible === false view.context?.live?.ptzVisible === false
? false ? false
+5 -26
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { afterAll, describe, expect, it, vi } from 'vitest'; import { afterAll, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended'; import { mock } from 'vitest-mock-extended';
import { import {
@@ -382,6 +383,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({ expect.objectContaining({
viewName: 'live', viewName: 'live',
cameraID: 'camera', cameraID: 'camera',
failSafe: true,
}), }),
); );
}); });
@@ -409,6 +411,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({ expect.objectContaining({
viewName: 'timeline', viewName: 'timeline',
cameraID: 'camera', cameraID: 'camera',
failSafe: true,
}), }),
); );
}); });
@@ -442,6 +445,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({ expect.objectContaining({
viewName: 'clips', viewName: 'clips',
cameraID: 'camera', cameraID: 'camera',
failSafe: true,
}), }),
); );
}); });
@@ -467,6 +471,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({ expect.objectContaining({
viewName: 'live', viewName: 'live',
cameraID: 'camera', cameraID: 'camera',
failSafe: true,
}), }),
); );
}); });
@@ -503,32 +508,6 @@ describe('ActionsManager.executeAction', () => {
expect(api.getViewManager().setViewByParameters).not.toBeCalled(); expect(api.getViewManager().setViewByParameters).not.toBeCalled();
}); });
it('with an unsupported view', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'timeline',
}),
);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
// Should have fallen back to the default view.
viewName: 'live',
cameraID: 'camera',
}),
);
});
}); });
it('should handle live_substream_select action', async () => { it('should handle live_substream_select action', async () => {
+18 -13
View File
@@ -7,6 +7,7 @@ import { EventMediaQueries } from '../../src/view/media-queries';
import { MediaQueriesResults } from '../../src/view/media-queries-results'; import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view'; import { View } from '../../src/view/view';
import { import {
createAggregateCameraCapabilities,
createCameraCapabilities, createCameraCapabilities,
createCameraConfig, createCameraConfig,
createCameraManager, createCameraManager,
@@ -15,7 +16,7 @@ import {
createHASS, createHASS,
createStore, createStore,
createView, createView,
generateViewMediaArray generateViewMediaArray,
} from '../test-utils'; } from '../test-utils';
vi.mock('../../src/utils/media-to-view'); vi.mock('../../src/utils/media-to-view');
@@ -218,8 +219,8 @@ describe('ViewManager.setViewByParameters', () => {
}, },
]), ]),
); );
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue( vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createCameraCapabilities({ createAggregateCameraCapabilities({
supportsClips: true, supportsClips: true,
}), }),
); );
@@ -247,8 +248,8 @@ describe('ViewManager.setViewByParameters', () => {
}, },
]), ]),
); );
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue( vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createCameraCapabilities({ createAggregateCameraCapabilities({
supportsClips: true, supportsClips: true,
}), }),
); );
@@ -336,9 +337,11 @@ describe('ViewManager.setViewByParameters', () => {
]), ]),
); );
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({ vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsSnapshots: false, supportsSnapshots: false,
})); }),
);
const manager = new ViewManager(api); const manager = new ViewManager(api);
manager.setViewByParameters({ manager.setViewByParameters({
@@ -360,9 +363,11 @@ describe('ViewManager.setViewByParameters', () => {
]), ]),
); );
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({ vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsSnapshots: false, supportsSnapshots: false,
})); }),
);
const manager = new ViewManager(api); const manager = new ViewManager(api);
manager.setViewByParameters({ manager.setViewByParameters({
@@ -392,8 +397,8 @@ describe('ViewManager.setViewByParameters', () => {
}, },
]), ]),
); );
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue( vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createCameraCapabilities({ createAggregateCameraCapabilities({
supportsClips: true, supportsClips: true,
supportsRecordings: true, supportsRecordings: true,
supportsSnapshots: true, supportsSnapshots: true,
@@ -443,8 +448,8 @@ describe('ViewManager.setViewByParameters', () => {
}, },
]), ]),
); );
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue( vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createCameraCapabilities({ createAggregateCameraCapabilities({
supportsClips: true, supportsClips: true,
supportsRecordings: true, supportsRecordings: true,
supportsSnapshots: true, supportsSnapshots: true,
@@ -6,6 +6,7 @@ import { CameraManager } from '../../src/camera-manager/manager';
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types'; import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
import { MicrophoneManager } from '../../src/card-controller/microphone-manager'; import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
import { ViewManager } from '../../src/card-controller/view-manager';
import { import {
MenuButtonController, MenuButtonController,
MenuButtonControllerOptions, MenuButtonControllerOptions,
@@ -17,7 +18,6 @@ import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results'; import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view'; import { View } from '../../src/view/view';
import { import {
createAggregateCameraCapabilities,
createCameraCapabilities, createCameraCapabilities,
createCameraConfig, createCameraConfig,
createCameraManager, createCameraManager,
@@ -41,6 +41,7 @@ const calculateButtons = (
config?: FrigateCardConfig; config?: FrigateCardConfig;
cameraManager?: CameraManager; cameraManager?: CameraManager;
view?: View; view?: View;
viewManager?: ViewManager;
}, },
): MenuItem[] => { ): MenuItem[] => {
let cameraManager: CameraManager | null = options?.cameraManager ?? null; let cameraManager: CameraManager | null = options?.cameraManager ?? null;
@@ -71,8 +72,10 @@ describe('MenuButtonController', () => {
controller = new MenuButtonController(); controller = new MenuButtonController();
}); });
it('should have frigate menu button with hidden menu style', () => { describe('should have frigate menu button', () => {
it('with hidden menu style', () => {
const buttons = calculateButtons(controller); const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'frigate', icon: 'frigate',
enabled: true, enabled: true,
@@ -84,10 +87,11 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have frigate menu button without hidden menu style', () => { it('without hidden menu style', () => {
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
config: createConfig({ menu: { style: 'overlay' } }), config: createConfig({ menu: { style: 'overlay' } }),
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'frigate', icon: 'frigate',
enabled: true, enabled: true,
@@ -98,8 +102,10 @@ describe('MenuButtonController', () => {
hold_action: createFrigateCardSimpleAction('diagnostics'), hold_action: createFrigateCardSimpleAction('diagnostics'),
}); });
}); });
});
it('should have cameras menu', () => { describe('should have cameras menu', () => {
it('with multiple cameras', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]), createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
@@ -108,8 +114,8 @@ describe('MenuButtonController', () => {
title: 'title', title: 'title',
icon: 'icon', icon: 'icon',
}); });
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:video-switch', icon: 'mdi:video-switch',
enabled: true, enabled: true,
@@ -147,15 +153,15 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should not have a cameras menu without a visible camera', () => { it('without a visible camera', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
{ cameraID: 'camera-1', config: createCameraConfig({ hide: true }) }, { cameraID: 'camera-1', config: createCameraConfig({ hide: true }) },
]), ]),
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toEqual( expect(buttons).not.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -164,8 +170,10 @@ describe('MenuButtonController', () => {
]), ]),
); );
}); });
});
it('should have substream button with single dependency', () => { describe('should have substream button', () => {
it('with single dependency', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -176,8 +184,8 @@ describe('MenuButtonController', () => {
{ cameraID: 'camera-2' }, { cameraID: 'camera-2' },
]), ]),
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
style: {}, style: {},
@@ -192,7 +200,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have substream button selected with single dependency', () => { it('with substream selected and single dependency', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -203,7 +211,6 @@ describe('MenuButtonController', () => {
{ cameraID: 'camera-2' }, { cameraID: 'camera-2' },
]), ]),
); );
const view = createView({ const view = createView({
camera: 'camera-1', camera: 'camera-1',
context: { context: {
@@ -212,11 +219,11 @@ describe('MenuButtonController', () => {
}, },
}, },
}); });
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager, cameraManager: cameraManager,
view: view, view: view,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
@@ -231,7 +238,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have substream menu without substream on with multiple dependencies', () => { it('with substream selected and multiple dependencies', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -271,6 +278,7 @@ describe('MenuButtonController', () => {
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
title: 'Substream(s)', title: 'Substream(s)',
@@ -322,7 +330,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have substream menu with substream on with multiple dependencies', () => { it('with substream selected and with multiple dependencies', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -347,7 +355,6 @@ describe('MenuButtonController', () => {
}, },
]), ]),
); );
const view = createView({ const view = createView({
camera: 'camera-1', camera: 'camera-1',
context: { context: {
@@ -356,7 +363,6 @@ describe('MenuButtonController', () => {
}, },
}, },
}); });
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager, cameraManager: cameraManager,
view: view, view: view,
@@ -414,9 +420,12 @@ describe('MenuButtonController', () => {
], ],
}); });
}); });
});
it('should have styled live menu button in live view', () => { describe('should have live menu button', () => {
it('when in live view', () => {
const buttons = calculateButtons(controller); const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:cctv', icon: 'mdi:cctv',
enabled: true, enabled: true,
@@ -428,9 +437,10 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled live menu button in non-live views', () => { it('when not in live view', () => {
const view = createView({ view: 'clips' }); const view = createView({ view: 'clips' });
const buttons = calculateButtons(controller, { view: view }); const buttons = calculateButtons(controller, { view: view });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:cctv', icon: 'mdi:cctv',
enabled: true, enabled: true,
@@ -441,16 +451,17 @@ describe('MenuButtonController', () => {
tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' },
}); });
}); });
});
it('should have styled clips menu button in clips view', () => { describe('should have clips menu button', () => {
const cameraManager = createCameraManager(); it('when in clips view', () => {
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( const viewManager = mock<ViewManager>();
createAggregateCameraCapabilities({ supportsClips: true }), vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'clips' }), view: createView({ view: 'clips' }),
viewManager: viewManager,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
enabled: true, enabled: true,
@@ -463,12 +474,13 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled clips menu button in non-clips view', () => { it('when not in clips view', () => {
const cameraManager = createCameraManager(); const viewManager = mock<ViewManager>();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
createAggregateCameraCapabilities({ supportsClips: true }), const buttons = calculateButtons(controller, {
); viewManager: viewManager,
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
enabled: true, enabled: true,
@@ -481,15 +493,28 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have styled snapshots menu button in snapshots view', () => { it('when not supported', () => {
const cameraManager = createCameraManager(); const viewManager = mock<ViewManager>();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(false);
createAggregateCameraCapabilities({ supportsSnapshots: true }), 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(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'snapshots' }), view: createView({ view: 'snapshots' }),
viewManager: viewManager,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:camera', icon: 'mdi:camera',
enabled: true, enabled: true,
@@ -502,33 +527,49 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled snapshots menu button in non-snapshots view', () => { it('when not in snapshots view', () => {
const cameraManager = createCameraManager(); const viewManager = mock<ViewManager>();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
createAggregateCameraCapabilities({ supportsSnapshots: true }),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Snapshots gallery',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'snapshots' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'snapshot' },
});
});
it('should have styled recordings menu button in recordings view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsRecordings: true }),
);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager, viewManager: viewManager,
view: createView({ view: 'recordings' }),
}); });
expect(buttons).toContainEqual({
icon: 'mdi:camera',
enabled: true,
priority: 50,
type: 'custom:frigate-card-menu-icon',
title: 'Snapshots gallery',
style: {},
tap_action: { action: 'fire-dom-event', frigate_card_action: 'snapshots' },
hold_action: { action: 'fire-dom-event', frigate_card_action: 'snapshot' },
});
});
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Snapshots gallery' }),
]),
);
});
});
describe('should have recordings menu button', () => {
it('when in recordings view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'recordings' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:album', icon: 'mdi:album',
enabled: false, enabled: false,
@@ -541,12 +582,13 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled recordings menu button in non-recordings view', () => { it('when not in recordings view', () => {
const cameraManager = createCameraManager(); const viewManager = mock<ViewManager>();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
createAggregateCameraCapabilities({ supportsRecordings: true }), const buttons = calculateButtons(controller, {
); viewManager: viewManager,
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:album', icon: 'mdi:album',
enabled: false, enabled: false,
@@ -559,10 +601,27 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have styled image menu button in image view', () => { it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).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 buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
view: createView({ view: 'image' }), view: createView({ view: 'image' }),
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:image', icon: 'mdi:image',
enabled: false, enabled: false,
@@ -574,8 +633,9 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled image menu button in non-image view', () => { it('when not in image view', () => {
const buttons = calculateButtons(controller); const buttons = calculateButtons(controller);
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:image', icon: 'mdi:image',
enabled: false, enabled: false,
@@ -586,16 +646,17 @@ describe('MenuButtonController', () => {
tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' },
}); });
}); });
it('should have styled timeline menu button in timeline view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'timeline' }),
}); });
describe('should have timeline button', () => {
it('when in timeline view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'timeline' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:chart-gantt', icon: 'mdi:chart-gantt',
enabled: true, enabled: true,
@@ -607,14 +668,14 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unstyled timeline menu button in non-timeline view', () => { it('when not in timeline view', () => {
const cameraManager = createCameraManager(); const viewManager = mock<ViewManager>();
vi.mocked(cameraManager.getAggregateCameraCapabilities).mockReturnValue( vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
createAggregateCameraCapabilities({ supportsTimeline: true }),
);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager, view: createView({ view: 'live' }),
viewManager: viewManager,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:chart-gantt', icon: 'mdi:chart-gantt',
enabled: true, enabled: true,
@@ -626,7 +687,22 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have download menu button', () => { it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).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' }); vi.stubGlobal('navigator', { userAgent: 'foo' });
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
@@ -643,6 +719,7 @@ describe('MenuButtonController', () => {
cameraManager: cameraManager, cameraManager: cameraManager,
view: view, view: view,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:download', icon: 'mdi:download',
enabled: true, enabled: true,
@@ -653,7 +730,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should not have download menu button when being casted', () => { it('not when being casted', () => {
vi.stubGlobal('navigator', { vi.stubGlobal('navigator', {
userAgent: userAgent:
'Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Mozilla/5.0 (Fuchsia) AppleWebKit/537.36 (KHTML, like Gecko) ' +
@@ -674,15 +751,18 @@ describe('MenuButtonController', () => {
cameraManager: cameraManager, cameraManager: cameraManager,
view: view, view: view,
}); });
expect(buttons).not.toEqual( expect(buttons).not.toEqual(
expect.arrayContaining([expect.objectContaining({ title: 'Download' })]), expect.arrayContaining([expect.objectContaining({ title: 'Download' })]),
); );
}); });
});
it('should have camera UI button', () => { it('should have camera UI button', () => {
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
showCameraUIButton: true, showCameraUIButton: true,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:web', icon: 'mdi:web',
enabled: true, enabled: true,
@@ -693,7 +773,8 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have microphone button', () => { describe('should have microphone button', () => {
it('with suitable loaded media', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager, microphoneManager: microphoneManager,
@@ -725,7 +806,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should not have microphone button when media does not support it', () => { it('without suitable loaded media', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager, microphoneManager: microphoneManager,
@@ -741,7 +822,7 @@ describe('MenuButtonController', () => {
); );
}); });
it('should have microphone button when microphone forbidden', () => { it('with forbidden microphone', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
mock<MicrophoneManager>(microphoneManager).isForbidden.mockReturnValue(true); mock<MicrophoneManager>(microphoneManager).isForbidden.mockReturnValue(true);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
@@ -763,7 +844,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have microphone button when microphone muted', () => { it('with muted microphone', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(true); mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
@@ -793,7 +874,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have microphone button when microphone muted with toggle type', () => { it('with muted toggle type microphone', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(true); mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(true);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
@@ -822,7 +903,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have microphone button when microphone unmuted with toggle type', () => { it('with unmuted toggle type microphone', () => {
const microphoneManager = new MicrophoneManager(createCardAPI()); const microphoneManager = new MicrophoneManager(createCardAPI());
mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(false); mock<MicrophoneManager>(microphoneManager).isMuted.mockReturnValue(false);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
@@ -853,8 +934,10 @@ describe('MenuButtonController', () => {
}, },
}); });
}); });
});
it('should have fullscreen button', () => { describe('should have fullscreen button', () => {
it('when not in fullscreen mode', () => {
// Need to write a readonly property. // Need to write a readonly property.
vi.stubGlobal('navigator', { userAgent: 'foo' }); vi.stubGlobal('navigator', { userAgent: 'foo' });
const buttons = calculateButtons(controller, { inFullscreenMode: false }); const buttons = calculateButtons(controller, { inFullscreenMode: false });
@@ -870,7 +953,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unfullscreen', () => { it('when in fullscreen mode', () => {
vi.stubGlobal('navigator', { userAgent: 'foo' }); vi.stubGlobal('navigator', { userAgent: 'foo' });
const buttons = calculateButtons(controller, { inFullscreenMode: true }); const buttons = calculateButtons(controller, { inFullscreenMode: true });
@@ -884,8 +967,10 @@ describe('MenuButtonController', () => {
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
});
it('should have expand button', () => { describe('should have expand button', () => {
it('when not expanded', () => {
const buttons = calculateButtons(controller, { inExpandedMode: false }); const buttons = calculateButtons(controller, { inExpandedMode: false });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
@@ -899,7 +984,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should have unexpand button', () => { it('when expanded', () => {
const buttons = calculateButtons(controller, { inExpandedMode: true }); const buttons = calculateButtons(controller, { inExpandedMode: true });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
@@ -912,8 +997,10 @@ describe('MenuButtonController', () => {
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
});
it('should have media players button', () => { describe('should have media players button', () => {
it('with media players', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -970,7 +1057,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should disable media players button when entity not found', () => { it('when entity not found', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -1011,6 +1098,7 @@ describe('MenuButtonController', () => {
], ],
}); });
}); });
});
it('should have pause button', () => { it('should have pause button', () => {
const player = mock<FrigateCardMediaPlayer>(); const player = mock<FrigateCardMediaPlayer>();
@@ -1124,6 +1212,7 @@ describe('MenuButtonController', () => {
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]), createStore([{ cameraID: 'camera-1' }, { cameraID: 'camera-2' }]),
); );
expect( expect(
calculateButtons(controller, { cameraManager: cameraManager, view: view }), calculateButtons(controller, { cameraManager: cameraManager, view: view }),
).toContainEqual({ ).toContainEqual({
@@ -1154,6 +1243,7 @@ describe('MenuButtonController', () => {
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toContainEqual({ expect(buttons).not.toContainEqual({
enabled: false, enabled: false,
icon: 'mdi:pan', icon: 'mdi:pan',
@@ -1176,8 +1266,8 @@ describe('MenuButtonController', () => {
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue( vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCameraCapabilities({ ptz: {} }), createCameraCapabilities({ ptz: {} }),
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
enabled: false, enabled: false,
icon: 'mdi:pan', icon: 'mdi:pan',
@@ -1204,11 +1294,11 @@ describe('MenuButtonController', () => {
camera: 'camera-1', camera: 'camera-1',
context: { live: { ptzVisible: false } }, context: { live: { ptzVisible: false } },
}); });
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
cameraManager: cameraManager, cameraManager: cameraManager,
view: view, view: view,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
enabled: false, enabled: false,
icon: 'mdi:pan', icon: 'mdi:pan',
@@ -1223,14 +1313,65 @@ describe('MenuButtonController', () => {
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
}); });
}); });
it('when a substream is PTZ enabled', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
);
vi.mocked(cameraManager.getCameraCapabilities).mockImplementation(
(cameraID: string) => {
if (cameraID === 'camera-2') {
return createCameraCapabilities({ ptz: {} });
}
return createCameraCapabilities();
},
);
const view = createView({
camera: 'camera-1',
context: {
live: {
overrides: new Map([['camera-1', 'camera-2']]),
},
},
});
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: view,
}); });
it('should handle dynamic buttons', () => { expect(buttons).toContainEqual({
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
});
});
});
describe('should handle dynamic buttons', () => {
it('successfully', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
style: {}, style: {},
}; };
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect( expect(
calculateButtons(controller).filter((menuButton) => isEqual(button, menuButton)) calculateButtons(controller).filter((menuButton) => isEqual(button, menuButton))
.length, .length,
@@ -1247,7 +1388,7 @@ describe('MenuButtonController', () => {
expect(calculateButtons(controller)).not.toContainEqual(button); expect(calculateButtons(controller)).not.toContainEqual(button);
}); });
it('should not set style for dynamic button with stock action', () => { it('with stock HA action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { action: 'navigate', navigation_path: 'foo' }, tap_action: { action: 'navigate', navigation_path: 'foo' },
@@ -1260,61 +1401,61 @@ describe('MenuButtonController', () => {
}); });
}); });
it('should not set style for dynamic button with non-Frigate fire-dom-event action', () => { it('with non-Frigate fire-dom-event action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { action: 'fire-dom-event' }, tap_action: { action: 'fire-dom-event' },
}; };
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
controller.addDynamicMenuButton(dynamicButton); controller.addDynamicMenuButton(dynamicButton);
expect(calculateButtons(controller)).toContainEqual({ expect(calculateButtons(controller)).toContainEqual({
...button, ...button,
style: {}, style: {},
}); });
}); });
it('should set style for dynamic button with Frigate view action', () => { it('with Frigate view action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' },
}; };
const view = createView({ view: 'clips' }); const view = createView({ view: 'clips' });
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({ expect(calculateButtons(controller, { view: view })).toContainEqual({
...button, ...button,
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
it('should set style for dynamic button with Frigate default action', () => { it('with Frigate default action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'default' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'default' },
}; };
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect(calculateButtons(controller)).toContainEqual({ expect(calculateButtons(controller)).toContainEqual({
...button, ...button,
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
it('should set style for dynamic button with fullscreen action', () => { it('with fullscreen action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
}; };
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { inFullscreenMode: true })).toContainEqual({ expect(calculateButtons(controller, { inFullscreenMode: true })).toContainEqual({
...button, ...button,
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
it('should set style for dynamic button with camera_select action', () => { it('with camera_select action', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: { tap_action: {
@@ -1323,16 +1464,16 @@ describe('MenuButtonController', () => {
camera: 'foo', camera: 'foo',
}, },
}; };
const view = createView({ camera: 'foo' }); const view = createView({ camera: 'foo' });
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({ expect(calculateButtons(controller, { view: view })).toContainEqual({
...button, ...button,
style: { color: 'var(--primary-color, white)' }, style: { color: 'var(--primary-color, white)' },
}); });
}); });
it('should set style for dynamic button with array of actions', () => { it('with array of actions', () => {
const button: MenuItem = { const button: MenuItem = {
...dynamicButton, ...dynamicButton,
tap_action: [ tap_action: [
@@ -1340,12 +1481,13 @@ describe('MenuButtonController', () => {
{ action: 'fire-dom-event', frigate_card_action: 'clips' }, { action: 'fire-dom-event', frigate_card_action: 'clips' },
], ],
}; };
const view = createView({ camera: 'clips' }); const view = createView({ camera: 'clips' });
controller.addDynamicMenuButton(button); controller.addDynamicMenuButton(button);
expect(calculateButtons(controller, { view: view })).toContainEqual({ expect(calculateButtons(controller, { view: view })).toContainEqual({
...button, ...button,
style: {}, style: {},
}); });
}); });
}); });
});