Add initial keyboard shortcut support.

This commit is contained in:
Dermot Duffy
2024-06-05 21:44:17 -07:00
parent 904f6d8142
commit 7ab545738d
186 changed files with 9564 additions and 3658 deletions
+140 -101
View File
@@ -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',
+48
View File
@@ -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
View File
@@ -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();
});
+13
View File
@@ -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');
});
});