feat: Implement support for gesture-based PTZ control (#2378)
- Closes: #1839
This commit is contained in:
@@ -1,21 +1,106 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { PTZControlsAction } from '../../../../src/card-controller/actions/actions/ptz-controls';
|
||||
import { View } from '../../../../src/view/view';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
it('should handle ptz_controls action', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
describe('PTZControlsAction', () => {
|
||||
it('should set enabled explicitly', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
enabled: true,
|
||||
type: 'buttons',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith(
|
||||
expect.objectContaining({ ptzControls: { enabled: true } }),
|
||||
);
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
|
||||
ptzControls: { enabled: true, type: 'buttons' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle enabled when not specified', async () => {
|
||||
const api = createCardAPI();
|
||||
const view = mock<View>();
|
||||
view.context = { ptzControls: { enabled: true } };
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
|
||||
ptzControls: { enabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set enabled when currently undefined and not specified', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
|
||||
ptzControls: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set type without affecting enabled when not specified', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
type: 'gestures',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
|
||||
ptzControls: { type: 'gestures' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should set type only leaving enabled unchanged', async () => {
|
||||
const api = createCardAPI();
|
||||
const view = mock<View>();
|
||||
view.context = { ptzControls: { enabled: true } };
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||
|
||||
const action = new PTZControlsAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
type: 'buttons',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toBeCalledWith({
|
||||
ptzControls: { type: 'buttons' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,818 @@
|
||||
import { createGesture } from '@use-gesture/vanilla';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { dispatchActionExecutionRequest } from '../../../src/card-controller/actions/utils/execution-request.js';
|
||||
import { PTZDragController } from '../../../src/components-lib/ptz/drag-controller';
|
||||
import {
|
||||
PTZAction,
|
||||
PTZActionPhase,
|
||||
} from '../../../src/config/schema/actions/custom/ptz';
|
||||
import { createPTZAction } from '../../../src/utils/action';
|
||||
import { createLitElement } from '../../test-utils';
|
||||
|
||||
vi.mock('@use-gesture/vanilla', () => ({
|
||||
createGesture: vi.fn(),
|
||||
dragAction: Symbol('dragAction'),
|
||||
pinchAction: Symbol('pinchAction'),
|
||||
wheelAction: Symbol('wheelAction'),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/card-controller/actions/utils/execution-request.js', () => ({
|
||||
dispatchActionExecutionRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
const ptzAction = (ptzAction: PTZAction, ptzPhase?: PTZActionPhase) => ({
|
||||
actions: createPTZAction({ ptzAction, ptzPhase }),
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('PTZDragController', () => {
|
||||
const destroy = vi.fn();
|
||||
const gestureInit = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
gestureInit.mockReturnValue({ destroy });
|
||||
vi.mocked(createGesture).mockReturnValue(gestureInit);
|
||||
});
|
||||
|
||||
const getHandlers = () =>
|
||||
gestureInit.mock.calls[0][1] as unknown as Record<
|
||||
string,
|
||||
(...args: unknown[]) => void
|
||||
>;
|
||||
|
||||
it('should register as a controller on the host', () => {
|
||||
const host = createLitElement();
|
||||
new PTZDragController(host);
|
||||
expect(host.addController).toBeCalled();
|
||||
});
|
||||
|
||||
describe('activation', () => {
|
||||
it('should activate and request host update', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
expect(host.requestUpdate).toBeCalled();
|
||||
});
|
||||
|
||||
it('should set cursor and touch-action styles on the element', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
expect(element.style.cursor).toBe('grab');
|
||||
expect(element.style.touchAction).toBe('none');
|
||||
});
|
||||
|
||||
it('should preserve existing styles for restoration', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.style.cursor = 'pointer';
|
||||
element.style.touchAction = 'auto';
|
||||
|
||||
controller.activateIfNecessary(element);
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(element.style.cursor).toBe('pointer');
|
||||
expect(element.style.touchAction).toBe('auto');
|
||||
});
|
||||
|
||||
it('should not activate twice', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
expect(createGesture).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should create gesture with drag, pinch, and wheel actions', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
expect(createGesture).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivation', () => {
|
||||
it('should deactivate and request host update', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
vi.mocked(host.requestUpdate).mockClear();
|
||||
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(host.requestUpdate).toBeCalled();
|
||||
});
|
||||
|
||||
it('should destroy the gesture recognizer', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(destroy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not deactivate when not active', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(host.requestUpdate).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should stop active directions on deactivation', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, -110],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('down', 'stop'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop active zoom on deactivation', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
controller.deactivateIfNecessary();
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_in', 'stop'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hostDisconnected', () => {
|
||||
it('should deactivate when host disconnects', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
controller.hostDisconnected();
|
||||
|
||||
expect(destroy).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag', () => {
|
||||
describe('continuous mode', () => {
|
||||
it('should start continuous PTZ left when dragging right', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start continuous PTZ right when dragging left', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [-110, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('right', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start continuous PTZ up when dragging down', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, 110],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('up', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start continuous PTZ down when dragging up', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, -110],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('down', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start both axes for diagonal drag', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, -110],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'start'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('down', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not re-dispatch when direction is unchanged', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [120, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should stop old and start new on X direction reversal', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [-110, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('right', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop old and start new on Y direction reversal', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, 110],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, -110],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('up', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('down', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop direction when axis returns to zero', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 110],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
// Movement returns to zero on both axes.
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('up', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should stop active directions on drag end', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, -110],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [110, -110],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('down', 'stop'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch relative on drag end after continuous', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [110, 0],
|
||||
});
|
||||
|
||||
// Only the stop is dispatched, not a relative action.
|
||||
expect(dispatchActionExecutionRequest).toBeCalledTimes(1);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('relative mode', () => {
|
||||
it('should not dispatch during small drag', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [30, -20],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch relative left and down on small drag end', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [30, -20],
|
||||
});
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [30, -20],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('left'));
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('down'));
|
||||
});
|
||||
|
||||
it('should dispatch relative right and up on small drag end', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [-30, 20],
|
||||
});
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [-30, 20],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('right'));
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('up'));
|
||||
});
|
||||
|
||||
it('should not dispatch relative on zero movement', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [0, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinch cancels drag', () => {
|
||||
it('should ignore drag events while pinching', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: true,
|
||||
last: false,
|
||||
movement: [80, 80],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should stop active continuous directions when pinch starts', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: true,
|
||||
last: false,
|
||||
movement: [120, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('left', 'stop'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore drag events after pinch until gesture ends', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
// Pinch poisons the drag.
|
||||
getHandlers().onDrag({
|
||||
pinching: true,
|
||||
last: false,
|
||||
movement: [80, 0],
|
||||
});
|
||||
|
||||
// Subsequent non-pinching drag is ignored.
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [110, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should resume drag handling after poisoned gesture ends', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
// Pinch poisons the drag.
|
||||
getHandlers().onDrag({
|
||||
pinching: true,
|
||||
last: false,
|
||||
movement: [80, 0],
|
||||
});
|
||||
|
||||
// Gesture ends -- clears the flag.
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: true,
|
||||
movement: [80, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not dispatch when movement is zero', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onDrag({
|
||||
pinching: false,
|
||||
last: false,
|
||||
movement: [0, 0],
|
||||
});
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinch', () => {
|
||||
it('should start zoom_in on pinch out', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_in', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start zoom_out on pinch in', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [-1], last: false });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_out', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop zoom and start new direction on pinch direction change', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onPinch({ direction: [-1], last: false });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_in', 'stop'),
|
||||
);
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_out', 'start'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should stop zoom on pinch end', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: true });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(
|
||||
host,
|
||||
ptzAction('zoom_in', 'stop'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore zero direction', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [0], last: false });
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not re-dispatch when zoom direction is unchanged', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
vi.mocked(dispatchActionExecutionRequest).mockClear();
|
||||
|
||||
getHandlers().onPinch({ direction: [1], last: false });
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('wheel', () => {
|
||||
it('should dispatch zoom_out on scroll down', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onWheel({ delta: [0, 100] });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('zoom_out'));
|
||||
});
|
||||
|
||||
it('should dispatch zoom_in on scroll up', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onWheel({ delta: [0, -100] });
|
||||
|
||||
expect(dispatchActionExecutionRequest).toBeCalledWith(host, ptzAction('zoom_in'));
|
||||
});
|
||||
|
||||
it('should not dispatch on zero delta', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
controller.activateIfNecessary(document.createElement('div'));
|
||||
|
||||
getHandlers().onWheel({ delta: [0, 0] });
|
||||
|
||||
expect(dispatchActionExecutionRequest).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursor', () => {
|
||||
it('should set grabbing cursor on pointer down', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
getHandlers().onPointerDown();
|
||||
|
||||
expect(element.style.cursor).toBe('grabbing');
|
||||
});
|
||||
|
||||
it('should restore grab cursor on pointer up', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
getHandlers().onPointerDown();
|
||||
getHandlers().onPointerUp();
|
||||
|
||||
expect(element.style.cursor).toBe('grab');
|
||||
});
|
||||
|
||||
it('should restore grab cursor on pointer leave', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
getHandlers().onPointerDown();
|
||||
getHandlers().onPointerLeave();
|
||||
|
||||
expect(element.style.cursor).toBe('grab');
|
||||
});
|
||||
|
||||
it('should restore grab cursor on pointer cancel', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new PTZDragController(host);
|
||||
|
||||
const element = document.createElement('div');
|
||||
controller.activateIfNecessary(element);
|
||||
|
||||
getHandlers().onPointerDown();
|
||||
getHandlers().onPointerCancel();
|
||||
|
||||
expect(element.style.cursor).toBe('grab');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -142,6 +142,34 @@ describe('PTZController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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],
|
||||
@@ -364,6 +392,79 @@ describe('PTZController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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).toBeCalled();
|
||||
expect(handler).toBeCalledWith(
|
||||
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).toBeCalledWith(
|
||||
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).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
actions: {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
type: 'gestures',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle action', () => {
|
||||
it('successfully', () => {
|
||||
const action = {
|
||||
|
||||
@@ -640,6 +640,36 @@ describe('ZoomController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should report activation state', () => {
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
|
||||
const controller = new ZoomController(element);
|
||||
expect(controller.isActivated()).toBe(false);
|
||||
|
||||
controller.activate();
|
||||
expect(controller.isActivated()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not zoom or pan when zoom is disabled', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
const panzoom = createMockPanZoom();
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(panzoom);
|
||||
|
||||
const controller = createAndRegisterZoom(element);
|
||||
|
||||
// Simulate being zoomed in.
|
||||
panzoom.getScale = vi.fn().mockReturnValue(1.2);
|
||||
|
||||
controller.setZoom(false);
|
||||
|
||||
const ev = new PointerEvent('pointerdown');
|
||||
element.dispatchEvent(ev);
|
||||
|
||||
expect(panzoom.handleDown).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should set touch action on zoom/unzoom', () => {
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(Panzoom).mockReturnValueOnce(createMockPanZoom());
|
||||
|
||||
@@ -977,6 +977,25 @@ describe('should handle version specific upgrades', () => {
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
it('should not upgrade ptz settings-only config', () => {
|
||||
const config = {
|
||||
type: 'custom:advanced-camera-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
live: {
|
||||
controls: {
|
||||
ptz: {
|
||||
type: 'gestures',
|
||||
hide_type: true,
|
||||
mode: 'on',
|
||||
orientation: 'vertical',
|
||||
position: 'bottom-right',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should move view.timeout_seconds', () => {
|
||||
|
||||
@@ -92,8 +92,10 @@ describe('config defaults', () => {
|
||||
style: 'chevrons',
|
||||
},
|
||||
ptz: {
|
||||
type: 'buttons',
|
||||
hide_home: false,
|
||||
hide_pan_tilt: false,
|
||||
hide_type: false,
|
||||
hide_zoom: false,
|
||||
mode: 'auto',
|
||||
orientation: 'horizontal',
|
||||
@@ -163,8 +165,10 @@ describe('config defaults', () => {
|
||||
style: 'thumbnails',
|
||||
},
|
||||
ptz: {
|
||||
type: 'buttons',
|
||||
hide_home: false,
|
||||
hide_pan_tilt: false,
|
||||
hide_type: false,
|
||||
hide_zoom: false,
|
||||
mode: 'off',
|
||||
orientation: 'horizontal',
|
||||
@@ -1072,7 +1076,6 @@ describe('config defaults', () => {
|
||||
{
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
|
||||
@@ -110,9 +110,10 @@ describe('createDisplayModeAction', () => {
|
||||
});
|
||||
|
||||
describe('createPTZControlsAction', () => {
|
||||
it('should create PTZ controls action', () => {
|
||||
it('should create PTZ controls action with enabled', () => {
|
||||
expect(
|
||||
createPTZControlsAction(true, {
|
||||
createPTZControlsAction({
|
||||
enabled: true,
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
@@ -122,6 +123,18 @@ describe('createPTZControlsAction', () => {
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create PTZ controls action with type', () => {
|
||||
expect(
|
||||
createPTZControlsAction({
|
||||
type: 'gestures',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
type: 'gestures',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPTZAction', () => {
|
||||
|
||||
Reference in New Issue
Block a user