Add initial keyboard shortcut support.
This commit is contained in:
@@ -68,17 +68,19 @@ describe('Capabilities', () => {
|
||||
it('when unset', () => {
|
||||
const capabilities = new Capabilities({});
|
||||
expect(capabilities.getPTZCapabilities()).toBeNull();
|
||||
expect(capabilities.hasPTZCapability()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('when set', () => {
|
||||
const ptz: PTZCapabilities = {
|
||||
panTilt: ['continuous' as const],
|
||||
left: ['continuous' as const],
|
||||
presets: ['1', '2'],
|
||||
};
|
||||
const capabilities = new Capabilities({
|
||||
ptz: ptz,
|
||||
});
|
||||
expect(capabilities.getPTZCapabilities()).toBe(ptz);
|
||||
expect(capabilities.hasPTZCapability()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ describe('FrigateCamera', () => {
|
||||
await camera.initialize(createHASS(), mock<EntityRegistryManager>());
|
||||
|
||||
expect(camera.getCapabilities()?.has('ptz')).toBeFalsy();
|
||||
expect(camera.getCapabilities()?.hasPTZCapability()).toBeFalsy();
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -214,10 +215,15 @@ describe('FrigateCamera', () => {
|
||||
|
||||
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
panTilt: ['continuous'],
|
||||
zoom: ['continuous'],
|
||||
left: ['continuous'],
|
||||
right: ['continuous'],
|
||||
up: ['continuous'],
|
||||
down: ['continuous'],
|
||||
zoomIn: ['continuous'],
|
||||
zoomOut: ['continuous'],
|
||||
presets: ['preset01'],
|
||||
});
|
||||
expect(camera.getCapabilities()?.hasPTZCapability()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('when getPTZInfo call succeeds with relative motion', async () => {
|
||||
@@ -240,10 +246,15 @@ describe('FrigateCamera', () => {
|
||||
|
||||
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
panTilt: ['relative'],
|
||||
zoom: ['relative'],
|
||||
left: [],
|
||||
right: [],
|
||||
up: [],
|
||||
down: [],
|
||||
zoomIn: [],
|
||||
zoomOut: [],
|
||||
presets: ['preset01'],
|
||||
});
|
||||
expect(camera.getCapabilities()?.hasPTZCapability()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
FrigateRecordingViewMedia,
|
||||
} from '../../../src/camera-manager/frigate/media';
|
||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
||||
import { PTZAction } from '../../../src/config/ptz';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateCardView,
|
||||
PTZAction,
|
||||
RawFrigateCardConfig,
|
||||
} from '../../../src/config/types';
|
||||
import { ViewMedia } from '../../../src/view/media';
|
||||
|
||||
@@ -1070,7 +1070,7 @@ describe('CameraManager', async () => {
|
||||
snapshots: true,
|
||||
|
||||
ptz: {
|
||||
panTilt: ['continuous'],
|
||||
left: ['continuous'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -1102,7 +1102,50 @@ describe('CameraManager', async () => {
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('successfully', async () => {
|
||||
it('without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const manager = createCameraManager(api, engine);
|
||||
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||
manager.executePTZAction('id', 'left');
|
||||
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('successfully from config', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
const action = {
|
||||
action: 'call-service' as const,
|
||||
service: 'service',
|
||||
}
|
||||
const manager = createCameraManager(api, engine, [
|
||||
{
|
||||
config: createCameraConfig({
|
||||
baseCameraConfig,
|
||||
id: 'another',
|
||||
ptz: {
|
||||
actions_left: action,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
manager.executePTZAction('another', 'left');
|
||||
|
||||
expect(api.getActionsManager().executeActions).toBeCalledWith(action);
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('successfully from engine', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const hass = createHASS();
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
getConfiguredPTZAction,
|
||||
getConfiguredPTZMovementType,
|
||||
getPTZCapabilitiesFromCameraConfig,
|
||||
} from '../../../src/camera-manager/utils/ptz';
|
||||
import { createCameraConfig } from '../../test-utils';
|
||||
import { PTZAction } from '../../../src/config/ptz';
|
||||
|
||||
const action = {
|
||||
action: 'call-service' as const,
|
||||
service: 'service',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: 'preset',
|
||||
preset: 'window',
|
||||
},
|
||||
};
|
||||
|
||||
describe('getConfiguredPTZAction', () => {
|
||||
describe('should return preset', () => {
|
||||
it('with preset', () => {
|
||||
expect(
|
||||
getConfiguredPTZAction(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
presets: {
|
||||
window: action,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'preset',
|
||||
{
|
||||
preset: 'window',
|
||||
},
|
||||
),
|
||||
).toEqual(action);
|
||||
});
|
||||
|
||||
it('without preset', () => {
|
||||
expect(
|
||||
getConfiguredPTZAction(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
presets: {
|
||||
window: action
|
||||
},
|
||||
},
|
||||
}),
|
||||
'preset',
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return continuous action', () => {
|
||||
it('with action', () => {
|
||||
expect(
|
||||
getConfiguredPTZAction(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
actions_left_start: action,
|
||||
},
|
||||
}),
|
||||
'left',
|
||||
{
|
||||
phase: 'start',
|
||||
},
|
||||
),
|
||||
).toEqual(action);
|
||||
});
|
||||
|
||||
it('without action', () => {
|
||||
expect(
|
||||
getConfiguredPTZAction(
|
||||
createCameraConfig({
|
||||
ptz: {},
|
||||
}),
|
||||
'left',
|
||||
{
|
||||
phase: 'start',
|
||||
},
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfiguredPTZMovementType', () => {
|
||||
it('with continuous', () => {
|
||||
expect(
|
||||
getConfiguredPTZMovementType(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
actions_left_start: action,
|
||||
actions_left_stop: action,
|
||||
},
|
||||
}),
|
||||
'left',
|
||||
),
|
||||
)?.toEqual(['continuous']);
|
||||
});
|
||||
|
||||
it('with relative', () => {
|
||||
expect(
|
||||
getConfiguredPTZMovementType(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
actions_left: action,
|
||||
},
|
||||
}),
|
||||
'left',
|
||||
),
|
||||
)?.toEqual(['relative']);
|
||||
});
|
||||
|
||||
it('with continuous and relative', () => {
|
||||
expect(
|
||||
getConfiguredPTZMovementType(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
actions_left: action,
|
||||
actions_left_start: action,
|
||||
actions_left_stop: action,
|
||||
},
|
||||
}),
|
||||
'left',
|
||||
),
|
||||
)?.toEqual(['continuous', 'relative']);
|
||||
});
|
||||
|
||||
it('with no actions', () => {
|
||||
expect(
|
||||
getConfiguredPTZMovementType(
|
||||
createCameraConfig({
|
||||
ptz: {},
|
||||
}),
|
||||
'left',
|
||||
),
|
||||
)?.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPTZCapabilitiesFromCameraConfig', () => {
|
||||
it('with nothing', () => {
|
||||
expect(getPTZCapabilitiesFromCameraConfig(createCameraConfig()))?.toBeNull();
|
||||
});
|
||||
|
||||
describe('with individual actions', () => {
|
||||
it.each([
|
||||
['left' as const, 'left'],
|
||||
['right' as const, 'right'],
|
||||
['up' as const, 'up'],
|
||||
['down' as const, 'down'],
|
||||
['zoom_in' as const, 'zoomIn'],
|
||||
['zoom_out' as const, 'zoomOut'],
|
||||
])('%s', async (actionName: PTZAction, capabilityName: string) => {
|
||||
expect(
|
||||
getPTZCapabilitiesFromCameraConfig(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
['actions_' + actionName]: action,
|
||||
},
|
||||
}),
|
||||
),
|
||||
)?.toEqual(
|
||||
expect.objectContaining({
|
||||
[capabilityName]: ['relative'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('with preset', () => {
|
||||
expect(
|
||||
getPTZCapabilitiesFromCameraConfig(
|
||||
createCameraConfig({
|
||||
ptz: {
|
||||
presets: {
|
||||
window: action,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)?.toEqual(
|
||||
expect.objectContaining({
|
||||
presets: ['window'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+7
-7
@@ -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',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { KeyAssignerController } from '../../src/components-lib/key-assigner-controller';
|
||||
import { createLitElement } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('KeyAssignerController', () => {
|
||||
it('should be creatable', () => {
|
||||
const controller = new KeyAssignerController(createLitElement());
|
||||
expect(controller).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should manage value', () => {
|
||||
it('should have no value to start', () => {
|
||||
const controller = new KeyAssignerController(createLitElement());
|
||||
expect(controller.hasValue()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set value', () => {
|
||||
const element = createLitElement();
|
||||
const valueChangeHandler = vi.fn();
|
||||
element.addEventListener('value-changed', valueChangeHandler);
|
||||
const controller = new KeyAssignerController(element);
|
||||
|
||||
controller.setValue({ key: 'ArrowLeft' });
|
||||
|
||||
expect(controller.hasValue()).toBeTruthy();
|
||||
expect(controller.getValue()).toEqual({ key: 'ArrowLeft' });
|
||||
expect(element.requestUpdate).toBeCalled();
|
||||
expect(valueChangeHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { value: { key: 'ArrowLeft' } },
|
||||
}),
|
||||
);
|
||||
|
||||
// Set again with the same value.
|
||||
controller.setValue({ key: 'ArrowLeft' });
|
||||
expect(element.requestUpdate).toBeCalledTimes(1);
|
||||
expect(valueChangeHandler).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should manage assignment state', () => {
|
||||
it('should not be assigned to start', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
expect(controller.isAssigning()).toBeFalsy();
|
||||
expect(element.getAttribute('assigning')).toBeNull();
|
||||
});
|
||||
|
||||
it('should toggle assigning', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
controller.toggleAssigning();
|
||||
expect(element.requestUpdate).toBeCalled();
|
||||
expect(controller.isAssigning()).toBeTruthy();
|
||||
expect(element.getAttribute('assigning')).toBe('');
|
||||
|
||||
expect(controller.hasValue()).toBeFalsy();
|
||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||
|
||||
expect(controller.hasValue()).toBeTruthy();
|
||||
expect(controller.isAssigning()).toBeFalsy();
|
||||
|
||||
controller.setValue(null);
|
||||
|
||||
// A key sent when not assigning will do nothing.
|
||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||
expect(controller.hasValue()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not assign when focus lost', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
|
||||
controller.hostConnected();
|
||||
|
||||
controller.toggleAssigning();
|
||||
expect(controller.isAssigning()).toBeTruthy();
|
||||
|
||||
element.dispatchEvent(new FocusEvent('blur'));
|
||||
expect(controller.isAssigning()).toBeFalsy();
|
||||
|
||||
controller.hostDisconnected();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should manage key down', () => {
|
||||
it('should reject modifiers only', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
controller.toggleAssigning();
|
||||
|
||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Control' }));
|
||||
expect(controller.hasValue()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should reject empty key', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
controller.toggleAssigning();
|
||||
|
||||
element.dispatchEvent(new KeyboardEvent('keydown', { key: '' }));
|
||||
expect(controller.hasValue()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should accept valid key', () => {
|
||||
const element = createLitElement();
|
||||
const controller = new KeyAssignerController(element);
|
||||
controller.toggleAssigning();
|
||||
|
||||
element.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'ArrowLeft',
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
metaKey: true,
|
||||
}),
|
||||
);
|
||||
expect(controller.hasValue()).toBeTruthy();
|
||||
expect(controller.getValue()).toEqual({
|
||||
key: 'ArrowLeft',
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
alt: true,
|
||||
meta: true,
|
||||
});
|
||||
expect(controller.isAssigning()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ViewDisplayMode,
|
||||
} from '../../src/config/types';
|
||||
import { FrigateCardMediaPlayer } from '../../src/types';
|
||||
import { createFrigateCardSimpleAction } from '../../src/utils/action';
|
||||
import { createGeneralAction } from '../../src/utils/action';
|
||||
import { ViewMedia } from '../../src/view/media';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import { View } from '../../src/view/view';
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createView,
|
||||
TestViewMedia,
|
||||
} from '../test-utils';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
|
||||
vi.mock('../../src/utils/media-player-controller.js');
|
||||
vi.mock('../../src/card-controller/microphone-manager.js');
|
||||
@@ -88,8 +89,8 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Frigate menu / Default view',
|
||||
tap_action: createFrigateCardSimpleAction('menu_toggle'),
|
||||
hold_action: createFrigateCardSimpleAction('diagnostics'),
|
||||
tap_action: createGeneralAction('menu_toggle'),
|
||||
hold_action: createGeneralAction('diagnostics'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,8 +105,8 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Frigate menu / Default view',
|
||||
tap_action: createFrigateCardSimpleAction('default'),
|
||||
hold_action: createFrigateCardSimpleAction('diagnostics'),
|
||||
tap_action: createGeneralAction('default'),
|
||||
hold_action: createGeneralAction('diagnostics'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1355,13 +1356,14 @@ describe('MenuButtonController', () => {
|
||||
|
||||
describe('should have show ptz button', () => {
|
||||
it('when the selected camera is not PTZ enabled', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||
createCapabilities(),
|
||||
);
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
},
|
||||
]);
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: cameraManager,
|
||||
cameraManager: createCameraManager(store),
|
||||
view: createView({ view: 'live' }),
|
||||
});
|
||||
|
||||
@@ -1373,13 +1375,15 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
|
||||
it('when not in live view', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ ptz: { panTilt: ['relative'] } }),
|
||||
);
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: cameraManager,
|
||||
cameraManager: createCameraManager(store),
|
||||
view: createView({ view: 'clips' }),
|
||||
});
|
||||
|
||||
@@ -1391,11 +1395,16 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
|
||||
it('when the selected camera is PTZ enabled', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ ptz: { panTilt: ['relative'] } }),
|
||||
);
|
||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: createCameraManager(store),
|
||||
});
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
enabled: false,
|
||||
@@ -1406,25 +1415,28 @@ describe('MenuButtonController', () => {
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'show_ptz',
|
||||
show_ptz: false,
|
||||
frigate_card_action: 'ptz_controls',
|
||||
enabled: false,
|
||||
},
|
||||
title: 'Show PTZ controls',
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
});
|
||||
});
|
||||
|
||||
it('when the context has PTZ visiblity turned off', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
|
||||
createCapabilities({ ptz: { panTilt: ['relative'] } }),
|
||||
);
|
||||
it('when the context has PTZ disabled', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
const view = createView({
|
||||
camera: 'camera-1',
|
||||
context: { live: { ptzVisible: false } },
|
||||
context: { ptzControls: { enabled: false } },
|
||||
});
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: cameraManager,
|
||||
cameraManager: createCameraManager(store),
|
||||
view: view,
|
||||
});
|
||||
|
||||
@@ -1435,8 +1447,8 @@ describe('MenuButtonController', () => {
|
||||
style: {},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'show_ptz',
|
||||
show_ptz: true,
|
||||
frigate_card_action: 'ptz_controls',
|
||||
enabled: true,
|
||||
},
|
||||
title: 'Show PTZ controls',
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -1444,24 +1456,16 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
|
||||
it('when a substream is PTZ enabled', () => {
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
|
||||
},
|
||||
{ cameraID: 'camera-2' },
|
||||
]),
|
||||
);
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockImplementation(
|
||||
(cameraID: string) => {
|
||||
if (cameraID === 'camera-2') {
|
||||
return createCapabilities({ ptz: { panTilt: ['relative'] } });
|
||||
}
|
||||
return createCapabilities();
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
|
||||
},
|
||||
);
|
||||
{
|
||||
cameraID: 'camera-2',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
const view = createView({
|
||||
camera: 'camera-1',
|
||||
context: {
|
||||
@@ -1471,7 +1475,7 @@ describe('MenuButtonController', () => {
|
||||
},
|
||||
});
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: cameraManager,
|
||||
cameraManager: createCameraManager(store),
|
||||
view: view,
|
||||
});
|
||||
|
||||
@@ -1484,8 +1488,8 @@ describe('MenuButtonController', () => {
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'show_ptz',
|
||||
show_ptz: false,
|
||||
frigate_card_action: 'ptz_controls',
|
||||
enabled: false,
|
||||
},
|
||||
title: 'Show PTZ controls',
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -1493,7 +1497,7 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have change zoom button', () => {
|
||||
describe('should have ptz home button', () => {
|
||||
it.each([
|
||||
['live' as const, true, false],
|
||||
['live' as const, undefined, false],
|
||||
@@ -1521,13 +1525,23 @@ describe('MenuButtonController', () => {
|
||||
results: [new TestViewMedia({ id: 'media-1' })],
|
||||
selectedIndex: 0,
|
||||
}),
|
||||
context: {
|
||||
zoom: {
|
||||
[targetID]: {
|
||||
isDefault: isDefault,
|
||||
...(isDefault !== undefined && {
|
||||
context: {
|
||||
zoom: {
|
||||
[targetID]: {
|
||||
observed: {
|
||||
isDefault: isDefault,
|
||||
unzoomed: false,
|
||||
zoom: 1,
|
||||
pan: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const buttons = calculateButtons(controller, {
|
||||
cameraManager: cameraManager,
|
||||
@@ -1537,19 +1551,19 @@ describe('MenuButtonController', () => {
|
||||
if (expectedResult) {
|
||||
expect(buttons).toContainEqual({
|
||||
enabled: true,
|
||||
icon: 'mdi:magnify-close',
|
||||
icon: 'mdi:home',
|
||||
priority: 50,
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'change_zoom',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
target_id: targetID,
|
||||
},
|
||||
title: 'Zoom to default',
|
||||
title: 'PTZ Home',
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
});
|
||||
} else {
|
||||
expect(buttons).not.toContainEqual(
|
||||
expect.objectContaining({ title: 'Set zoom to default' }),
|
||||
expect.objectContaining({ title: 'Digital zoom to default' }),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -362,53 +362,60 @@ describe('MenuController', () => {
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without config', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.actionHandler(createHASS(), createEvent('tap'));
|
||||
controller.actionHandler(createEvent('tap'));
|
||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute simple action in non-hidden menu', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(hass, createEvent('tap'), tapActionConfig);
|
||||
expect(vi.mocked(handleActionConfig)).toBeCalledWith(
|
||||
host,
|
||||
hass,
|
||||
tapActionConfig,
|
||||
action,
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfig);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action], config: tapActionConfig },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should execute simple action in with config in event', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(hass, createEvent('tap', tapActionConfig));
|
||||
expect(vi.mocked(handleActionConfig)).toBeCalledWith(
|
||||
host,
|
||||
hass,
|
||||
tapActionConfig,
|
||||
action,
|
||||
controller.actionHandler(createEvent('tap', tapActionConfig));
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action], config: tapActionConfig },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute simple array of actions in non-hidden menu', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(hass, createEvent('tap'), tapActionConfigMulti);
|
||||
expect(vi.mocked(handleActionConfig)).toBeCalledTimes(3);
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfigMulti);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action, action, action], config: tapActionConfigMulti },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should close menu', () => {
|
||||
it('tap', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
@@ -419,13 +426,12 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(hass, createEvent('tap'), tapActionConfig);
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfig);
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('end_tap', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
@@ -436,7 +442,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(hass, createEvent('end_tap'), {
|
||||
controller.actionHandler(createEvent('end_tap'), {
|
||||
end_tap_action: action,
|
||||
});
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
@@ -446,7 +452,6 @@ describe('MenuController', () => {
|
||||
describe('should not close menu', () => {
|
||||
it('start_tap with later action', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
@@ -457,7 +462,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(hass, createEvent('start_tap'), {
|
||||
controller.actionHandler(createEvent('start_tap'), {
|
||||
start_tap_action: action,
|
||||
end_tap_action: action,
|
||||
});
|
||||
@@ -466,7 +471,6 @@ describe('MenuController', () => {
|
||||
|
||||
it('with a menu toggle action', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
@@ -477,7 +481,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(false);
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
|
||||
controller.actionHandler(hass, createEvent('tap'), {
|
||||
controller.actionHandler(createEvent('tap'), {
|
||||
camera_entity: 'foo',
|
||||
tap_action: menuToggleAction,
|
||||
});
|
||||
@@ -497,7 +501,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(hass, createEvent('end_tap'), tapActionConfig);
|
||||
controller.actionHandler(createEvent('end_tap'), tapActionConfig);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { PTZController } from '../../src/components-lib/ptz-controller';
|
||||
import {
|
||||
FrigateCardPTZConfig,
|
||||
frigateCardPTZSchema,
|
||||
PTZControlAction,
|
||||
} from '../../src/config/types';
|
||||
import {
|
||||
frigateCardHandleActionConfig,
|
||||
getActionConfigGivenAction,
|
||||
} from '../../src/utils/action.js';
|
||||
import {
|
||||
createCapabilities,
|
||||
createCameraManager,
|
||||
createHASS,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('../../src/utils/action.js');
|
||||
|
||||
const createConfig = (config?: Partial<FrigateCardPTZConfig>): FrigateCardPTZConfig => {
|
||||
return frigateCardPTZSchema.parse({
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('PTZController', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should be creatable', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get config creatable', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
const config = createConfig();
|
||||
controller.setConfig(config);
|
||||
expect(controller.getConfig()).toBe(config);
|
||||
});
|
||||
|
||||
describe('should set element attributes', () => {
|
||||
describe('orientation', () => {
|
||||
describe('with config', () => {
|
||||
it.each([['horizontal' as const], ['vertical' as const]])(
|
||||
'%s',
|
||||
(orientation: 'horizontal' | 'vertical') => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ orientation: orientation }));
|
||||
expect(element.getAttribute('data-orientation')).toBe(orientation);
|
||||
},
|
||||
);
|
||||
});
|
||||
it('without config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig();
|
||||
expect(element.getAttribute('data-orientation')).toBe('horizontal');
|
||||
});
|
||||
});
|
||||
describe('position', () => {
|
||||
describe('with config', () => {
|
||||
it.each([
|
||||
['top-left' as const],
|
||||
['top-right' as const],
|
||||
['bottom-left' as const],
|
||||
['bottom-right' as const],
|
||||
])(
|
||||
'%s',
|
||||
(position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ position: position }));
|
||||
expect(element.getAttribute('data-position')).toBe(position);
|
||||
},
|
||||
);
|
||||
});
|
||||
it('without config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig();
|
||||
expect(element.getAttribute('data-position')).toBe('bottom-right');
|
||||
});
|
||||
});
|
||||
it('with style in config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ style: { transform: 'none', left: '50%' } }));
|
||||
expect(element.getAttribute('style')).toBe('transform:none;left:50%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should respect mode', () => {
|
||||
it('off', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
it('off but forced on', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setForceVisibility(true);
|
||||
// No actions, no rendering, no matter what.
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
it('on without actions', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'on' }));
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
it('on with actions', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
|
||||
controller.setCamera(createCameraManager(), 'camera.office');
|
||||
expect(controller.shouldDisplay()).toBeTruthy();
|
||||
});
|
||||
it('on with actions but forced off', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'on', actions_left: {} }));
|
||||
controller.setCamera(createCameraManager(), 'camera.office');
|
||||
controller.setForceVisibility(false);
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get PTZ actions', () => {
|
||||
it('without config', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller.getPTZActions('left')).toBeNull();
|
||||
});
|
||||
|
||||
describe('using defaults', () => {
|
||||
describe('with continous PTZ', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('%s', (actionName: PTZControlAction) => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig());
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||
createCapabilities({
|
||||
ptz: {
|
||||
panTilt: ['continuous'],
|
||||
zoom: ['continuous'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions(actionName)).toEqual({
|
||||
start_tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz',
|
||||
ptz_action: actionName,
|
||||
ptz_phase: 'start',
|
||||
},
|
||||
end_tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz',
|
||||
ptz_action: actionName,
|
||||
ptz_phase: 'stop',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with relative PTZ', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('%s', (actionName: PTZControlAction) => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig());
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||
createCapabilities({
|
||||
ptz: {
|
||||
panTilt: ['relative'],
|
||||
zoom: ['relative'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions(actionName)).toEqual({
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz',
|
||||
ptz_action: actionName,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('home', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig());
|
||||
|
||||
const cameraManager = createCameraManager();
|
||||
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||
createCapabilities({
|
||||
ptz: {
|
||||
presets: ['preset-foo'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions('home')).toEqual({
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz',
|
||||
ptz_action: 'preset',
|
||||
ptz_preset: 'preset-foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('using config', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('configured %s', (actionName: PTZControlAction) => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
[`actions_${actionName}`]: {
|
||||
argument: actionName,
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.setCamera(createCameraManager(), 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions(actionName)).toEqual({
|
||||
argument: actionName,
|
||||
});
|
||||
});
|
||||
|
||||
it('configured home', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
actions_home: {
|
||||
argument: 'home',
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.setCamera(createCameraManager(), 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions('home')).toEqual({
|
||||
argument: 'home',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle action', () => {
|
||||
it('without hass or action', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setHASS();
|
||||
controller.setCamera();
|
||||
controller.handleAction(
|
||||
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||
);
|
||||
expect(frigateCardHandleActionConfig).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('with action', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
const hass = createHASS();
|
||||
controller.setHASS(hass);
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
[`actions_left`]: {},
|
||||
}),
|
||||
);
|
||||
const tapAction = {
|
||||
action: 'none' as const,
|
||||
};
|
||||
const actionsConfig = {
|
||||
tap_action: tapAction,
|
||||
};
|
||||
vi.mocked(getActionConfigGivenAction).mockReturnValue(tapAction);
|
||||
|
||||
controller.handleAction(
|
||||
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||
actionsConfig,
|
||||
);
|
||||
expect(frigateCardHandleActionConfig).toBeCalledWith(
|
||||
element,
|
||||
hass,
|
||||
actionsConfig,
|
||||
'tap',
|
||||
actionsConfig.tap_action,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Capabilities } from '../../../src/camera-manager/capabilities';
|
||||
import { PTZController } from '../../../src/components-lib/ptz/ptz-controller';
|
||||
import { PTZControlAction } from '../../../src/config/ptz';
|
||||
import { PTZControlsConfig, ptzControlsConfigSchema } from '../../../src/config/types';
|
||||
import { createCameraManager, createCapabilities, createStore } from '../../test-utils';
|
||||
|
||||
const createConfig = (config?: Partial<PTZControlsConfig>): PTZControlsConfig => {
|
||||
return ptzControlsConfigSchema.parse({
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('PTZController', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should be creatable', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get config', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
const config = createConfig();
|
||||
controller.setConfig(config);
|
||||
expect(controller.getConfig()).toBe(config);
|
||||
});
|
||||
|
||||
describe('should set element attributes', () => {
|
||||
describe('orientation', () => {
|
||||
describe('with config', () => {
|
||||
it.each([['horizontal' as const], ['vertical' as const]])(
|
||||
'%s',
|
||||
(orientation: 'horizontal' | 'vertical') => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ orientation: orientation }));
|
||||
expect(element.getAttribute('data-orientation')).toBe(orientation);
|
||||
},
|
||||
);
|
||||
});
|
||||
it('without config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig();
|
||||
expect(element.getAttribute('data-orientation')).toBe('horizontal');
|
||||
});
|
||||
});
|
||||
describe('position', () => {
|
||||
describe('with config', () => {
|
||||
it.each([
|
||||
['top-left' as const],
|
||||
['top-right' as const],
|
||||
['bottom-left' as const],
|
||||
['bottom-right' as const],
|
||||
])(
|
||||
'%s',
|
||||
(position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right') => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ position: position }));
|
||||
expect(element.getAttribute('data-position')).toBe(position);
|
||||
},
|
||||
);
|
||||
});
|
||||
it('without config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig();
|
||||
expect(element.getAttribute('data-position')).toBe('bottom-right');
|
||||
});
|
||||
});
|
||||
it('with style in config', () => {
|
||||
const element = document.createElement('div');
|
||||
const controller = new PTZController(element);
|
||||
controller.setConfig(createConfig({ style: { transform: 'none', left: '50%' } }));
|
||||
expect(element.getAttribute('style')).toBe('transform:none;left:50%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should respect mode', () => {
|
||||
it('off', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
it('forced on', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setForceVisibility(true);
|
||||
|
||||
expect(controller.shouldDisplay()).toBeTruthy();
|
||||
});
|
||||
it('forced off', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setForceVisibility(false);
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('configured on', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'on' }));
|
||||
expect(controller.shouldDisplay()).toBeTruthy();
|
||||
});
|
||||
it('configured off', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'off' }));
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('auto without capability', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'auto' }));
|
||||
controller.setCamera(createCameraManager(), 'camera.office');
|
||||
expect(controller.shouldDisplay()).toBeFalsy();
|
||||
});
|
||||
it('auto with capability', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
const cameraManager = createCameraManager(store);
|
||||
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
|
||||
createCapabilities({
|
||||
ptz: {
|
||||
left: ['relative'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig({ mode: 'auto' }));
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
expect(controller.shouldDisplay()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get PTZ actions', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('%s', (actionName: PTZControlAction) => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig());
|
||||
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: new Capabilities({
|
||||
ptz: {
|
||||
left: ['relative'],
|
||||
right: ['relative'],
|
||||
up: ['relative'],
|
||||
down: ['relative'],
|
||||
zoomIn: ['relative'],
|
||||
zoomOut: ['relative'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const cameraManager = createCameraManager(store);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions()?.[actionName]).toEqual({
|
||||
start_tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
ptz_action: actionName,
|
||||
ptz_phase: 'start',
|
||||
},
|
||||
end_tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
ptz_action: actionName,
|
||||
ptz_phase: 'stop',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('home', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
controller.setConfig(createConfig());
|
||||
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: new Capabilities({
|
||||
ptz: {
|
||||
left: ['relative'],
|
||||
right: ['relative'],
|
||||
up: ['relative'],
|
||||
down: ['relative'],
|
||||
zoomIn: ['relative'],
|
||||
zoomOut: ['relative'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const cameraManager = createCameraManager(store);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.getPTZActions()['home']).toEqual({
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle action', () => {
|
||||
it('successfully', () => {
|
||||
const action = {
|
||||
action: 'more-info' as const,
|
||||
};
|
||||
const config = {
|
||||
tap_action: action,
|
||||
camera_entity: 'camera.office',
|
||||
};
|
||||
|
||||
const element = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new PTZController(element);
|
||||
controller.setCamera();
|
||||
controller.handleAction(
|
||||
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||
config,
|
||||
);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
action: action,
|
||||
config: config,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call action without actions config', () => {
|
||||
const element = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new PTZController(element);
|
||||
controller.setCamera();
|
||||
controller.handleAction(
|
||||
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||
);
|
||||
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not call action without hass', () => {
|
||||
const element = document.createElement('div');
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new PTZController(element);
|
||||
controller.setCamera();
|
||||
controller.handleAction(
|
||||
new CustomEvent<{ action: string }>('@action', { detail: { action: 'tap' } }),
|
||||
);
|
||||
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should identify useful actions', () => {
|
||||
it('without a camera', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
expect(controller.hasUsefulAction()).toEqual({
|
||||
pt: true,
|
||||
z: true,
|
||||
home: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('without camera PTZ capabilities', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
ptz: {},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const cameraManager = createCameraManager(store);
|
||||
controller.setCamera(cameraManager, 'camera.office');
|
||||
|
||||
expect(controller.hasUsefulAction()).toEqual({
|
||||
pt: true,
|
||||
z: true,
|
||||
home: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('with camera pan and tilt capabilities', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
ptz: {
|
||||
left: ['relative'],
|
||||
right: ['relative'],
|
||||
up: ['relative'],
|
||||
down: ['relative'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
controller.setCamera(createCameraManager(store), 'camera.office');
|
||||
|
||||
expect(controller.hasUsefulAction()).toEqual({
|
||||
pt: true,
|
||||
z: false,
|
||||
home: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('with camera zoom capabilities', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
ptz: {
|
||||
zoomIn: ['relative'],
|
||||
zoomOut: ['relative'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
controller.setCamera(createCameraManager(store), 'camera.office');
|
||||
|
||||
expect(controller.hasUsefulAction()).toEqual({
|
||||
pt: false,
|
||||
z: true,
|
||||
home: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('with camera presets', () => {
|
||||
const controller = new PTZController(document.createElement('div'));
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({
|
||||
ptz: {
|
||||
presets: ['door'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]);
|
||||
controller.setCamera(createCameraManager(store), 'camera.office');
|
||||
|
||||
expect(controller.hasUsefulAction()).toEqual({
|
||||
pt: false,
|
||||
z: false,
|
||||
home: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { ZoomController } from '../../../src/components-lib/zoom/zoom-controller
|
||||
import { ResizeObserverMock, requestAnimationFrameMock } from '../../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/panzoom');
|
||||
vi.mock('lodash-es/debounce', () => ({
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
@@ -311,8 +311,8 @@ describe('ZoomController', () => {
|
||||
const element = document.createElement('div');
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const defaultFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:default', defaultFunc);
|
||||
const changeFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:change', changeFunc);
|
||||
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
createAndRegisterZoom(element);
|
||||
@@ -327,8 +327,11 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(defaultFunc).toBeCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: false } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: false }),
|
||||
}),
|
||||
);
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
|
||||
@@ -341,8 +344,11 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(defaultFunc).toBeCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: true } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -350,12 +356,12 @@ describe('ZoomController', () => {
|
||||
const element = document.createElement('div');
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const defaultFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:default', defaultFunc);
|
||||
const changeFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:change', changeFunc);
|
||||
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
const controller = createAndRegisterZoom(element);
|
||||
controller.setDefaultConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
const ev_1 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
|
||||
detail: {
|
||||
@@ -367,8 +373,11 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(defaultFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: false } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: false }),
|
||||
}),
|
||||
);
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
|
||||
@@ -381,8 +390,11 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(defaultFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: true } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -390,8 +402,8 @@ describe('ZoomController', () => {
|
||||
const element = document.createElement('div');
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const defaultFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:default', defaultFunc);
|
||||
const changeFunc = vi.fn();
|
||||
element.addEventListener('frigate-card:zoom:change', changeFunc);
|
||||
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
const controller = createAndRegisterZoom(element);
|
||||
@@ -406,11 +418,14 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_1);
|
||||
expect(defaultFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: false } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: false }),
|
||||
}),
|
||||
);
|
||||
|
||||
controller.setDefaultConfig({});
|
||||
controller.setDefaultSettings({});
|
||||
|
||||
const ev_2 = new CustomEvent<PanzoomEventDetail>('panzoomchange', {
|
||||
detail: {
|
||||
@@ -422,8 +437,11 @@ describe('ZoomController', () => {
|
||||
},
|
||||
});
|
||||
element.dispatchEvent(ev_2);
|
||||
expect(defaultFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ detail: { isDefault: true } }),
|
||||
|
||||
expect(changeFunc).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({ isDefault: true }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -438,7 +456,7 @@ describe('ZoomController', () => {
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const controller = new ZoomController(element);
|
||||
controller.setDefaultConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
// Controller was not activated, config setting will not update pan/zoom.
|
||||
expect(panzoom.zoom).not.toBeCalled();
|
||||
@@ -448,16 +466,16 @@ describe('ZoomController', () => {
|
||||
expect(Panzoom).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
contain: "outside",
|
||||
contain: 'outside',
|
||||
cursor: undefined,
|
||||
maxScale: 10,
|
||||
minScale: 1,
|
||||
noBind: true,
|
||||
touchAction: "",
|
||||
touchAction: '',
|
||||
startScale: 2,
|
||||
startX: 115.62,
|
||||
startY: 63.6525,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -469,12 +487,15 @@ describe('ZoomController', () => {
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const controller = createAndRegisterZoom(element);
|
||||
controller.setDefaultConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
triggerResizeObserver();
|
||||
|
||||
expect(panzoom.zoom).toBeCalledWith(2, { animate: false });
|
||||
expect(panzoom.pan).toBeCalledWith(115.62, 63.6525, { animate: false });
|
||||
expect(panzoom.pan).toBeCalledWith(115.62, 63.6525, {
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('with set of config when a default is already set', () => {
|
||||
@@ -487,23 +508,25 @@ describe('ZoomController', () => {
|
||||
const controller = createAndRegisterZoom(element);
|
||||
|
||||
// This call will do nothing since this is what zoom/pan already are.
|
||||
controller.setDefaultConfig({ zoom: 1, pan: { x: 0, y: 0 } });
|
||||
controller.setDefaultSettings({ zoom: 1, pan: { x: 0, y: 0 } });
|
||||
|
||||
expect(panzoom.zoom).not.toBeCalled();
|
||||
expect(panzoom.pan).not.toBeCalled();
|
||||
|
||||
controller.setDefaultConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
expect(panzoom.zoom).toHaveBeenNthCalledWith(1, 2, { animate: false });
|
||||
expect(panzoom.pan).toHaveBeenNthCalledWith(1, 115.62, 63.6525, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
controller.setConfig({ zoom: 3, pan: { x: 5, y: 6 } });
|
||||
controller.setSettings({ zoom: 3, pan: { x: 5, y: 6 } });
|
||||
|
||||
expect(panzoom.zoom).toHaveBeenNthCalledWith(2, 3, { animate: false });
|
||||
expect(panzoom.pan).toHaveBeenNthCalledWith(2, 147.6, 81.18, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -516,30 +539,31 @@ describe('ZoomController', () => {
|
||||
|
||||
const controller = createAndRegisterZoom(element);
|
||||
|
||||
controller.setConfig({ zoom: 1 });
|
||||
controller.setSettings({ zoom: 1 });
|
||||
expect(panzoom.zoom).not.toHaveBeenCalled();
|
||||
|
||||
controller.setConfig({ pan: { x: 50, y: 50 } });
|
||||
controller.setSettings({ pan: { x: 50, y: 50 } });
|
||||
expect(panzoom.zoom).not.toHaveBeenCalled();
|
||||
|
||||
controller.setConfig({ zoom: 1, pan: { x: 50, y: 50 } });
|
||||
controller.setSettings({ zoom: 1, pan: { x: 50, y: 50 } });
|
||||
expect(panzoom.zoom).not.toHaveBeenCalled();
|
||||
|
||||
controller.setConfig({});
|
||||
controller.setSettings({});
|
||||
expect(panzoom.zoom).not.toHaveBeenCalled();
|
||||
|
||||
controller.setConfig({ zoom: 2 });
|
||||
controller.setSettings({ zoom: 2 });
|
||||
|
||||
expect(panzoom.zoom).toBeCalledTimes(1);
|
||||
expect(panzoom.pan).toBeCalledTimes(1);
|
||||
expect(panzoom.zoom).toHaveBeenNthCalledWith(1, 2, { animate: false });
|
||||
expect(panzoom.pan).toHaveBeenNthCalledWith(1, 0, 0, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
vi.mocked(panzoom.getScale).mockReturnValue(2);
|
||||
vi.mocked(panzoom.getPan).mockReturnValue({ x: 0, y: 0 });
|
||||
controller.setConfig({ zoom: 2 });
|
||||
controller.setSettings({ zoom: 2 });
|
||||
|
||||
expect(panzoom.zoom).toBeCalledTimes(1);
|
||||
expect(panzoom.pan).toBeCalledTimes(1);
|
||||
@@ -553,15 +577,16 @@ describe('ZoomController', () => {
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const controller = createAndRegisterZoom(element);
|
||||
controller.setDefaultConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
controller.setDefaultSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
mockClear(panzoom);
|
||||
|
||||
controller.setConfig({});
|
||||
controller.setSettings({});
|
||||
|
||||
// Should fall back to default.
|
||||
expect(panzoom.zoom).toBeCalledWith(2, { animate: false });
|
||||
expect(panzoom.pan).toBeCalledWith(115.62, 63.6525, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,11 +598,12 @@ describe('ZoomController', () => {
|
||||
setElementToDefaultCardSize(element);
|
||||
|
||||
const controller = createAndRegisterZoom(element);
|
||||
controller.setConfig({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
controller.setSettings({ zoom: 2, pan: { x: 3, y: 4 } });
|
||||
|
||||
expect(panzoom.zoom).toHaveBeenNthCalledWith(1, 2, { animate: false });
|
||||
expect(panzoom.pan).toHaveBeenNthCalledWith(1, 115.62, 63.6525, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
vi.mocked(panzoom.getScale).mockReturnValue(2);
|
||||
@@ -588,7 +614,8 @@ describe('ZoomController', () => {
|
||||
|
||||
expect(panzoom.zoom).toHaveBeenNthCalledWith(2, 2, { animate: false });
|
||||
expect(panzoom.pan).toHaveBeenNthCalledWith(2, 57.81, 31.82625, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
generateViewContextForZoomChange,
|
||||
handleZoomDefaultEvent,
|
||||
generateViewContextForZoom,
|
||||
handleZoomSettingsObservedEvent,
|
||||
} from '../../../src/components-lib/zoom/zoom-view-context';
|
||||
|
||||
describe('generateViewContextForZoomChangeRequest', () => {
|
||||
it('with config', () => {
|
||||
describe('generateViewContextForZoom', () => {
|
||||
it('with observed', () => {
|
||||
expect(
|
||||
generateViewContextForZoomChange('target', {
|
||||
zoom: {
|
||||
generateViewContextForZoom('target', {
|
||||
observed: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
zoom: {
|
||||
target: {
|
||||
observed: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
requested: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('with requested', () => {
|
||||
expect(
|
||||
generateViewContextForZoom('target', {
|
||||
requested: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
},
|
||||
@@ -16,7 +41,7 @@ describe('generateViewContextForZoomChangeRequest', () => {
|
||||
).toEqual({
|
||||
zoom: {
|
||||
target: {
|
||||
zoom: {
|
||||
requested: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
},
|
||||
@@ -24,50 +49,23 @@ describe('generateViewContextForZoomChangeRequest', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('without config', () => {
|
||||
expect(generateViewContextForZoomChange('target')).toEqual({
|
||||
zoom: {
|
||||
target: {
|
||||
zoom: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateViewContextForZoomDefault', () => {
|
||||
it('default', () => {
|
||||
expect(generateViewContextForZoomChange('target', { isDefault: true })).toEqual({
|
||||
zoom: {
|
||||
target: {
|
||||
isDefault: true,
|
||||
zoom: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('not default', () => {
|
||||
expect(generateViewContextForZoomChange('target', { isDefault: false })).toEqual({
|
||||
zoom: {
|
||||
target: {
|
||||
isDefault: false,
|
||||
zoom: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
it('handleZoomDefaultEvent', () => {
|
||||
it('handleZoomSettingsObservedEvent', () => {
|
||||
const element = document.createElement('div');
|
||||
const callback = vi.fn();
|
||||
element.addEventListener('frigate-card:view:change-context', callback);
|
||||
handleZoomDefaultEvent(
|
||||
handleZoomSettingsObservedEvent(
|
||||
element,
|
||||
new CustomEvent('frigate-card:zoom:default', { detail: { isDefault: true } }),
|
||||
new CustomEvent('frigate-card:zoom:change', {
|
||||
detail: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: true,
|
||||
unzoomed: true,
|
||||
},
|
||||
}),
|
||||
'target',
|
||||
);
|
||||
expect(callback).toBeCalledWith(
|
||||
@@ -75,8 +73,8 @@ it('handleZoomDefaultEvent', () => {
|
||||
detail: {
|
||||
zoom: {
|
||||
target: {
|
||||
zoom: null,
|
||||
isDefault: true,
|
||||
observed: { pan: { x: 1, y: 2 }, zoom: 3, isDefault: true, unzoomed: true },
|
||||
requested: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+667
-107
File diff suppressed because it is too large
Load Diff
+169
-44
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
cameraConfigSchema,
|
||||
conditionalSchema,
|
||||
customSchema,
|
||||
dimensionsConfigSchema,
|
||||
frigateCardCustomActionsBaseSchema,
|
||||
frigateCardPTZSchema,
|
||||
} from '../../src/config/types';
|
||||
import { createConfig } from '../test-utils';
|
||||
|
||||
@@ -36,6 +36,10 @@ describe('config defaults', () => {
|
||||
file_pattern: '%H-%M-%S',
|
||||
},
|
||||
},
|
||||
ptz: {
|
||||
c2r_delay_between_calls_seconds: 0.2,
|
||||
r2c_delay_between_calls_seconds: 0.5,
|
||||
},
|
||||
triggers: {
|
||||
events: ['events', 'clips', 'snapshots'],
|
||||
entities: [],
|
||||
@@ -55,7 +59,6 @@ describe('config defaults', () => {
|
||||
image: {
|
||||
mode: 'url',
|
||||
refresh_seconds: 1,
|
||||
zoomable: true,
|
||||
},
|
||||
live: {
|
||||
auto_mute: ['unselected', 'hidden', 'microphone'],
|
||||
@@ -72,7 +75,7 @@ describe('config defaults', () => {
|
||||
hide_home: false,
|
||||
hide_pan_tilt: false,
|
||||
hide_zoom: false,
|
||||
mode: 'on',
|
||||
mode: 'auto',
|
||||
orientation: 'horizontal',
|
||||
position: 'bottom-right',
|
||||
},
|
||||
@@ -135,6 +138,14 @@ describe('config defaults', () => {
|
||||
size: 48,
|
||||
style: 'thumbnails',
|
||||
},
|
||||
ptz: {
|
||||
hide_home: false,
|
||||
hide_pan_tilt: false,
|
||||
hide_zoom: false,
|
||||
mode: 'off',
|
||||
orientation: 'horizontal',
|
||||
position: 'bottom-right',
|
||||
},
|
||||
thumbnails: {
|
||||
mode: 'right',
|
||||
show_details: true,
|
||||
@@ -175,10 +186,6 @@ describe('config defaults', () => {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
default_zoom: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
display_mode: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
@@ -224,10 +231,14 @@ describe('config defaults', () => {
|
||||
enabled: false,
|
||||
priority: 50,
|
||||
},
|
||||
ptz: {
|
||||
ptz_controls: {
|
||||
enabled: false,
|
||||
priority: 50,
|
||||
},
|
||||
ptz_home: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
recordings: {
|
||||
enabled: false,
|
||||
priority: 50,
|
||||
@@ -285,6 +296,30 @@ describe('config defaults', () => {
|
||||
camera_select: 'current',
|
||||
dark_mode: 'off',
|
||||
default: 'live',
|
||||
keyboard_shortcuts: {
|
||||
enabled: true,
|
||||
ptz_down: {
|
||||
key: 'ArrowDown',
|
||||
},
|
||||
ptz_home: {
|
||||
key: 'h',
|
||||
},
|
||||
ptz_left: {
|
||||
key: 'ArrowLeft',
|
||||
},
|
||||
ptz_right: {
|
||||
key: 'ArrowRight',
|
||||
},
|
||||
ptz_up: {
|
||||
key: 'ArrowUp',
|
||||
},
|
||||
ptz_zoom_in: {
|
||||
key: '+',
|
||||
},
|
||||
ptz_zoom_out: {
|
||||
key: '-',
|
||||
},
|
||||
},
|
||||
triggers: {
|
||||
show_trigger_status: false,
|
||||
untrigger_seconds: 0,
|
||||
@@ -336,46 +371,136 @@ it('should transform action', () => {
|
||||
});
|
||||
|
||||
describe('should convert webrtc card PTZ to Frigate card PTZ', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
['home' as const],
|
||||
])('%s', (action: string) => {
|
||||
expect(
|
||||
frigateCardPTZSchema.parse({
|
||||
type: 'custom:frigate-card-ptz',
|
||||
service: 'foo',
|
||||
[`data_${action}`]: {
|
||||
tap_action: {
|
||||
action: 'none',
|
||||
describe('relative actions', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('%s', (action: string) => {
|
||||
expect(
|
||||
cameraConfigSchema.parse({
|
||||
ptz: {
|
||||
service: 'foo',
|
||||
[`data_${action}`]: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
[`actions_${action}`]: {
|
||||
tap_action: {
|
||||
action: 'call-service',
|
||||
service: 'foo',
|
||||
data: {
|
||||
tap_action: {
|
||||
action: 'none',
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
ptz: expect.objectContaining({
|
||||
[`actions_${action}`]: {
|
||||
action: 'call-service',
|
||||
service: 'foo',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('continuous actions', () => {
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
['zoom_in' as const],
|
||||
['zoom_out' as const],
|
||||
])('%s', (action: string) => {
|
||||
expect(
|
||||
cameraConfigSchema.parse({
|
||||
ptz: {
|
||||
service: 'foo',
|
||||
[`data_${action}_start`]: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
phase: 'start',
|
||||
},
|
||||
[`data_${action}_stop`]: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
phase: 'stop',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
ptz: expect.objectContaining({
|
||||
[`actions_${action}_start`]: {
|
||||
action: 'call-service',
|
||||
service: 'foo',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
phase: 'start',
|
||||
},
|
||||
},
|
||||
[`actions_${action}_stop`]: {
|
||||
action: 'call-service',
|
||||
service: 'foo',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: action,
|
||||
phase: 'stop',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('presets', () => {
|
||||
expect(
|
||||
cameraConfigSchema.parse({
|
||||
ptz: {
|
||||
service: 'service_outer',
|
||||
presets: {
|
||||
service: 'service_inner',
|
||||
data_home: {
|
||||
device: '048123',
|
||||
cmd: 'home',
|
||||
},
|
||||
data_another: {
|
||||
device: '048123',
|
||||
cmd: 'another',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
service: 'foo',
|
||||
|
||||
hide_home: false,
|
||||
hide_pan_tilt: false,
|
||||
hide_zoom: false,
|
||||
mode: 'on',
|
||||
orientation: 'horizontal',
|
||||
position: 'bottom-right',
|
||||
});
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
ptz: expect.objectContaining({
|
||||
presets: {
|
||||
home: {
|
||||
action: 'call-service',
|
||||
service: 'service_inner',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: 'home',
|
||||
},
|
||||
},
|
||||
another: {
|
||||
action: 'call-service',
|
||||
service: 'service_inner',
|
||||
data: {
|
||||
device: '048123',
|
||||
cmd: 'another',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+26
-4
@@ -14,13 +14,13 @@ import {
|
||||
CameraEventCallback,
|
||||
CameraManagerMediaCapabilities,
|
||||
} from '../src/camera-manager/types';
|
||||
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';
|
||||
import { CardElementManager } 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';
|
||||
@@ -40,11 +40,13 @@ import {
|
||||
CameraConfig,
|
||||
FrigateCardCondition,
|
||||
FrigateCardConfig,
|
||||
FrigateCardCustomAction,
|
||||
PerformanceConfig,
|
||||
RawFrigateCardConfig,
|
||||
cameraConfigSchema,
|
||||
frigateCardConditionSchema,
|
||||
frigateCardConfigSchema,
|
||||
frigateCardCustomActionSchema,
|
||||
performanceConfigSchema,
|
||||
} from '../src/config/types';
|
||||
import { CapabilitiesRaw, ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||
@@ -53,6 +55,17 @@ import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||
import { View, ViewParameters } from '../src/view/view';
|
||||
import { KeyboardStateManager } from '../src/card-controller/keyboard-state-manager';
|
||||
|
||||
export const createAction = (
|
||||
action: Record<string, unknown>,
|
||||
): FrigateCardCustomAction | null => {
|
||||
const result = frigateCardCustomActionSchema.safeParse({
|
||||
action: 'custom:frigate-card-action',
|
||||
...action,
|
||||
});
|
||||
return result.success ? result.data : null;
|
||||
};
|
||||
|
||||
export const createCameraConfig = (config?: unknown): CameraConfig => {
|
||||
return cameraConfigSchema.parse(config ?? {});
|
||||
@@ -209,9 +222,16 @@ export const createStore = (
|
||||
return store;
|
||||
};
|
||||
|
||||
export const createCameraManager = (): CameraManager => {
|
||||
export const createCameraManager = (store?: CameraManagerStore): CameraManager => {
|
||||
const cameraStore = store ?? createStore();
|
||||
const cameraManager = mock<CameraManager>();
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(createStore());
|
||||
vi.mocked(cameraManager.getStore).mockReturnValue(cameraStore);
|
||||
vi.mocked(cameraManager.getCameraCapabilities).mockImplementation(
|
||||
(cameraID: string): Capabilities | null => {
|
||||
return cameraStore.getCamera(cameraID)?.getCapabilities() ?? null;
|
||||
},
|
||||
);
|
||||
|
||||
return cameraManager;
|
||||
};
|
||||
|
||||
@@ -404,6 +424,7 @@ export const createParent = (options?: { children?: HTMLElement[] }): HTMLElemen
|
||||
|
||||
export const createLitElement = (): LitElement => {
|
||||
const element = document.createElement('div') as unknown as LitElement;
|
||||
element.addController = vi.fn();
|
||||
element.requestUpdate = vi.fn();
|
||||
return element;
|
||||
};
|
||||
@@ -426,6 +447,7 @@ export const createCardAPI = (): CardController => {
|
||||
api.getHASSManager.mockReturnValue(mock<HASSManager>());
|
||||
api.getInitializationManager.mockReturnValue(mock<InitializationManager>());
|
||||
api.getInteractionManager.mockReturnValue(mock<InteractionManager>());
|
||||
api.getKeyboardStateManager.mockReturnValue(mock<KeyboardStateManager>());
|
||||
api.getMediaLoadedInfoManager.mockReturnValue(mock<MediaLoadedInfoManager>());
|
||||
api.getMediaPlayerManager.mockReturnValue(mock<MediaPlayerManager>());
|
||||
api.getMessageManager.mockReturnValue(mock<MessageManager>());
|
||||
|
||||
+140
-101
@@ -1,33 +1,33 @@
|
||||
import { handleActionConfig, hasAction } from '@dermotduffy/custom-card-helpers';
|
||||
import { hasAction } from '@dermotduffy/custom-card-helpers';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { actionSchema } from '../../src/config/types';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
createFrigateCardCameraAction,
|
||||
createFrigateCardChangeZoomAction,
|
||||
createFrigateCardDisplayModeAction,
|
||||
createFrigateCardMediaPlayerAction,
|
||||
createFrigateCardShowPTZAction,
|
||||
createFrigateCardSimpleAction,
|
||||
frigateCardHandleAction,
|
||||
frigateCardHandleActionConfig,
|
||||
convertActionToCardCustomAction,
|
||||
createCameraAction,
|
||||
createDisplayModeAction,
|
||||
createGeneralAction,
|
||||
createLogAction,
|
||||
createMediaPlayerAction,
|
||||
createPTZDigitalAction,
|
||||
createPTZMultiAction,
|
||||
createPTZControlsAction,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
createPTZAction,
|
||||
} from '../../src/utils/action';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/custom-card-helpers');
|
||||
|
||||
describe('convertActionToFrigateCardCustomAction', () => {
|
||||
it('should skip null action', () => {
|
||||
expect(convertActionToFrigateCardCustomAction(null)).toBeFalsy();
|
||||
expect(convertActionToCardCustomAction(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should parse valid', () => {
|
||||
expect(
|
||||
convertActionToFrigateCardCustomAction({
|
||||
convertActionToCardCustomAction({
|
||||
action: 'custom:frigate-card-action',
|
||||
frigate_card_action: 'download',
|
||||
}),
|
||||
@@ -38,14 +38,14 @@ describe('convertActionToFrigateCardCustomAction', () => {
|
||||
});
|
||||
|
||||
it('should not parse invalid', () => {
|
||||
expect(convertActionToFrigateCardCustomAction('this is garbage')).toBeNull();
|
||||
expect(convertActionToCardCustomAction('this is garbage')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardSimpleAction', () => {
|
||||
describe('createGeneralAction', () => {
|
||||
it('should create general action', () => {
|
||||
expect(
|
||||
createFrigateCardSimpleAction('clips', {
|
||||
createGeneralAction('clips', {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -56,23 +56,23 @@ describe('createFrigateCardSimpleAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardCameraAction', () => {
|
||||
describe('createCameraAction', () => {
|
||||
it('should create camera_select', () => {
|
||||
expect(
|
||||
createFrigateCardCameraAction('camera_select', 'camera', { cardID: 'card_id' }),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
camera: 'camera',
|
||||
frigate_card_action: 'camera_select',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
expect(createCameraAction('camera_select', 'camera', { cardID: 'card_id' })).toEqual(
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
camera: 'camera',
|
||||
frigate_card_action: 'camera_select',
|
||||
card_id: 'card_id',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardMediaPlayerAction', () => {
|
||||
describe('createMediaPlayerAction', () => {
|
||||
it('should create media_player', () => {
|
||||
expect(
|
||||
createFrigateCardMediaPlayerAction('device', 'play', {
|
||||
createMediaPlayerAction('device', 'play', {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -85,10 +85,10 @@ describe('createFrigateCardMediaPlayerAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardDisplayModeAction', () => {
|
||||
describe('createDisplayModeAction', () => {
|
||||
it('should create display mode action', () => {
|
||||
expect(
|
||||
createFrigateCardDisplayModeAction('grid', {
|
||||
createDisplayModeAction('grid', {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -100,49 +100,143 @@ describe('createFrigateCardDisplayModeAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardShowPTZAction', () => {
|
||||
it('should create show PTZ action', () => {
|
||||
describe('createPTZControlsAction', () => {
|
||||
it('should create PTZ controls action', () => {
|
||||
expect(
|
||||
createFrigateCardShowPTZAction(true, {
|
||||
createPTZControlsAction(true, {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'show_ptz',
|
||||
show_ptz: true,
|
||||
frigate_card_action: 'ptz_controls',
|
||||
enabled: true,
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardChangeZoomAction', () => {
|
||||
it('should create change zoom default action', () => {
|
||||
describe('createPTZAction', () => {
|
||||
it('should create ptz action without parameters', () => {
|
||||
expect(
|
||||
createFrigateCardChangeZoomAction('target_id', {
|
||||
createPTZAction({
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'change_zoom',
|
||||
frigate_card_action: 'ptz',
|
||||
card_id: 'card_id',
|
||||
target_id: 'target_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create change zoom specific action', () => {
|
||||
it('should create ptz action with parameters', () => {
|
||||
expect(
|
||||
createFrigateCardChangeZoomAction('target_id', {
|
||||
createPTZAction({
|
||||
cardID: 'card_id',
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
ptzAction: 'right',
|
||||
ptzPhase: 'start',
|
||||
ptzPreset: 'preset',
|
||||
cameraID: 'camera_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'change_zoom',
|
||||
frigate_card_action: 'ptz',
|
||||
card_id: 'card_id',
|
||||
camera: 'camera_id',
|
||||
ptz_action: 'right',
|
||||
ptz_phase: 'start',
|
||||
ptz_preset: 'preset',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPTZDigitalAction', () => {
|
||||
it('should create ptz digital without parameters', () => {
|
||||
expect(
|
||||
createPTZDigitalAction({
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_digital',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create ptz digital with parameters', () => {
|
||||
expect(
|
||||
createPTZDigitalAction({
|
||||
cardID: 'card_id',
|
||||
targetID: 'target_id',
|
||||
absolute: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
},
|
||||
ptzAction: 'right',
|
||||
ptzPhase: 'start',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_digital',
|
||||
card_id: 'card_id',
|
||||
target_id: 'target_id',
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
absolute: {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
},
|
||||
ptz_action: 'right',
|
||||
ptz_phase: 'start',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPTZMultiAction', () => {
|
||||
it('should create ptz multi with parameters', () => {
|
||||
expect(
|
||||
createPTZMultiAction({
|
||||
cardID: 'card_id',
|
||||
ptzAction: 'right',
|
||||
ptzPreset: 'preset',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
card_id: 'card_id',
|
||||
ptz_action: 'right',
|
||||
ptz_preset: 'preset',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create ptz multi without parameters', () => {
|
||||
expect(
|
||||
createPTZMultiAction({
|
||||
cardID: 'card_id',
|
||||
ptzAction: 'right',
|
||||
ptzPhase: 'start',
|
||||
targetID: 'target_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
card_id: 'card_id',
|
||||
ptz_action: 'right',
|
||||
ptz_phase: 'start',
|
||||
target_id: 'target_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLogAction', () => {
|
||||
it('should create log action', () => {
|
||||
expect(
|
||||
createLogAction('Hello, world!', {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'log',
|
||||
message: 'Hello, world!',
|
||||
card_id: 'card_id',
|
||||
level: 'info',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -190,61 +284,6 @@ describe('getActionConfigGivenAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('frigateCardHandleActionConfig', () => {
|
||||
const element = document.createElement('div');
|
||||
const action = actionSchema.parse({
|
||||
action: 'none',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not handle missing arguments', () => {
|
||||
expect(
|
||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'triple_poke'),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should handle simple case', () => {
|
||||
const hass = createHASS();
|
||||
frigateCardHandleActionConfig(element, hass, {}, 'tap', action);
|
||||
expect(handleActionConfig).toBeCalled();
|
||||
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
|
||||
});
|
||||
|
||||
it('should handle array case', () => {
|
||||
const hass = createHASS();
|
||||
frigateCardHandleActionConfig(element, hass, {}, 'tap', [action, action, action]);
|
||||
expect(handleActionConfig).toBeCalledTimes(3);
|
||||
expect(handleActionConfig).toBeCalledWith(element, hass, {}, action);
|
||||
});
|
||||
|
||||
it('should handle null case', () => {
|
||||
const hass = createHASS();
|
||||
frigateCardHandleActionConfig(element, hass, {}, 'tap', null);
|
||||
expect(handleActionConfig).toBeCalledWith(element, hass, {}, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('frigateCardHandleAction', () => {
|
||||
const element = document.createElement('div');
|
||||
const action = actionSchema.parse({
|
||||
action: 'none',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call action handler', () => {
|
||||
frigateCardHandleAction(element, createHASS(), {}, action);
|
||||
expect(handleActionConfig).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('frigateCardHasAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'toggle',
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
isTruthy,
|
||||
isValidDate,
|
||||
prettifyTitle,
|
||||
recursivelyMergeObjectsConcatenatingArraysUniquely,
|
||||
recursivelyMergeObjectsNotArrays,
|
||||
runWhenIdleIfSupported,
|
||||
setify,
|
||||
@@ -345,6 +346,53 @@ describe('recursivelyMergeObjectsNotArrays', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('recursivelyMergeObjectsConcatenatingArraysUniquely', () => {
|
||||
it('should recursively merge objects but uniquely concat arrays', () => {
|
||||
expect(
|
||||
recursivelyMergeObjectsConcatenatingArraysUniquely(
|
||||
{},
|
||||
{
|
||||
a: {
|
||||
b: {
|
||||
c: 3,
|
||||
d: {
|
||||
e: 4,
|
||||
},
|
||||
array: [5, 1, 2, 3, 4],
|
||||
},
|
||||
other: {
|
||||
field: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
a: {
|
||||
b: {
|
||||
array: [4, 4, 5],
|
||||
d: {
|
||||
e: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
a: {
|
||||
b: {
|
||||
c: 3,
|
||||
array: [5, 1, 2, 3, 4],
|
||||
d: {
|
||||
e: 5,
|
||||
},
|
||||
},
|
||||
other: {
|
||||
field: 7,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aspectRatioToStyle', () => {
|
||||
it('default', () => {
|
||||
expect(aspectRatioToStyle()).toEqual({ 'aspect-ratio': 'auto' });
|
||||
|
||||
+147
-31
@@ -1,38 +1,154 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FrigateCardPTZConfig, frigateCardPTZSchema } from '../../src/config/types';
|
||||
import { hasUsablePTZ } from '../../src/utils/ptz';
|
||||
import { createCapabilities } from '../test-utils';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import {
|
||||
getPTZTarget,
|
||||
hasCameraTruePTZ,
|
||||
ptzActionToCapabilityKey,
|
||||
} from '../../src/utils/ptz';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createCameraManager,
|
||||
createStore,
|
||||
createView,
|
||||
} from '../test-utils';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import { FrigateCardView } from '../../src/config/types';
|
||||
|
||||
const createPTZConfig = (
|
||||
config?: Partial<FrigateCardPTZConfig>,
|
||||
): FrigateCardPTZConfig => {
|
||||
return frigateCardPTZSchema.parse(config ?? {});
|
||||
};
|
||||
describe('getPTZTarget', () => {
|
||||
describe('in a viewer view', () => {
|
||||
it('with media', () => {
|
||||
const media = [new TestViewMedia({ id: 'media-id' })];
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
queryResults: new MediaQueriesResults({ results: media, selectedIndex: 0 }),
|
||||
});
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toEqual({
|
||||
targetID: 'media-id',
|
||||
type: 'digital',
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasUsablePTZ', () => {
|
||||
it('should return true with manual actions', () => {
|
||||
expect(
|
||||
hasUsablePTZ(
|
||||
createCapabilities(),
|
||||
createPTZConfig({
|
||||
actions_left: {},
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
it('without media', () => {
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
});
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toBeNull();
|
||||
});
|
||||
|
||||
it('with true PTZ restriction', () => {
|
||||
const media = [new TestViewMedia({ id: 'media-id' })];
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
queryResults: new MediaQueriesResults({ results: media, selectedIndex: 0 }),
|
||||
});
|
||||
expect(
|
||||
getPTZTarget(view, { type: 'ptz', cameraManager: createCameraManager() }),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
it('should return true with capabilities', () => {
|
||||
expect(
|
||||
hasUsablePTZ(
|
||||
createCapabilities({
|
||||
ptz: {
|
||||
panTilt: ['continuous'],
|
||||
},
|
||||
|
||||
describe('in live view', () => {
|
||||
it('without restriction with true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
});
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager(store) })).toEqual({
|
||||
targetID: 'camera-1',
|
||||
type: 'ptz',
|
||||
});
|
||||
});
|
||||
|
||||
it('without restriction without true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
});
|
||||
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toEqual({
|
||||
targetID: 'camera-1',
|
||||
type: 'digital',
|
||||
});
|
||||
});
|
||||
|
||||
it('with truePTZ restriction without true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
});
|
||||
|
||||
expect(
|
||||
getPTZTarget(view, { type: 'ptz', cameraManager: createCameraManager() }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('with digitalPTZ restriction with true PTZ capability', () => {
|
||||
const view = createView({
|
||||
view: 'live',
|
||||
camera: 'camera-1',
|
||||
});
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
getPTZTarget(view, {
|
||||
type: 'digital',
|
||||
cameraManager: createCameraManager(store),
|
||||
}),
|
||||
createPTZConfig(),
|
||||
),
|
||||
).toBeTruthy();
|
||||
).toEqual({
|
||||
targetID: 'camera-1',
|
||||
type: 'digital',
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should return false with manual actions or capabilities', () => {
|
||||
expect(hasUsablePTZ(createCapabilities(), createPTZConfig())).toBeFalsy();
|
||||
|
||||
describe('in non-media views', () => {
|
||||
it.each([['timeline' as const], ['diagnostics' as const]])(
|
||||
'%s',
|
||||
(viewName: FrigateCardView) => {
|
||||
const view = createView({
|
||||
view: viewName,
|
||||
});
|
||||
expect(getPTZTarget(view, { cameraManager: createCameraManager() })).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCameraTruePTZ', () => {
|
||||
it('with true PTZ', () => {
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(hasCameraTruePTZ(createCameraManager(store), 'camera-1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('without true PTZ', () => {
|
||||
expect(hasCameraTruePTZ(createCameraManager(createStore()), 'camera-1')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('ptzActionToCapabilityKey', () => {
|
||||
expect(ptzActionToCapabilityKey('left')).toBe('left');
|
||||
expect(ptzActionToCapabilityKey('right')).toBe('right');
|
||||
expect(ptzActionToCapabilityKey('up')).toBe('up');
|
||||
expect(ptzActionToCapabilityKey('down')).toBe('down');
|
||||
expect(ptzActionToCapabilityKey('zoom_in')).toBe('zoomIn');
|
||||
expect(ptzActionToCapabilityKey('zoom_out')).toBe('zoomOut');
|
||||
expect(ptzActionToCapabilityKey('preset')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -37,4 +37,17 @@ describe('hasSubstream/getStreamCameraID', () => {
|
||||
expect(hasSubstream(view)).toBeFalsy();
|
||||
expect(getStreamCameraID(view)).toBe('camera');
|
||||
});
|
||||
it('should respect cameraID override', () => {
|
||||
const view = new View({
|
||||
view: 'live',
|
||||
camera: 'camera',
|
||||
context: {
|
||||
live: {
|
||||
overrides: new Map([['camera', 'camera2'], ['camera3', 'camera4']]),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(hasSubstream(view)).toBeTruthy();
|
||||
expect(getStreamCameraID(view, 'camera3')).toBe('camera4');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user