Files
advanced-camera-card/tests/card-controller/actions/actions/ptz-digital.test.ts
T

672 lines
17 KiB
TypeScript

import type { ViewContext } from 'view';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { PTZDigitalAction } from '../../../../src/card-controller/actions/actions/ptz-digital';
import type { Action } from '../../../../src/card-controller/actions/types';
import type { CardController } from '../../../../src/card-controller/controller';
import type {
PartialZoomSettings,
ZoomSettingsObserved,
} from '../../../../src/components-lib/zoom/types';
import type { PTZAction } from '../../../../src/config/schema/actions/custom/ptz';
import { applySetViewModifiers } from '../../../card-controller/view/test-utils';
import { createCardAPI } from '../../../test-utils';
import { createView } from '../../../view/test-utils';
const getRequestedZoom = (
api: CardController,
context?: ViewContext,
n = 0,
): PartialZoomSettings | null | undefined =>
applySetViewModifiers(api.getViewManager(), createView({ context }), n).context?.zoom
?.camera?.requested;
describe('should handle ptz digital action', () => {
const defaultSettings = {
pan: {
x: 50,
y: 50,
},
zoom: 1,
};
const createObserved = (
observed?: Partial<ZoomSettingsObserved>,
): ZoomSettingsObserved => ({
...defaultSettings,
isDefault: true,
unzoomed: true,
...observed,
});
it('should honor absolute parameters', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
absolute: {
zoom: 2,
pan: {
x: 3,
y: 4,
},
},
},
);
await action.execute(api);
expect(getRequestedZoom(api)).toEqual({
pan: {
x: 3,
y: 4,
},
zoom: 2,
});
});
it('should return to default without absolute parameters or action', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
},
);
await action.execute(api);
expect(getRequestedZoom(api)).toEqual({});
});
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2632
it('should return to default when chained after an absolute request', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
await new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
absolute: {
zoom: 3,
},
},
).execute(api);
await new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
},
).execute(api);
// Both actions land on a single view, as they do when an automation runs
// them back to back.
const view = createView();
applySetViewModifiers(api.getViewManager(), view, 0);
applySetViewModifiers(api.getViewManager(), view, 1);
expect(view.context?.zoom?.camera?.requested).toEqual({});
});
it('should do nothing without a view', async () => {
const api = createCardAPI();
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
});
it('should do nothing without a camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
// There is no media associated with a timeline, so there's no camera to
// change the PTZ settings for.
view: 'timeline',
}),
);
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
},
);
await action.execute(api);
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
});
describe('should honor ptz_action', () => {
it.each([
[
'zoom_in',
'zoom_in' as const,
{
zoom: 1.1,
},
createObserved(),
],
[
'zoom_in at maximum zoom',
'zoom_in' as const,
{
zoom: 10,
},
createObserved({
zoom: 10,
}),
],
[
'zoom_out',
'zoom_out' as const,
{
zoom: 1.9,
},
createObserved({
zoom: 2,
}),
],
[
'zoom_out at minimum zoom',
'zoom_out' as const,
{
zoom: 1,
},
createObserved({
zoom: 1,
}),
],
[
'left',
'left' as const,
{
pan: {
x: 45,
y: 50,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'left at left edge',
'left' as const,
{
pan: {
x: 0,
y: 50,
},
},
createObserved({
pan: {
x: 0,
y: 50,
},
}),
],
[
'right',
'right' as const,
{
pan: {
x: 55,
y: 50,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'right at right edge',
'right' as const,
{
pan: {
x: 100,
y: 50,
},
},
createObserved({
pan: {
x: 100,
y: 50,
},
}),
],
[
'up',
'up' as const,
{
pan: {
x: 50,
y: 45,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'up at top edge',
'up' as const,
{
pan: {
x: 50,
y: 0,
},
},
createObserved({
pan: {
x: 50,
y: 0,
},
}),
],
[
'down',
'down' as const,
{
pan: {
x: 50,
y: 55,
},
},
createObserved({
pan: {
x: 50,
y: 50,
},
}),
],
[
'down at bottom edge',
'down' as const,
{
pan: {
x: 50,
y: 100,
},
},
createObserved({
pan: {
x: 50,
y: 100,
},
}),
],
[
'action with undefined observed',
'down' as const,
{
pan: {
x: 50,
y: 55,
},
},
],
])(
'%s',
async (
_testTitle: string,
ptzAction: PTZAction,
expectedSettings: PartialZoomSettings,
current?: ZoomSettingsObserved,
) => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
context: {
zoom: {
camera: {
observed: current,
},
},
},
}),
);
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: ptzAction,
},
);
await action.execute(api);
expect(
getRequestedZoom(api, { zoom: { camera: { observed: current } } }),
).toEqual({
...defaultSettings,
...expectedSettings,
});
},
);
});
// @vitest-environment jsdom
describe('should honor ptz_phase', () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it('start', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'right',
ptz_phase: 'start',
},
);
await action.execute(api);
expect(getRequestedZoom(api)).toEqual({
...defaultSettings,
pan: {
x: 55,
y: 50,
},
});
// Update the context to reflect the first step.
const observed = createObserved({
pan: {
x: 55,
y: 50,
},
});
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({ context: { zoom: { camera: { observed } } } }),
);
vi.runOnlyPendingTimers();
expect(getRequestedZoom(api, { zoom: { camera: { observed } } }, 1)).toEqual({
...defaultSettings,
pan: {
x: 60,
y: 50,
},
});
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
action.stop();
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
});
it('stop', async () => {
const api = createCardAPI();
const context = {};
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const startAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'right',
ptz_phase: 'start',
});
await startAction.execute(api);
expect(getRequestedZoom(api)).toEqual({
...defaultSettings,
pan: {
x: 55,
y: 50,
},
});
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
const stopAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_phase: 'stop',
});
await stopAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
});
it('should stop without anything in progress', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const stopAction = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_phase: 'stop',
},
);
await stopAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
});
it('should only move for the latest of two concurrent starts', async () => {
const api = createCardAPI();
const context = {};
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const createStartAction = (ptzAction: PTZAction): PTZDigitalAction =>
new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: ptzAction,
ptz_phase: 'start',
});
await Promise.all([
createStartAction('left').execute(api),
createStartAction('up').execute(api),
]);
// The 'up' start replaces the 'left' start before the 'left' start has
// taken its first step, so only the 'up' start moves.
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
expect(getRequestedZoom(api)).toEqual({
...defaultSettings,
pan: {
x: 50,
y: 45,
},
});
const stopAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_phase: 'stop',
});
await stopAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
});
it('should continue movement when a stop for a different movement arrives', async () => {
const api = createCardAPI();
const context = {};
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const leftAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
ptz_phase: 'start',
});
await leftAction.execute(api);
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
expect(getRequestedZoom(api)).toEqual({
...defaultSettings,
pan: {
x: 45,
y: 50,
},
});
// A stop for a movement that is not in progress leaves the 'left'
// movement running.
const stopUpAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'up',
ptz_phase: 'stop',
});
await stopUpAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
const stopLeftAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
ptz_phase: 'stop',
});
await stopLeftAction.execute(api);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
});
it('should not stop an in-progress action that is not a digital PTZ action', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const incumbent = mock<Action>();
const context = { ptzDigital: { camera: { inProgressAction: incumbent } } };
const stopAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
ptz_phase: 'stop',
});
await stopAction.execute(api);
expect(incumbent.stop).not.toHaveBeenCalled();
});
it('should not repeat steps when stopped during the first step', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const action = new PTZDigitalAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
ptz_phase: 'start',
},
);
// The stop arrives while the first step is being taken.
vi.mocked(api.getViewManager().setViewWithModifiers).mockImplementationOnce(() => {
action.stop();
});
await action.execute(api);
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
});
it('should continue movement when a start is concurrent with a stop', async () => {
const api = createCardAPI();
const context = {};
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
const leftAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'left',
ptz_phase: 'start',
});
await leftAction.execute(api);
const stopAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_phase: 'stop',
});
const upAction = new PTZDigitalAction(context, {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_digital',
ptz_action: 'up',
ptz_phase: 'start',
});
// The left key is released as the up key is pressed.
await Promise.all([stopAction.execute(api), upAction.execute(api)]);
// One step for the left start, one for the up start.
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
vi.runOnlyPendingTimers();
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(3);
});
});
});