533 lines
17 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|