Add initial keyboard shortcut support.

This commit is contained in:
Dermot Duffy
2024-06-05 21:44:17 -07:00
parent 904f6d8142
commit 7ab545738d
186 changed files with 9564 additions and 3658 deletions
@@ -1,914 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { afterAll, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import {
ActionType,
FrigateCardCustomAction,
FrigateCardView,
frigateCardCustomActionSchema,
} from '../../src/config/types';
import { FrigateCardMediaPlayer } from '../../src/types';
import {
convertActionToFrigateCardCustomAction,
frigateCardHandleAction,
frigateCardHandleActionConfig,
getActionConfigGivenAction,
} from '../../src/utils/action.js';
import { ActionsManager, Interaction } from '../../src/card-controller/actions-manager';
import {
createCardAPI,
createConfig,
createHASS,
createMediaLoadedInfo,
createView,
createViewWithMedia,
} from '../test-utils';
vi.mock('../../src/utils/action.js');
const createAction = (
action: Record<string, unknown>,
): FrigateCardCustomAction | null => {
const result = frigateCardCustomActionSchema.safeParse({
action: 'custom:frigate-card-action',
...action,
});
return result.success ? result.data : null;
};
describe('ActionsManager.getMergedActions', () => {
const config = {
view: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '1',
},
},
},
live: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '2',
},
},
},
media_gallery: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '3',
},
},
},
media_viewer: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '4',
},
},
},
image: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '5',
},
},
},
};
afterAll(() => {
vi.restoreAllMocks();
});
it('should get no merged actions with a message', () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ view: 'live' }),
);
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
const manager = new ActionsManager(api);
expect(manager.getMergedActions()).toEqual({});
});
describe('should get merged actions with live view', () => {
it.each([
[
'live' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '2',
},
},
],
[
'clips' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '3',
},
},
],
[
'clip' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '4',
},
},
],
[
'image' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '5',
},
},
],
['timeline' as const, {}],
])('%s', (viewName: FrigateCardView, result: Record<string, unknown>) => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ view: viewName }),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
const manager = new ActionsManager(api);
expect(manager.getMergedActions()).toEqual(result);
});
});
});
// @vitest-environment jsdom
describe('ActionsManager.handleInteraction', () => {
it('should handle interaction', () => {
const api = createCardAPI();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new ActionsManager(api);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const actionForThisInteraction: ActionType = {
action: 'none',
};
vi.mocked(getActionConfigGivenAction).mockReturnValue(actionForThisInteraction);
manager.handleInteractionEvent(
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
);
expect(frigateCardHandleActionConfig).toBeCalledWith(
element,
hass,
manager.getMergedActions(),
'tap',
actionForThisInteraction,
);
});
it('should not handle interaction without hass', () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
// No values of hass.
manager.handleInteractionEvent(
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
);
expect(frigateCardHandleActionConfig).not.toBeCalledWith();
});
it('should not handle malformed interaction', () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
manager.handleInteractionEvent(
new CustomEvent<Interaction>('event', {
// Malformed interaction type.
detail: { action: 'double_finger_snap' } as unknown as Interaction,
}),
);
expect(frigateCardHandleActionConfig).not.toBeCalledWith();
});
});
describe('ActionsManager.handleActionEvent', () => {
it('should handle event', () => {
const action = createAction({ frigate_card_action: 'default' })!;
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
detail: action,
});
// The file containing convertActionToFrigateCardCustomAction (action.ts) is
// mocked, so need to provide a value here.
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
const api = createCardAPI();
const manager = new ActionsManager(api);
manager.handleActionEvent(event);
expect(api.getViewManager().setViewDefault).toBeCalled();
});
it('should not handle event without detail', () => {
const action = createAction({ frigate_card_action: 'default' })!;
const event = new Event('ll-custom');
// Mock this out just so that if the sentinel in handleActionEvent failed,
// it would still trigger a test failure below.
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
const api = createCardAPI();
const manager = new ActionsManager(api);
manager.handleActionEvent(event);
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
it('should not handle malformed action', () => {
const action = createAction({ frigate_card_action: 'default' })!;
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
detail: action,
});
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(null);
const api = createCardAPI();
const manager = new ActionsManager(api);
manager.handleActionEvent(event);
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
});
describe('ActionsManager.executeAction', () => {
it('should not handle actions with different card_id', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
card_id: 'foo',
}),
);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
card_id: 'NOT_foo',
frigate_card_action: 'default',
})!,
);
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
it('should handle default action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'default',
})!,
);
expect(api.getViewManager().setViewDefault).toBeCalled();
});
describe('should handle view action', async () => {
it.each([
['clip' as const],
['clips' as const],
['image' as const],
['live' as const],
['recording' as const],
['recordings' as const],
['snapshot' as const],
['snapshots' as const],
['timeline' as const],
])('%s', async (viewName: FrigateCardView) => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: viewName,
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: viewName,
}),
);
});
});
it('should handle download action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'download',
})!,
);
expect(api.getDownloadManager().downloadViewerMedia).toBeCalled();
});
it('should handle camera ui action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_ui',
})!,
);
expect(api.getCameraURLManager().openURL).toBeCalled();
});
it('should handle expand action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'expand',
})!,
);
expect(api.getExpandManager().toggleExpanded).toBeCalled();
});
it('should handle fullscreen action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'fullscreen',
})!,
);
expect(api.getFullscreenManager().toggleFullscreen).toBeCalled();
});
it('should handle menu toggle action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'menu_toggle',
})!,
);
expect(api.getCardElementManager().toggleMenu).toBeCalled();
});
describe('should handle camera_select action', () => {
it('with valid camera and view', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('without config', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'timeline',
}),
);
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'timeline',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('with target view', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
// Change to clips view when the camera changes.
camera_select: 'clips',
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'live',
}),
);
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'clips',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('with triggered camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(
api.getTriggersManager().getMostRecentlyTriggeredCameraID,
).mockReturnValue('camera');
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
triggered: true,
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('without camera or triggered camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(
api.getTriggersManager().getMostRecentlyTriggeredCameraID,
).mockReturnValue('camera');
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
})!,
);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
it('without a current view', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
});
it('should handle live_substream_select action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'live_substream_select',
camera: 'substream',
})!,
);
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream');
});
it('should handle live_substream_off action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'live_substream_off',
})!,
);
expect(api.getViewManager().setViewWithoutSubstream).toBeCalled();
});
it('should handle live_substream_on action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'live_substream_on',
})!,
);
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith();
});
describe('should handle media_player action', () => {
it('to stop', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'media_player',
media_player_action: 'stop',
media_player: 'this_is_a_media_player',
})!,
);
expect(api.getMediaPlayerManager().stop).toBeCalledWith('this_is_a_media_player');
});
it('to play live', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera',
view: 'live',
}),
);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
})!,
);
expect(api.getMediaPlayerManager().playLive).toBeCalledWith(
'this_is_a_media_player',
'camera',
);
});
it('to play media', async () => {
const api = createCardAPI();
const view = createViewWithMedia({
camera: 'camera',
view: 'media',
});
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
})!,
);
expect(api.getMediaPlayerManager().playMedia).toBeCalledWith(
'this_is_a_media_player',
view.queryResults?.getSelectedResult(),
);
});
it('to play media without selected media', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'media',
}),
);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
})!,
);
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
});
});
it('should handle diagnostics action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'diagnostics',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'diagnostics',
}),
);
});
it('should handle microphone_mute action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'microphone_mute',
})!,
);
expect(api.getMicrophoneManager().mute).toBeCalled();
});
it('should handle microphone_unmute action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'microphone_unmute',
})!,
);
expect(api.getMicrophoneManager().unmute).toBeCalled();
});
describe('should handle media player action', () => {
it.each([
['mute' as const],
['unmute' as const],
['play' as const],
['pause' as const],
])('%s', async (action: 'mute' | 'unmute' | 'play' | 'pause') => {
const api = createCardAPI();
const player = mock<FrigateCardMediaPlayer>();
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: action,
})!,
);
expect(player[action]).toBeCalled();
});
});
it('should handle screenshot action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'screenshot',
})!,
);
expect(api.getDownloadManager().downloadScreenshot).toBeCalled();
});
it('should handle display_mode_select action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'display_mode_select',
display_mode: 'grid',
})!,
);
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
});
describe('should handle ptz action', () => {
it('with selected camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'ptz',
ptz_action: 'left',
})!,
);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
preset: undefined,
},
);
});
it('without selected camera', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'ptz',
ptz_action: 'left',
})!,
);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
});
});
it('should handle show_ptz action', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'show_ptz',
show_ptz: true,
})!,
);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith(
expect.objectContaining({ live: { ptzVisible: true } }),
);
});
describe('should handle change_zoom action', () => {
it('default', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'change_zoom',
target_id: 'camera.office',
})!,
);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith(
expect.objectContaining({
zoom: {
'camera.office': {
zoom: {},
},
},
}),
);
});
it('non-default', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'change_zoom',
target_id: 'camera.office',
zoom: 2,
pan: {
x: 3,
y: 4,
},
})!,
);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith(
expect.objectContaining({
zoom: {
'camera.office': {
zoom: {
zoom: 2,
pan: {
x: 3,
y: 4,
},
},
},
},
}),
);
});
});
it('should handle unknown action', async () => {
const manager = new ActionsManager(createCardAPI());
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
await manager.executeFrigateAction(
// Have to manually create the action (vs using `createAction()`) since
// it's malformed.
{
frigate_card_action: 'not_a_real_action',
} as unknown as FrigateCardCustomAction,
);
expect(spy).toBeCalledWith(
'Frigate card received unknown card action: not_a_real_action',
);
});
});
describe('ActionsManager.executeActions', () => {
it('should execute actions', async () => {
const api = createCardAPI();
const hass = createHASS();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = new ActionsManager(api);
const action = createAction({
frigate_card_action: 'default',
})!;
manager.executeActions(action);
expect(frigateCardHandleAction).toBeCalledWith(element, hass, {}, action);
});
it('should not execute actions without hass', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
const manager = new ActionsManager(api);
manager.executeActions(
createAction({
frigate_card_action: 'default',
})!,
);
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
});
@@ -0,0 +1,289 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import {
ActionsManager,
Interaction,
InteractionName,
} from '../../../src/card-controller/actions/actions-manager';
import { FrigateCardView } from '../../../src/config/types';
import { createLogAction } from '../../../src/utils/action';
import {
createAction,
createCardAPI,
createConfig,
createHASS,
createView,
} from '../../test-utils';
describe('ActionsManager', () => {
describe('getMergedActions', () => {
const config = {
view: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '1',
},
},
},
live: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '2',
},
},
},
media_gallery: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '3',
},
},
},
media_viewer: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '4',
},
},
},
image: {
actions: {
tap_action: {
action: 'navigate',
navigation_path: '5',
},
},
},
};
afterAll(() => {
vi.restoreAllMocks();
});
it('should get no merged actions with a message', () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ view: 'live' }),
);
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
const manager = new ActionsManager(api);
expect(manager.getMergedActions()).toEqual({});
});
describe('should get merged actions with live view', () => {
it.each([
[
'live' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '2',
},
},
],
[
'clips' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '3',
},
},
],
[
'clip' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '4',
},
},
],
[
'image' as const,
{
tap_action: {
action: 'navigate',
navigation_path: '5',
},
},
],
['timeline' as const, {}],
])('%s', (viewName: FrigateCardView, result: Record<string, unknown>) => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ view: viewName }),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig(config),
);
const manager = new ActionsManager(api);
expect(manager.getMergedActions()).toEqual(result);
});
});
});
// @vitest-environment jsdom
describe('handleInteractionEvent', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('should handle interaction', () => {
const api = createCardAPI();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
actions: {
tap_action: createLogAction("Hello, world!"),
},
},
}),
);
const manager = new ActionsManager(api);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
manager.handleInteractionEvent(
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
);
expect(consoleSpy).toBeCalled();
});
describe('should handle unexpected interactions', () => {
it.each([['malformed_type_of_tap' as const], ['double_tap' as const]])(
'%s',
(interaction: string) => {
const api = createCardAPI();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
actions: {
tap_action: createLogAction("Hello, world!"),
},
},
}),
);
const manager = new ActionsManager(api);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
manager.handleInteractionEvent(
new CustomEvent<Interaction>('event', {
detail: { action: interaction as unknown as InteractionName },
}),
);
expect(consoleSpy).not.toBeCalled();
},
);
});
});
describe('handleCustomActionEvent', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('should handle event', () => {
const action = createLogAction("Hello, world!");
const event = new CustomEvent('ll-custom', {
detail: action,
});
const api = createCardAPI();
const manager = new ActionsManager(api);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
manager.handleCustomActionEvent(event);
expect(consoleSpy).toBeCalled();
});
it('should not handle event without detail', () => {
const manager = new ActionsManager(createCardAPI());
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
manager.handleCustomActionEvent(new Event('ll-custom'));
expect(consoleSpy).not.toBeCalled();
});
});
describe('handleActionExecutionRequestEvent', () => {
it('should execute actions', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
await manager.handleActionExecutionRequestEvent(
new CustomEvent('frigate-card:action:execution-request', {
detail: { action: createLogAction("Hello, world!") },
}),
);
expect(consoleSpy).toBeCalled();
});
});
describe('executeAction', () => {
it('should execute actions', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
await manager.executeActions(createLogAction("Hello, world!"));
expect(consoleSpy).toBeCalled();
});
});
describe('uninitialize', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('should stop actions', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
const promise = manager.executeActions([
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
createAction({
frigate_card_action: 'sleep',
duration: {
m: 1,
},
})!,
createLogAction("Hello, world!"),
]);
// Stop inflight actions.
manager.uninitialize();
// Advance timers (causes the sleep to end).
vi.runOnlyPendingTimers();
await promise;
// Action set will not continue.
expect(consoleSpy).not.toBeCalled();
});
});
});
@@ -0,0 +1,19 @@
import { it } from 'vitest';
import { BaseAction } from '../../../../src/card-controller/actions/actions/base';
import { createCardAPI } from '../../../test-utils';
it('should construct', async () => {
const api = createCardAPI();
const action = new BaseAction(
{},
{
action: 'fire-dom-event',
},
);
await action.execute(api);
await action.stop();
// These methods have no observable effect on the base class, so this test is
// currently only providing coverage and proof of no exceptions!
});
@@ -0,0 +1,163 @@
import { describe, expect, it, vi } from 'vitest';
import { CameraSelectAction } from '../../../../src/card-controller/actions/actions/camera-select';
import { createCardAPI, createConfig, createView } from '../../../test-utils';
describe('should handle camera_select action', () => {
it('with valid camera and view', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('without config', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'timeline',
}),
);
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'timeline',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('with target view', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
// Change to clips view when the camera changes.
camera_select: 'clips',
},
}),
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'live',
}),
);
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'clips',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('with triggered camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(api.getTriggersManager().getMostRecentlyTriggeredCameraID).mockReturnValue(
'camera',
);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
triggered: true,
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
it('without camera or triggered camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(api.getTriggersManager().getMostRecentlyTriggeredCameraID).mockReturnValue(
'camera',
);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
it('without a current view', async () => {
const api = createCardAPI();
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_select',
camera: 'camera',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { CameraUIAction } from '../../../../src/card-controller/actions/actions/camera-ui';
import { createCardAPI } from '../../../test-utils';
it('should handle camera_ui action', async () => {
const api = createCardAPI();
const action = new CameraUIAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'camera_ui',
},
);
await action.execute(api);
expect(api.getCameraURLManager().openURL).toBeCalled();
});
@@ -0,0 +1,15 @@
import { expect, it } from "vitest";
import { DefaultAction } from "../../../../src/card-controller/actions/actions/default";
import { createCardAPI } from "../../../test-utils";
it('should handle default action', async () => {
const api = createCardAPI();
const action = new DefaultAction({}, {
action: 'fire-dom-event',
frigate_card_action: 'default',
});
await action.execute(api);
expect(api.getViewManager().setViewDefault).toBeCalled();
});
@@ -0,0 +1,16 @@
import { expect, it } from "vitest";
import { DisplayModeSelectAction } from "../../../../src/card-controller/actions/actions/display-mode-select";
import { createCardAPI } from "../../../test-utils";
it('should handle default action', async () => {
const api = createCardAPI();
const action = new DisplayModeSelectAction({}, {
action: 'fire-dom-event',
frigate_card_action: 'display_mode_select',
display_mode: 'grid',
});
await action.execute(api);
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { DownloadAction } from '../../../../src/card-controller/actions/actions/download';
import { createCardAPI } from '../../../test-utils';
it('should handle download action', async () => {
const api = createCardAPI();
const action = new DownloadAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'download',
},
);
await action.execute(api);
expect(api.getDownloadManager().downloadViewerMedia).toBeCalled();
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { ExpandAction } from '../../../../src/card-controller/actions/actions/expand';
import { createCardAPI } from '../../../test-utils';
it('should handle expand action', async () => {
const api = createCardAPI();
const action = new ExpandAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'expand',
},
);
await action.execute(api);
expect(api.getExpandManager().toggleExpanded).toBeCalled();
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { FullscreenAction } from '../../../../src/card-controller/actions/actions/fullscreen';
import { createCardAPI } from '../../../test-utils';
it('should handle fullscreen action', async () => {
const api = createCardAPI();
const action = new FullscreenAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'fullscreen',
},
);
await action.execute(api);
expect(api.getFullscreenManager().toggleFullscreen).toBeCalled();
});
@@ -0,0 +1,46 @@
import { describe, expect, it, vi } from 'vitest';
import { createCardAPI, createHASS, createLitElement } from '../../../test-utils';
import { GenericAction } from '../../../../src/card-controller/actions/actions/generic';
import { handleActionConfig } from '@dermotduffy/custom-card-helpers';
vi.mock('@dermotduffy/custom-card-helpers');
describe('should handle generic action', () => {
it('without hass', async () => {
const api = createCardAPI();
const action = new GenericAction(
{},
{
action: 'fire-dom-event',
},
);
await action.execute(api);
expect(handleActionConfig).not.toBeCalled();
});
// @vitest-environment jsdom
it('with hass', async () => {
const api = createCardAPI();
const hass = createHASS();
const element = createLitElement();
vi.mocked(api.getCardElementManager()).getElement.mockReturnValue(element);
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
const action = new GenericAction(
{},
{
action: 'fire-dom-event',
},
);
await action.execute(api);
expect(handleActionConfig).toBeCalledWith(
element,
hass,
{},
{ action: 'fire-dom-event' },
);
});
});
@@ -0,0 +1,24 @@
import { afterEach, expect, it, vi } from 'vitest';
import { LogAction } from '../../../../src/card-controller/actions/actions/log';
import { createCardAPI } from '../../../test-utils';
afterEach(() => {
vi.resetAllMocks();
});
it('should handle log action', async () => {
const api = createCardAPI();
const action = new LogAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'log',
message: 'Hello, world!',
level: 'warn',
},
);
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
await action.execute(api);
expect(spy).toBeCalledWith('Hello, world!');
});
@@ -0,0 +1,104 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MediaPlayerAction } from '../../../../src/card-controller/actions/actions/media-player';
import { createCardAPI, createView, createViewWithMedia } from '../../../test-utils';
afterEach(() => {
vi.resetAllMocks();
});
describe('should handle media_player action', () => {
it('to stop', async () => {
const api = createCardAPI();
const action = new MediaPlayerAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player_action: 'stop',
media_player: 'this_is_a_media_player',
},
);
await action.execute(api);
expect(api.getMediaPlayerManager().stop).toBeCalledWith('this_is_a_media_player');
});
it('to play live', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera',
view: 'live',
}),
);
const action = new MediaPlayerAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
},
);
await action.execute(api);
expect(api.getMediaPlayerManager().playLive).toBeCalledWith(
'this_is_a_media_player',
'camera',
);
});
it('to play media', async () => {
const api = createCardAPI();
const view = createViewWithMedia({
camera: 'camera',
view: 'media',
});
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
const action = new MediaPlayerAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
},
);
await action.execute(api);
expect(api.getMediaPlayerManager().playMedia).toBeCalledWith(
'this_is_a_media_player',
view.queryResults?.getSelectedResult(),
);
});
it('to play media without selected media', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'media',
}),
);
const action = new MediaPlayerAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player_action: 'play',
media_player: 'this_is_a_media_player',
},
);
await action.execute(api);
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
});
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { MenuToggleAction } from '../../../../src/card-controller/actions/actions/menu-toggle';
import { createCardAPI } from '../../../test-utils';
it('should handle menu toggle action', async () => {
const api = createCardAPI();
const action = new MenuToggleAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'menu_toggle',
},
);
await action.execute(api);
expect(api.getCardElementManager().toggleMenu).toBeCalled();
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { MicrophoneMuteAction } from '../../../../src/card-controller/actions/actions/microphone-mute';
import { createCardAPI } from '../../../test-utils';
it('should handle microphone_mute action', async () => {
const api = createCardAPI();
const action = new MicrophoneMuteAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'microphone_mute',
},
);
await action.execute(api);
expect(api.getMicrophoneManager().mute).toBeCalled();
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { createCardAPI } from '../../../test-utils';
import { MicrophoneUnmuteAction } from '../../../../src/card-controller/actions/actions/microphone-unmute';
it('should handle microphone_unmute action', async () => {
const api = createCardAPI();
const action = new MicrophoneUnmuteAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'microphone_unmute',
},
);
await action.execute(api);
expect(api.getMicrophoneManager().unmute).toBeCalled();
});
@@ -0,0 +1,26 @@
import { expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { MuteAction } from '../../../../src/card-controller/actions/actions/mute';
import { FrigateCardMediaPlayer } from '../../../../src/types';
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
it('should handle mute action', async () => {
const api = createCardAPI();
const player = mock<FrigateCardMediaPlayer>();
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const action = new MuteAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'mute',
},
);
await action.execute(api);
expect(player.mute).toBeCalled();
});
@@ -0,0 +1,26 @@
import { expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { PauseAction } from '../../../../src/card-controller/actions/actions/pause';
import { FrigateCardMediaPlayer } from '../../../../src/types';
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
it('should handle pause action', async () => {
const api = createCardAPI();
const player = mock<FrigateCardMediaPlayer>();
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const action = new PauseAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'pause',
},
);
await action.execute(api);
expect(player.pause).toBeCalled();
});
@@ -0,0 +1,26 @@
import { expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { PlayAction } from '../../../../src/card-controller/actions/actions/play';
import { FrigateCardMediaPlayer } from '../../../../src/types';
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
it('should handle play action', async () => {
const api = createCardAPI();
const player = mock<FrigateCardMediaPlayer>();
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const action = new PlayAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'play',
},
);
await action.execute(api);
expect(player.play).toBeCalled();
});
@@ -0,0 +1,21 @@
import { expect, it } from 'vitest';
import { createCardAPI } from '../../../test-utils';
import { PTZControlsAction } from '../../../../src/card-controller/actions/actions/ptz-controls';
it('should handle ptz_controls action', async () => {
const api = createCardAPI();
const action = new PTZControlsAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_controls',
enabled: true,
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith(
expect.objectContaining({ ptzControls: { enabled: true } }),
);
});
@@ -0,0 +1,479 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { PTZDigitalAction } from '../../../../src/card-controller/actions/actions/ptz-digital';
import {
PartialZoomSettings,
ZoomSettingsObserved,
} from '../../../../src/components-lib/zoom/types';
import { PTZAction } from '../../../../src/config/ptz';
import { createCardAPI, createView } from '../../../test-utils';
describe('should handle ptz digital action', () => {
const defaultSettings = {
pan: {
x: 50,
y: 50,
},
zoom: 1,
};
const createObserved = (
observed?: Partial<ZoomSettingsObserved>,
): ZoomSettingsObserved => ({
...defaultSettings,
isDefault: true,
unzoomed: true,
...observed,
});
it('should honor absolute parameters', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
absolute: {
zoom: 2,
pan: {
x: 3,
y: 4,
},
},
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {
pan: {
x: 3,
y: 4,
},
zoom: 2,
},
},
},
});
});
it('should return to default without absolute parameters or action', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {},
},
},
});
});
it('should do nothing without a view', async () => {
const api = createCardAPI();
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).not.toBeCalledWith();
});
it('should do nothing without a camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
// There is no media associated with a timeline, so there's no camera to
// change the PTZ settings for.
view: 'timeline',
}),
);
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).not.toBeCalled();
});
describe('should honor ptz_action', () => {
it.each([
[
'zoom_in',
'zoom_in' as const,
{
zoom: 1.1,
},
createObserved(),
],
[
'zoom_in at maximum zoom',
'zoom_in' as const,
{
zoom: 10,
},
createObserved({
zoom: 10,
}),
],
[
'zoom_out',
'zoom_out' as const,
{
zoom: 1.9,
},
createObserved({
zoom: 2,
}),
],
[
'zoom_out at minimum zoom',
'zoom_out' as const,
{
zoom: 1,
},
createObserved({
zoom: 1,
}),
],
[
'left',
'left' as const,
{
pan: {
x: 45,
y: 50,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'left at left edge',
'left' as const,
{
pan: {
x: 0,
y: 50,
},
},
createObserved({
pan: {
x: 0,
y: 50,
},
}),
],
[
'right',
'right' as const,
{
pan: {
x: 55,
y: 50,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'right at right edge',
'right' as const,
{
pan: {
x: 100,
y: 50,
},
},
createObserved({
pan: {
x: 100,
y: 50,
},
}),
],
[
'up',
'up' as const,
{
pan: {
x: 50,
y: 45,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'up at top edge',
'up' as const,
{
pan: {
x: 50,
y: 0,
},
},
createObserved({
pan: {
x: 50,
y: 0,
},
}),
],
[
'down',
'down' as const,
{
pan: {
x: 50,
y: 55,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'down at bottom edge',
'down' as const,
{
pan: {
x: 50,
y: 100,
},
},
createObserved({
pan: {
x: 50,
y: 100,
},
}),
],
[
'action with undefined observed',
'down' as const,
{
pan: {
x: 50,
y: 55,
},
},
],
])(
'%s',
async (
_testTitle: string,
ptzAction: PTZAction,
expectedSettings: PartialZoomSettings,
current?: ZoomSettingsObserved,
) => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
context: {
zoom: {
camera: {
observed: current,
},
},
},
}),
);
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_action: ptzAction,
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {
...defaultSettings,
...expectedSettings,
},
},
},
});
},
);
});
// @vitest-environment jsdom
describe('should honor ptz_phase', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('start', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_action: 'right',
ptz_phase: 'start',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {
...defaultSettings,
pan: {
x: 55,
y: 50,
},
},
},
},
});
// Update the context to reflect the first step.
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
context: {
zoom: {
camera: {
observed: createObserved({
pan: {
x: 55,
y: 50,
},
}),
},
},
},
}),
);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {
...defaultSettings,
pan: {
x: 60,
y: 50,
},
},
},
},
});
expect(api.getViewManager().setViewWithMergedContext).toBeCalledTimes(2);
action.stop();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithMergedContext).toBeCalledTimes(2);
});
it('stop', async () => {
const api = createCardAPI();
const context = {};
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const startAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_action: 'right',
ptz_phase: 'start',
});
await startAction.execute(api);
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
zoom: {
camera: {
observed: undefined,
requested: {
...defaultSettings,
pan: {
x: 55,
y: 50,
},
},
},
},
});
expect(api.getViewManager().setViewWithMergedContext).toBeCalledTimes(1);
const stopAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
frigate_card_action: 'ptz_digital',
ptz_phase: 'stop',
});
await stopAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithMergedContext).toBeCalledTimes(1);
});
});
});
@@ -0,0 +1,150 @@
import { describe, expect, it, vi } from 'vitest';
import { Capabilities } from '../../../../src/camera-manager/capabilities';
import { PTZMultiAction } from '../../../../src/card-controller/actions/actions/ptz-multi';
import {
createCameraManager,
createCardAPI,
createStore,
createView,
} from '../../../test-utils';
describe('should handle ptz multi action', () => {
describe.each([
['with explicit target_id', 'camera.office'],
['without explicit target_id', null],
])('%s', async (_testTitle: string, targetID: string | null) => {
it('should use real ptz when camera has ptz support', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.office',
}),
);
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
const action = new PTZMultiAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: 'left',
...(targetID && { target_id: targetID }),
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
preset: undefined,
},
);
expect(api.getViewManager().setViewWithMergedContext).not.toBeCalled();
});
it('should use digital ptz when camera does not have ptz support', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.office',
}),
);
const store = createStore([
{
cameraID: 'camera.office',
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
const action = new PTZMultiAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: 'right',
...(targetID && { target_id: targetID }),
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
zoom: {
'camera.office': {
observed: undefined,
requested: expect.objectContaining({
pan: {
x: 55,
y: 50,
},
zoom: 1,
}),
},
},
});
});
});
it('should do nothing without a view or explicit target_id', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
const action = new PTZMultiAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: 'right',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
expect(api.getViewManager().setViewWithMergedContext).not.toBeCalled();
});
it('should do nothing with a media-less view without an explicit target_id', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'timeline',
}),
);
const store = createStore([
{
cameraID: 'camera.office',
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
const action = new PTZMultiAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: 'right',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
expect(api.getViewManager().setViewWithMergedContext).not.toBeCalled();
});
});
@@ -0,0 +1,513 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { PTZAction } from '../../../../src/card-controller/actions/actions/ptz';
import {
createCameraConfig,
createCameraManager,
createCardAPI,
createStore,
createView,
} from '../../../test-utils';
import { Capabilities } from '../../../../src/camera-manager/capabilities';
describe('should handle ptz action', () => {
it('should execute simple action', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.office',
}),
);
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
camera: 'camera.office',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
preset: undefined,
},
);
});
describe('without explicit camera', () => {
it('when current camera supports PTZ', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
preset: undefined,
},
);
});
it('when substream supports PTZ', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
config: createCameraConfig({
dependencies: { cameras: ['camera.office_hd'] },
}),
},
{
cameraID: 'camera.office_hd',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.office',
context: {
live: {
overrides: new Map([['camera.office', 'camera.office_hd']]),
},
},
}),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office_hd',
'left',
{
phase: undefined,
preset: undefined,
},
);
});
it('when no camera supports PTZ', async () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
});
});
it('when there is no view', async () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
});
describe('when there is no action', () => {
it('should call first preset', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({ ptz: { presets: ['home'] } }),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'preset',
{
phase: undefined,
preset: 'home',
},
);
});
it('should not call preset when there are no presets', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['relative'],
presets: [],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).not.toBeCalled();
});
});
it('should execute preset', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
presets: ['window'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'preset',
ptz_preset: 'window',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'preset',
{
phase: undefined,
preset: 'window',
},
);
});
it('should execute action with phase', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['continuous'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
ptz_phase: 'start',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: 'start',
},
);
});
// @vitest-environment jsdom
describe('when relative is requested but unsupported', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('should emulate relative', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['continuous'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const action = new PTZAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: 'start',
},
);
vi.runOnlyPendingTimers();
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: 'stop',
},
);
});
it('should honor stop', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['continuous'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const context = {};
const action = new PTZAction(context, {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
});
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(1);
action.stop();
vi.runOnlyPendingTimers();
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(1);
});
});
describe('when continuous is requested but unsupported', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('should emulate continuous', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['relative'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const context = {};
const startAction = new PTZAction(
context,
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
ptz_phase: 'start',
},
);
await startAction.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledWith(
'camera.office',
'left',
{
phase: undefined,
},
);
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(1);
await vi.runOnlyPendingTimersAsync()
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
await vi.runOnlyPendingTimersAsync()
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(3);
const stopAction = new PTZAction(
context,
{
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
ptz_phase: 'stop',
},
);
await stopAction.execute(api);
// There should be no additional calls.
await vi.runOnlyPendingTimersAsync()
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(3);
});
it('should honor stop', async () => {
const api = createCardAPI();
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
left: ['relative'],
},
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ camera: 'camera.office' }),
);
const context = {};
const action = new PTZAction(context, {
action: 'fire-dom-event',
frigate_card_action: 'ptz',
ptz_action: 'left',
ptz_phase: 'start',
});
await action.execute(api);
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(1);
await vi.runOnlyPendingTimersAsync()
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
action.stop();
await vi.runOnlyPendingTimersAsync()
// There should be no additional calls.
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
});
});
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { ScreenshotAction } from '../../../../src/card-controller/actions/actions/screenshot';
import { createCardAPI } from '../../../test-utils';
it('should handle screenshot action', async () => {
const api = createCardAPI();
const action = new ScreenshotAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'screenshot',
},
);
await action.execute(api);
expect(api.getDownloadManager().downloadScreenshot).toBeCalled();
});
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from 'vitest';
import { ActionSet } from '../../../../src/card-controller/actions/actions/set';
import { createLogAction } from '../../../../src/utils/action';
import { createCardAPI } from '../../../test-utils';
describe('ActionSet', () => {
it('should execute single action', async () => {
const api = createCardAPI();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const set = new ActionSet({}, createLogAction('Hello, world!'));
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
await set.execute(api);
expect(consoleSpy).toBeCalled();
});
it('should not execute invalid action', async () => {
const api = createCardAPI();
const set = new ActionSet(
{},
createLogAction('Hello, world!', {
cardID: 'another-card',
}),
);
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
await set.execute(api);
expect(consoleSpy).not.toBeCalled();
});
it('should stop execution', async () => {
const api = createCardAPI();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const set = new ActionSet({}, createLogAction('Hello, world!'));
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
await set.stop();
await set.execute(api);
expect(consoleSpy).not.toBeCalled();
});
});
@@ -0,0 +1,29 @@
import { afterAll, expect, it, vi } from 'vitest';
import { createCardAPI } from '../../../test-utils';
import { SleepAction } from '../../../../src/card-controller/actions/actions/sleep';
import { sleep } from '../../../../src/utils/basic';
vi.mock('../../../../src/utils/basic');
afterAll(() => {
vi.restoreAllMocks();
});
it('should handle sleep action', async () => {
const api = createCardAPI();
const action = new SleepAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'sleep',
duration: {
s: 5,
ms: 200
},
},
);
await action.execute(api);
expect(sleep).toBeCalledWith(5.2);
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { SubstreamOffAction } from '../../../../src/card-controller/actions/actions/substream-off';
import { createCardAPI } from '../../../test-utils';
it('should handle live_substream_off action', async () => {
const api = createCardAPI();
const action = new SubstreamOffAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'live_substream_off',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithoutSubstream).toBeCalled();
});
@@ -0,0 +1,18 @@
import { expect, it } from 'vitest';
import { SubstreamOnAction } from '../../../../src/card-controller/actions/actions/substream-on';
import { createCardAPI } from '../../../test-utils';
it('should handle live_substream_on action', async () => {
const api = createCardAPI();
const action = new SubstreamOnAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'live_substream_on',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith();
});
@@ -0,0 +1,19 @@
import { expect, it } from 'vitest';
import { SubstreamSelectAction } from '../../../../src/card-controller/actions/actions/substream-select';
import { createCardAPI } from '../../../test-utils';
it('should handle live_substream_select action', async () => {
const api = createCardAPI();
const action = new SubstreamSelectAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'live_substream_select',
camera: 'substream',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream');
});
@@ -0,0 +1,26 @@
import { expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { UnmuteAction } from '../../../../src/card-controller/actions/actions/unmute';
import { FrigateCardMediaPlayer } from '../../../../src/types';
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
it('should handle unmute action', async () => {
const api = createCardAPI();
const player = mock<FrigateCardMediaPlayer>();
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
const action = new UnmuteAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: 'unmute',
},
);
await action.execute(api);
expect(player.unmute).toBeCalled();
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { ViewAction } from '../../../../src/card-controller/actions/actions/view';
import { createCardAPI } from '../../../test-utils';
describe('should handle view action', () => {
it.each([
['clip' as const],
['clips' as const],
['diagnostics' as const],
['image' as const],
['live' as const],
['recording' as const],
['recordings' as const],
['snapshot' as const],
['snapshots' as const],
['timeline' as const],
])('%s', async (viewName) => {
const api = createCardAPI();
const action = new ViewAction(
{},
{
action: 'fire-dom-event',
frigate_card_action: viewName,
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
viewName: viewName,
}),
);
});
});
@@ -0,0 +1,138 @@
import { describe, expect, it, vi } from 'vitest';
import { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
import { DefaultAction } from '../../../src/card-controller/actions/actions/default';
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
import { GenericAction } from '../../../src/card-controller/actions/actions/generic';
import { LogAction } from '../../../src/card-controller/actions/actions/log';
import { MediaPlayerAction } from '../../../src/card-controller/actions/actions/media-player';
import { MenuToggleAction } from '../../../src/card-controller/actions/actions/menu-toggle';
import { MicrophoneMuteAction } from '../../../src/card-controller/actions/actions/microphone-mute';
import { MicrophoneUnmuteAction } from '../../../src/card-controller/actions/actions/microphone-unmute';
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
import { PTZDigitalAction } from '../../../src/card-controller/actions/actions/ptz-digital';
import { PTZMultiAction } from '../../../src/card-controller/actions/actions/ptz-multi';
import { ScreenshotAction } from '../../../src/card-controller/actions/actions/screenshot';
import { SleepAction } from '../../../src/card-controller/actions/actions/sleep';
import { SubstreamOffAction } from '../../../src/card-controller/actions/actions/substream-off';
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
import { SubstreamSelectAction } from '../../../src/card-controller/actions/actions/substream-select';
import { UnmuteAction } from '../../../src/card-controller/actions/actions/unmute';
import { ViewAction } from '../../../src/card-controller/actions/actions/view';
import { ActionFactory } from '../../../src/card-controller/actions/factory';
import { FrigateCardCustomAction } from '../../../src/config/types';
// @vitest-environment jsdom
describe('ActionFactory', () => {
it('mismatched card-id', () => {
const factory = new ActionFactory();
expect(
factory.createAction(
{},
{ action: 'fire-dom-event', frigate_card_action: 'clip', card_id: 'card_id' },
{
cardID: 'different_card_id',
},
),
).toBeNull();
});
describe('generic', () => {
it('non frigate card action', () => {
const factory = new ActionFactory();
expect(factory.createAction({}, { action: 'fire-dom-event' })).toBeInstanceOf(
GenericAction,
);
});
it('non fire-dom-event', () => {
const factory = new ActionFactory();
expect(factory.createAction({}, { action: 'more-info' })).toBeInstanceOf(
GenericAction,
);
});
});
describe('actions', () => {
it.each([
[{ frigate_card_action: 'camera_select' as const }, CameraSelectAction],
[{ frigate_card_action: 'camera_ui' as const }, CameraUIAction],
[{ frigate_card_action: 'clip' as const }, ViewAction],
[{ frigate_card_action: 'clips' as const }, ViewAction],
[{ frigate_card_action: 'default' as const }, DefaultAction],
[{ frigate_card_action: 'diagnostics' as const }, ViewAction],
[
{
frigate_card_action: 'display_mode_select' as const,
display_mode: 'single' as const,
},
DisplayModeSelectAction,
],
[{ frigate_card_action: 'download' as const }, DownloadAction],
[{ frigate_card_action: 'expand' as const }, ExpandAction],
[{ frigate_card_action: 'fullscreen' as const }, FullscreenAction],
[{ frigate_card_action: 'image' as const }, ViewAction],
[{ frigate_card_action: 'live_substream_off' as const }, SubstreamOffAction],
[{ frigate_card_action: 'live_substream_on' as const }, SubstreamOnAction],
[
{
frigate_card_action: 'live_substream_select' as const,
camera: 'camera.office',
},
SubstreamSelectAction,
],
[{ frigate_card_action: 'live' as const }, ViewAction],
[
{ frigate_card_action: 'log' as const, message: 'Hello, world!' as const },
LogAction,
],
[
{
frigate_card_action: 'media_player' as const,
media_player: 'media_player.foo' as const,
media_player_action: 'play' as const,
},
MediaPlayerAction,
],
[{ frigate_card_action: 'menu_toggle' as const }, MenuToggleAction],
[{ frigate_card_action: 'microphone_mute' as const }, MicrophoneMuteAction],
[{ frigate_card_action: 'microphone_unmute' as const }, MicrophoneUnmuteAction],
[{ frigate_card_action: 'mute' as const }, MuteAction],
[{ frigate_card_action: 'pause' as const }, PauseAction],
[{ frigate_card_action: 'play' as const }, PlayAction],
[{ frigate_card_action: 'ptz_digital' as const }, PTZDigitalAction],
[
{ frigate_card_action: 'ptz_multi' as const, ptz_action: 'right' as const },
PTZMultiAction,
],
[{ frigate_card_action: 'ptz' as const, ptz_action: 'right' as const }, PTZAction],
[{ frigate_card_action: 'recording' as const }, ViewAction],
[{ frigate_card_action: 'recordings' as const }, ViewAction],
[{ frigate_card_action: 'screenshot' as const }, ScreenshotAction],
[
{ frigate_card_action: 'ptz_controls' as const, enabled: true },
PTZControlsAction,
],
[{ frigate_card_action: 'sleep' as const }, SleepAction],
[{ frigate_card_action: 'snapshot' as const }, ViewAction],
[{ frigate_card_action: 'snapshots' as const }, ViewAction],
[{ frigate_card_action: 'timeline' as const }, ViewAction],
[{ frigate_card_action: 'unmute' as const }, UnmuteAction],
])(
'frigate_card_action: $frigate_card_action',
(action: Partial<FrigateCardCustomAction>, classObject: object) => {
const factory = new ActionFactory();
expect(
factory.createAction({}, { action: 'fire-dom-event', ...action }),
).toBeInstanceOf(classObject);
},
);
});
});
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { timeDeltaToSeconds } from '../../../../src/card-controller/actions/utils/time-delta';
describe('timeDeltaToSeconds', () => {
it('hours', () => {
expect(timeDeltaToSeconds({ h: 1 })).toBe(3600);
});
it('minutes', () => {
expect(timeDeltaToSeconds({ m: 1 })).toBe(60);
});
it('seconds', () => {
expect(timeDeltaToSeconds({ s: 1 })).toBe(1);
});
it('milliseconds', () => {
expect(timeDeltaToSeconds({ ms: 1 })).toBe(0.001);
});
it('combination', () => {
expect(timeDeltaToSeconds({ h: 1, m: 2, s: 3, ms: 4 })).toBe(3723.004);
});
});
@@ -1,18 +1,25 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
import { frigateCardHandleAction } from '../../src/utils/action.js';
import { createCardAPI, createConfig, createHASS } from '../test-utils.js';
vi.mock('../../src/utils/action.js');
import { createCardAPI, createHASS } from '../test-utils.js';
import { ActionType } from '../../src/config/types.js';
import { AuxillaryActionConfig } from '../../src/card-controller/actions/types.js';
describe('AutomationsManager', () => {
const actions = [
{
action: 'custom:frigate-card-action',
action: 'fire-dom-event' as const,
frigate_card_action: 'clips',
},
];
const conditions = [{ condition: 'fullscreen', fullscreen: true }];
const conditions = [{ condition: 'fullscreen' as const, fullscreen: true }];
const automation = {
conditions: conditions,
actions: actions,
};
const not_automation = {
conditions: conditions,
actions_not: actions,
};
afterEach(() => {
vi.clearAllMocks();
@@ -23,7 +30,8 @@ describe('AutomationsManager', () => {
const automationsManager = new AutomationsManager(api);
automationsManager.execute();
expect(frigateCardHandleAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
it('should do nothing without automations', () => {
@@ -31,105 +39,79 @@ describe('AutomationsManager', () => {
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const automationsManager = new AutomationsManager(api);
automationsManager.setAutomationsFromConfig();
automationsManager.execute();
expect(frigateCardHandleAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
it('should execute actions', () => {
const config = createConfig({
automations: [
{
conditions: conditions,
actions: actions,
},
],
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
const automationsManager = new AutomationsManager(api);
automationsManager.setAutomationsFromConfig();
automationsManager.addAutomations([automation]);
automationsManager.execute();
expect(frigateCardHandleAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
expect(frigateCardHandleAction).toBeCalledTimes(1);
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
// Automation will not re-fire when condition continues to evaluate the
// same.
automationsManager.execute();
expect(frigateCardHandleAction).toBeCalledTimes(1);
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
automationsManager.execute();
expect(frigateCardHandleAction).toBeCalledTimes(1);
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
expect(frigateCardHandleAction).toBeCalledTimes(2);
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
});
it('should execute actions_not', () => {
const config = createConfig({
automations: [
{
conditions: conditions,
actions_not: actions,
},
],
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(false);
const automationsManager = new AutomationsManager(api);
automationsManager.setAutomationsFromConfig();
automationsManager.addAutomations([not_automation]);
automationsManager.execute();
expect(frigateCardHandleAction).toBeCalled();
expect(api.getActionsManager().executeActions).toBeCalled();
});
it('should prevent automation loops', () => {
const config = createConfig({
automations: [
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions: actions,
},
{
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
actions_not: actions,
},
],
});
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
const automationsManager = new AutomationsManager(api);
automationsManager.setAutomationsFromConfig();
automationsManager.addAutomations([automation, not_automation]);
// Create a setup where one automation action causes another...
let evaluation = true;
vi.mocked(frigateCardHandleAction).mockImplementation(() => {
evaluation = !evaluation;
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(
evaluation,
);
automationsManager.execute();
});
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
async (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_action: ActionType | ActionType[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config?: AuxillaryActionConfig,
): Promise<void> => {
evaluation = !evaluation;
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(
evaluation,
);
automationsManager.execute();
},
);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(evaluation);
@@ -143,6 +125,24 @@ describe('AutomationsManager', () => {
}),
);
expect(frigateCardHandleAction).toBeCalledTimes(10);
expect(api.getActionsManager().executeActions).toBeCalledTimes(10);
});
it('should delete automations', () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const automationsManager = new AutomationsManager(api);
automationsManager.addAutomations([automation]);
vi.mocked(api.getConditionsManager().evaluateConditions).mockReturnValue(true);
automationsManager.execute();
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
automationsManager.deleteAutomations();
automationsManager.execute();
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
});
});
@@ -119,7 +119,7 @@ describe('CardElementManager', () => {
);
expect(addEventListener).toBeCalledWith(
'll-custom',
api.getActionsManager().handleActionEvent,
api.getActionsManager().handleCustomActionEvent,
);
expect(addEventListener).toBeCalledWith(
'action',
@@ -169,7 +169,7 @@ describe('CardElementManager', () => {
);
expect(removeEventListener).toBeCalledWith(
'll-custom',
api.getActionsManager().handleActionEvent,
api.getActionsManager().handleCustomActionEvent,
);
expect(removeEventListener).toBeCalledWith(
'action',
@@ -185,6 +185,10 @@ describe('CardElementManager', () => {
);
expect(windowRemoveEventListener).toBeCalledWith('popstate', expect.anything());
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
expect(api.getFullscreenManager().disconnect).toBeCalled();
expect(api.getKeyboardStateManager().uninitialize).toBeCalled();
expect(api.getActionsManager().uninitialize).toBeCalled();
expect(api.getInitializationManager().uninitialize).toBeCalledWith('cameras');
});
});
@@ -930,5 +930,60 @@ describe('ConditionsManager', () => {
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
});
describe('with key condition', () => {
it('simple keypress', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'key' as const, key: 'a' }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
keys: {
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
},
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({
keys: {
a: { state: 'up', ctrl: false, shift: false, alt: false, meta: false },
},
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
it('keypress with modifiers', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [
{
condition: 'key' as const,
key: 'a',
state: 'down' as const,
ctrl: true,
shift: true,
alt: true,
meta: true,
},
];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
keys: {
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
},
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
keys: {
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: false },
},
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
keys: {
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: true },
},
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
});
});
});
@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ZodError } from 'zod';
import { frigateCardConfigSchema } from '../../src/config/types';
import { getOverriddenConfig } from '../../src/card-controller/conditions-manager';
import { ConfigManager } from '../../src/card-controller/config-manager';
import { InitializationAspect } from '../../src/card-controller/initialization-manager';
import { createCardAPI, createConfig } from '../test-utils';
import { frigateCardConfigSchema } from '../../../src/config/types';
import { getOverriddenConfig } from '../../../src/card-controller/conditions-manager';
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
import { createCardAPI, createConfig } from '../../test-utils';
vi.mock('../../src/card-controller/conditions-manager.js');
vi.mock('../../../src/card-controller/conditions-manager.js');
describe('ConfigManager', () => {
beforeEach(() => {
@@ -97,7 +97,7 @@ describe('ConfigManager', () => {
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
expect(api.getViewManager().reset).toBeCalled();
expect(api.getMessageManager().reset).toBeCalled();
expect(api.getAutomationsManager().setAutomationsFromConfig).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalled();
expect(api.getStyleManager().setPerformance).toBeCalled();
expect(api.getCardElementManager().update).toBeCalled();
});
@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest';
import { createCardAPI, createConfig } from '../../test-utils';
import { setAutomationsFromConfig } from '../../../src/card-controller/config/load-automations';
describe('setAutomationsFromConfig', () => {
it('without config', () => {
const api = createCardAPI();
setAutomationsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([]);
});
it('with config', () => {
const automations = [
{
actions: [
{
action: 'fire-dom-event' as const,
frigate_card_action: 'clips',
},
],
conditions: [{ condition: 'fullscreen' as const, fullscreen: true }],
},
];
const api = createCardAPI();
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(
createConfig({
automations: automations,
}),
);
setAutomationsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalledWith(automations);
});
});
@@ -0,0 +1,143 @@
import { describe, expect, it, vi } from 'vitest';
import { createCardAPI, createConfig } from '../../test-utils';
import { setKeyboardShortcutsFromConfig } from '../../../src/card-controller/config/load-keyboard-shortcuts';
import { PTZKeyboardShortcutName } from '../../../src/config/keyboard-shortcuts';
import { PTZAction } from '../../../src/config/ptz';
describe('setKeyboardShortcutsFromConfig', () => {
it('without shortcuts', () => {
const api = createCardAPI();
setKeyboardShortcutsFromConfig(api, 'tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
it('with shortcuts disabled', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
keyboard_shortcuts: {
enabled: false,
},
},
}),
);
setKeyboardShortcutsFromConfig(api, 'tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
describe('PTZ shortcuts', () => {
describe('actions', () => {
it.each([
['ptz_left' as const, 'left' as const],
['ptz_right' as const, 'right' as const],
['ptz_up' as const, 'up' as const],
['ptz_down' as const, 'down' as const],
['ptz_zoom_in' as const, 'zoom_in' as const],
['ptz_zoom_out' as const, 'zoom_out' as const],
])('%s', (name: PTZKeyboardShortcutName, ptzAction: PTZAction) => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
keyboard_shortcuts: {
enabled: true,
ptz_home: null,
ptz_left: null,
ptz_right: null,
ptz_up: null,
ptz_down: null,
ptz_zoom_in: null,
ptz_zoom_out: null,
[name]: { key: 'z' },
},
},
}),
);
setKeyboardShortcutsFromConfig(api, 'tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([
{
actions: [
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: ptzAction,
ptz_phase: 'start',
},
],
conditions: [
{
alt: undefined,
condition: 'key',
ctrl: undefined,
key: 'z',
meta: undefined,
shift: undefined,
state: 'down',
},
],
tag: 'tag',
},
{
actions: [
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
ptz_action: ptzAction,
ptz_phase: 'stop',
},
],
conditions: [
{
condition: 'key',
key: 'z',
state: 'up',
},
],
tag: 'tag',
},
]);
});
it('ptz_home', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
setKeyboardShortcutsFromConfig(api, 'tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().addAutomations).toBeCalledWith(
expect.arrayContaining([
{
actions: [
{
action: 'fire-dom-event',
frigate_card_action: 'ptz_multi',
},
],
conditions: [
{
alt: undefined,
condition: 'key',
ctrl: undefined,
key: 'h',
meta: undefined,
shift: undefined,
state: 'down',
},
],
tag: 'tag',
},
]),
);
});
});
});
});
+13 -5
View File
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CameraManager } from '../../src/camera-manager/manager';
import { FrigateCardEditor } from '../../src/editor';
import { ActionsManager } from '../../src/card-controller/actions-manager';
import { ActionsManager } from '../../src/card-controller/actions/actions-manager';
import { AutoUpdateManager } from '../../src/card-controller/auto-update-manager';
import { AutomationsManager } from '../../src/card-controller/automations-manager';
import { CameraURLManager } from '../../src/card-controller/camera-url-manager';
@@ -10,7 +9,7 @@ import {
CardHTMLElement,
} from '../../src/card-controller/card-element-manager';
import { ConditionsManager } from '../../src/card-controller/conditions-manager';
import { ConfigManager } from '../../src/card-controller/config-manager';
import { ConfigManager } from '../../src/card-controller/config/config-manager';
import { CardController } from '../../src/card-controller/controller';
import { DownloadManager } from '../../src/card-controller/download-manager';
import { ExpandManager } from '../../src/card-controller/expand-manager';
@@ -18,6 +17,7 @@ import { FullscreenManager } from '../../src/card-controller/fullscreen-manager'
import { HASSManager } from '../../src/card-controller/hass-manager';
import { InitializationManager } from '../../src/card-controller/initialization-manager';
import { InteractionManager } from '../../src/card-controller/interaction-manager';
import { KeyboardStateManager } from '../../src/card-controller/keyboard-state-manager';
import { MediaLoadedInfoManager } from '../../src/card-controller/media-info-manager';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
import { MessageManager } from '../../src/card-controller/message-manager';
@@ -26,23 +26,25 @@ import { QueryStringManager } from '../../src/card-controller/query-string-manag
import { StyleManager } from '../../src/card-controller/style-manager';
import { TriggersManager } from '../../src/card-controller/triggers-manager';
import { ViewManager } from '../../src/card-controller/view-manager';
import { FrigateCardEditor } from '../../src/editor';
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
vi.mock('../../src/camera-manager/manager');
vi.mock('../../src/card-controller/actions-manager');
vi.mock('../../src/card-controller/actions/actions-manager');
vi.mock('../../src/card-controller/auto-update-manager');
vi.mock('../../src/card-controller/automations-manager');
vi.mock('../../src/card-controller/camera-url-manager');
vi.mock('../../src/card-controller/card-element-manager');
vi.mock('../../src/card-controller/conditions-manager');
vi.mock('../../src/card-controller/config-manager');
vi.mock('../../src/card-controller/config/config-manager');
vi.mock('../../src/card-controller/download-manager');
vi.mock('../../src/card-controller/expand-manager');
vi.mock('../../src/card-controller/fullscreen-manager');
vi.mock('../../src/card-controller/hass-manager');
vi.mock('../../src/card-controller/initialization-manager');
vi.mock('../../src/card-controller/interaction-manager');
vi.mock('../../src/card-controller/keyboard-state-manager');
vi.mock('../../src/card-controller/media-info-manager');
vi.mock('../../src/card-controller/media-player-manager');
vi.mock('../../src/card-controller/message-manager');
@@ -187,6 +189,12 @@ describe('CardController', () => {
);
});
it('getKeyboardStateManager', () => {
expect(createController().getKeyboardStateManager()).toBe(
vi.mocked(KeyboardStateManager).mock.instances[0],
);
});
it('getMediaLoadedInfoManager', () => {
expect(createController().getMediaLoadedInfoManager()).toBe(
vi.mocked(MediaLoadedInfoManager).mock.instances[0],
@@ -0,0 +1,86 @@
import { describe, expect, it, vi } from 'vitest';
import { KeyboardStateManager } from '../../src/card-controller/keyboard-state-manager';
import { createCardAPI, createLitElement } from '../test-utils';
// @vitest-environment jsdom
describe('KeyboardStateManager', () => {
it('should construct', () => {
expect(new KeyboardStateManager(createCardAPI())).toBeTruthy();
});
it('should set state on keydown', () => {
const api = createCardAPI();
const element = createLitElement();
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new KeyboardStateManager(api);
manager.initialize();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
expect(api.getConditionsManager().setState).toHaveBeenCalledWith({
keys: {
a: { state: 'down', ctrl: false, alt: false, meta: false, shift: false },
},
});
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
// Duplicate keydown should not re-set the state.
expect(api.getConditionsManager().setState).toBeCalledTimes(1);
});
it('should set state on keyup', () => {
const api = createCardAPI();
const element = createLitElement();
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new KeyboardStateManager(api);
manager.initialize();
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
// Key not held down in the first place should not update the state.
expect(api.getConditionsManager().setState).not.toBeCalled();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' }));
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
keys: {
a: { state: 'up', ctrl: false, alt: false, meta: false, shift: false },
},
});
});
it('should set state on focus loss', () => {
const api = createCardAPI();
const element = createLitElement();
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new KeyboardStateManager(api);
manager.initialize();
element.dispatchEvent(new FocusEvent('blur'));
expect(api.getConditionsManager().setState).not.toBeCalled();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
element.dispatchEvent(new FocusEvent('blur'));
expect(api.getConditionsManager().setState).toBeCalledTimes(2);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith({
keys: {},
});
});
it('should not act after uninitialization', () => {
const api = createCardAPI();
const element = createLitElement();
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new KeyboardStateManager(api);
manager.initialize();
manager.uninitialize();
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
expect(api.getConditionsManager().setState).not.toBeCalled();
});
});
@@ -1,10 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
import {
FrigateCardGeneralAction,
FrigateCardUserSpecifiedView,
} from '../../src/config/types';
import { createCardAPI } from '../test-utils';
const setQueryString = (qs: string): void => {
@@ -28,7 +24,7 @@ describe('QueryStringManager', () => {
manager.executeAll();
expect(manager.hasViewRelatedActions()).toBeFalsy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
@@ -76,11 +72,13 @@ describe('QueryStringManager', () => {
manager.executeAll();
expect(manager.hasViewRelatedActions()).toBeFalsy();
expect(api.getActionsManager().executeFrigateAction).toBeCalledWith({
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: action,
});
expect(api.getActionsManager().executeActions).toBeCalledWith([
{
action: 'fire-dom-event',
card_id: 'id',
frigate_card_action: action,
},
]);
});
});
@@ -97,7 +95,7 @@ describe('QueryStringManager', () => {
expect(api.getViewManager().setViewDefault).toBeCalled();
expect(manager.hasViewRelatedActions()).toBeTruthy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
@@ -114,7 +112,7 @@ describe('QueryStringManager', () => {
});
expect(manager.hasViewRelatedActions()).toBeTruthy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
@@ -131,7 +129,7 @@ describe('QueryStringManager', () => {
});
expect(manager.hasViewRelatedActions()).toBeTruthy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
});
@@ -147,7 +145,7 @@ describe('QueryStringManager', () => {
manager.executeAll();
expect(manager.hasViewRelatedActions()).toBeFalsy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
},
@@ -165,7 +163,7 @@ describe('QueryStringManager', () => {
manager.executeAll();
expect(manager.hasViewRelatedActions()).toBeFalsy();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
expect(consoleSpy).toBeCalled();
@@ -212,7 +210,7 @@ describe('QueryStringManager', () => {
manager.executeAll();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
expect(api.getViewManager().setViewDefault).not.toBeCalled();
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
@@ -296,7 +294,7 @@ describe('QueryStringManager', () => {
manager.executeViewRelated();
expect(api.getActionsManager().executeFrigateAction).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
});
});