Add initial keyboard shortcut support.
This commit is contained in:
+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