Files
advanced-camera-card/tests/components-lib/ptz/ptz-controller.test.ts
T

533 lines
17 KiB
TypeScript

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 type { PTZControlAction } from '../../../src/config/schema/actions/custom/ptz';
import {
ptzControlsConfigSchema,
type PTZControlsConfig,
} from '../../../src/config/schema/common/controls/ptz';
import { PTZMovementType } from '../../../src/types';
import {
createCameraManager,
createCapabilities,
createStore,
} from '../../camera-manager/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: [PTZMovementType.Relative] } }),
},
]);
const cameraManager = createCameraManager(store);
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCapabilities({
ptz: {
left: [PTZMovementType.Relative],
},
}),
);
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig({ mode: 'auto' }));
controller.setCamera(cameraManager, 'camera.office');
expect(controller.shouldDisplay()).toBeTruthy();
});
});
describe('hasPhysicalPTZ', () => {
it('should return false without camera', () => {
const controller = new PTZController(document.createElement('div'));
expect(controller.hasPhysicalPTZ()).toBeFalsy();
});
it('should return false without PTZ capability', () => {
const controller = new PTZController(document.createElement('div'));
controller.setCamera(createCameraManager(), 'camera.office');
expect(controller.hasPhysicalPTZ()).toBeFalsy();
});
it('should return true with PTZ capability', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager).getCameraCapabilities.mockReturnValue(
createCapabilities({
ptz: {
left: [PTZMovementType.Relative],
},
}),
);
const controller = new PTZController(document.createElement('div'));
controller.setCamera(cameraManager, 'camera.office');
expect(controller.hasPhysicalPTZ()).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: [PTZMovementType.Relative],
right: [PTZMovementType.Relative],
up: [PTZMovementType.Relative],
down: [PTZMovementType.Relative],
zoomIn: [PTZMovementType.Relative],
zoomOut: [PTZMovementType.Relative],
},
}),
},
]);
const cameraManager = createCameraManager(store);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions()?.[actionName]).toEqual({
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: actionName,
ptz_phase: 'start',
},
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_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: [PTZMovementType.Relative],
right: [PTZMovementType.Relative],
up: [PTZMovementType.Relative],
down: [PTZMovementType.Relative],
zoomIn: [PTZMovementType.Relative],
zoomOut: [PTZMovementType.Relative],
presets: ['door', 'window'],
},
}),
},
]);
const cameraManager = createCameraManager(store);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions()['home']).toEqual({
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
},
});
});
it('only presets', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
const store = createStore([
{
cameraID: 'camera.office',
capabilities: new Capabilities({
ptz: {
presets: ['door', 'window'],
},
}),
},
]);
const cameraManager = createCameraManager(store);
controller.setCamera(cameraManager, 'camera.office');
expect(controller.getPTZActions()['presets']).toEqual([
{
actions: {
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'preset',
ptz_preset: 'door',
},
},
preset: 'door',
},
{
actions: {
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'preset',
ptz_preset: 'window',
},
},
preset: 'window',
},
]);
});
it('should return digital PTZ actions without camera capabilities', () => {
const controller = new PTZController(document.createElement('div'));
controller.setConfig(createConfig());
expect(controller.getPTZActions()).toEqual({
down: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'down',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'down',
ptz_phase: 'start',
},
},
home: {
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
},
},
left: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'left',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'left',
ptz_phase: 'start',
},
},
right: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'right',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'right',
ptz_phase: 'start',
},
},
up: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'up',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'up',
ptz_phase: 'start',
},
},
zoom_in: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'zoom_in',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'zoom_in',
ptz_phase: 'start',
},
},
zoom_out: {
end_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'zoom_out',
ptz_phase: 'stop',
},
start_tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_multi',
ptz_action: 'zoom_out',
ptz_phase: 'start',
},
},
});
});
});
describe('should toggle type', () => {
it('from gestures to buttons', () => {
const element = document.createElement('div');
const handler = vi.fn();
element.addEventListener('advanced-camera-card:action:execution-request', handler);
const controller = new PTZController(element);
const ev = new Event('click');
vi.spyOn(ev, 'stopPropagation');
controller.toggleTypeHandler(ev, 'gestures');
expect(ev.stopPropagation).toHaveBeenCalled();
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
type: 'buttons',
},
},
}),
);
});
it('from buttons to gestures', () => {
const element = document.createElement('div');
const handler = vi.fn();
element.addEventListener('advanced-camera-card:action:execution-request', handler);
const controller = new PTZController(element);
const ev = new Event('click');
controller.toggleTypeHandler(ev, 'buttons');
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
type: 'gestures',
},
},
}),
);
});
it('from undefined to gestures', () => {
const element = document.createElement('div');
const handler = vi.fn();
element.addEventListener('advanced-camera-card:action:execution-request', handler);
const controller = new PTZController(element);
const ev = new Event('click');
controller.toggleTypeHandler(ev);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
type: 'gestures',
},
},
}),
);
});
});
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('advanced-camera-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).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
actions: action,
config: config,
},
}),
);
});
it('should not call action without actions config', () => {
const element = document.createElement('div');
const handler = vi.fn();
element.addEventListener('advanced-camera-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.toHaveBeenCalled();
});
it('should not call action without hass', () => {
const element = document.createElement('div');
const handler = vi.fn();
element.addEventListener('advanced-camera-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.toHaveBeenCalled();
});
});
});