fix: Ensure a bare ptz_digital action resets the zoom (#2635)
- Closes: #2632
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { EffectAction } from '../../../../src/card-controller/actions/actions/effect';
|
||||
import { createEffectAction } from '../../../../src/utils/action';
|
||||
@@ -7,6 +7,7 @@ import { createCardAPI } from '../../../test-utils';
|
||||
describe('EffectAction', () => {
|
||||
it('should call startEffect when action is start', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getEffectsManager().startEffect).mockResolvedValue(undefined);
|
||||
const actionConfig = createEffectAction('snow', 'start');
|
||||
const action = new EffectAction({}, actionConfig);
|
||||
|
||||
@@ -27,6 +28,7 @@ describe('EffectAction', () => {
|
||||
|
||||
it('should call toggleEffect when action is toggle', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getEffectsManager().toggleEffect).mockResolvedValue(undefined);
|
||||
const actionConfig = createEffectAction('snow', 'toggle');
|
||||
const action = new EffectAction({}, actionConfig);
|
||||
|
||||
@@ -35,6 +37,24 @@ describe('EffectAction', () => {
|
||||
expect(api.getEffectsManager().toggleEffect).toHaveBeenCalledWith('snow');
|
||||
});
|
||||
|
||||
it('should tolerate a failure to start the effect', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getEffectsManager().startEffect).mockRejectedValue(new Error());
|
||||
const actionConfig = createEffectAction('snow', 'start');
|
||||
const action = new EffectAction({}, actionConfig);
|
||||
|
||||
await expect(action.execute(api)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should tolerate a failure to toggle the effect', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getEffectsManager().toggleEffect).mockRejectedValue(new Error());
|
||||
const actionConfig = createEffectAction('snow', 'toggle');
|
||||
const action = new EffectAction({}, actionConfig);
|
||||
|
||||
await expect(action.execute(api)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have a no-op stop method', async () => {
|
||||
const actionConfig = createEffectAction('snow', 'stop');
|
||||
const action = new EffectAction({}, actionConfig);
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import type { ViewContext } from 'view';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PTZDigitalAction } from '../../../../src/card-controller/actions/actions/ptz-digital';
|
||||
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: {
|
||||
@@ -48,19 +59,12 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {
|
||||
pan: {
|
||||
x: 3,
|
||||
y: 4,
|
||||
},
|
||||
zoom: 2,
|
||||
},
|
||||
},
|
||||
expect(getRequestedZoom(api)).toEqual({
|
||||
pan: {
|
||||
x: 3,
|
||||
y: 4,
|
||||
},
|
||||
zoom: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,14 +82,40 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {},
|
||||
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 () => {
|
||||
@@ -102,7 +132,7 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).not.toHaveBeenCalledWith();
|
||||
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without a camera', async () => {
|
||||
@@ -126,7 +156,7 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('should honor ptz_action', () => {
|
||||
@@ -339,16 +369,11 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {
|
||||
...defaultSettings,
|
||||
...expectedSettings,
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(
|
||||
getRequestedZoom(api, { zoom: { camera: { observed: current } } }),
|
||||
).toEqual({
|
||||
...defaultSettings,
|
||||
...expectedSettings,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -380,61 +405,40 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {
|
||||
...defaultSettings,
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
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: createObserved({
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createView({ context: { zoom: { camera: { observed } } } }),
|
||||
);
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {
|
||||
...defaultSettings,
|
||||
pan: {
|
||||
x: 60,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(getRequestedZoom(api, { zoom: { camera: { observed } } }, 1)).toEqual({
|
||||
...defaultSettings,
|
||||
pan: {
|
||||
x: 60,
|
||||
y: 50,
|
||||
},
|
||||
});
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledTimes(2);
|
||||
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
|
||||
|
||||
action.stop();
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledTimes(2);
|
||||
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('stop', async () => {
|
||||
@@ -450,21 +454,14 @@ describe('should handle ptz digital action', () => {
|
||||
});
|
||||
await startAction.execute(api);
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
|
||||
zoom: {
|
||||
camera: {
|
||||
observed: undefined,
|
||||
requested: {
|
||||
...defaultSettings,
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(getRequestedZoom(api)).toEqual({
|
||||
...defaultSettings,
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
});
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledTimes(1);
|
||||
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
|
||||
|
||||
const stopAction = new PTZDigitalAction(context, {
|
||||
action: 'fire-dom-event',
|
||||
@@ -475,7 +472,7 @@ describe('should handle ptz digital action', () => {
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenCalledTimes(1);
|
||||
expect(api.getViewManager().setViewWithModifiers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Capabilities } from '../../../../src/camera-manager/capabilities';
|
||||
import { PTZMultiAction } from '../../../../src/card-controller/actions/actions/ptz-multi';
|
||||
import { PTZMovementType } from '../../../../src/types';
|
||||
import { createCameraManager, createStore } from '../../../camera-manager/test-utils';
|
||||
import { applySetViewModifiers } from '../../../card-controller/view/test-utils';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { createView } from '../../../view/test-utils';
|
||||
|
||||
@@ -47,7 +48,7 @@ describe('should handle ptz multi action', () => {
|
||||
preset: undefined,
|
||||
},
|
||||
);
|
||||
expect(api.getViewManager().setViewWithMergedContext).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use digital ptz when camera does not have ptz support', async () => {
|
||||
@@ -77,19 +78,13 @@ describe('should handle ptz multi action', () => {
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getCameraManager().executePTZAction).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithMergedContext).toHaveBeenLastCalledWith({
|
||||
zoom: {
|
||||
'camera.office': {
|
||||
observed: undefined,
|
||||
requested: expect.objectContaining({
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
zoom: 1,
|
||||
}),
|
||||
},
|
||||
const modifiedView = applySetViewModifiers(api.getViewManager(), createView());
|
||||
expect(modifiedView.context?.zoom?.['camera.office'].requested).toEqual({
|
||||
pan: {
|
||||
x: 55,
|
||||
y: 50,
|
||||
},
|
||||
zoom: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -115,7 +110,7 @@ describe('should handle ptz multi action', () => {
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getCameraManager().executePTZAction).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithMergedContext).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing with a media-less view without an explicit target_id', async () => {
|
||||
@@ -144,6 +139,6 @@ describe('should handle ptz multi action', () => {
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getCameraManager().executePTZAction).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithMergedContext).not.toHaveBeenCalled();
|
||||
expect(api.getViewManager().setViewWithModifiers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect, it } from 'vitest';
|
||||
|
||||
import { ZoomRequestViewModifier } from '../../../../src/card-controller/view/modifiers/zoom-request';
|
||||
import { createView } from '../../../view/test-utils';
|
||||
|
||||
it('should write a request when the target has no zoom context', () => {
|
||||
const view = createView();
|
||||
|
||||
new ZoomRequestViewModifier('target', { zoom: 3 }).modify(view);
|
||||
|
||||
expect(view.context?.zoom?.target.requested).toEqual({ zoom: 3 });
|
||||
});
|
||||
|
||||
it('should preserve the observed settings of the target', () => {
|
||||
const observed = {
|
||||
pan: { x: 1, y: 2 },
|
||||
zoom: 3,
|
||||
isDefault: false,
|
||||
unzoomed: false,
|
||||
};
|
||||
const view = createView({ context: { zoom: { target: { observed } } } });
|
||||
|
||||
new ZoomRequestViewModifier('target', { zoom: 4 }).modify(view);
|
||||
|
||||
expect(view.context?.zoom?.target.observed).toEqual(observed);
|
||||
});
|
||||
|
||||
it('should replace an earlier request rather than merge into it', () => {
|
||||
const view = createView({
|
||||
context: { zoom: { target: { requested: { zoom: 3, pan: { x: 1, y: 2 } } } } },
|
||||
});
|
||||
|
||||
new ZoomRequestViewModifier('target', { pan: { x: 5, y: 6 } }).modify(view);
|
||||
|
||||
expect(view.context?.zoom?.target.requested).toEqual({ pan: { x: 5, y: 6 } });
|
||||
});
|
||||
|
||||
it('should allow an empty request to erase an earlier request', () => {
|
||||
const view = createView({
|
||||
context: { zoom: { target: { requested: { zoom: 3 } } } },
|
||||
});
|
||||
|
||||
new ZoomRequestViewModifier('target', {}).modify(view);
|
||||
|
||||
expect(view.context?.zoom?.target.requested).toEqual({});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { vi } from 'vitest';
|
||||
import { expect, vi } from 'vitest';
|
||||
|
||||
import type { CardController } from '../../../src/card-controller/controller';
|
||||
import { applyViewModifiers } from '../../../src/card-controller/view/modifiers';
|
||||
import type { ViewManagerInterface } from '../../../src/card-controller/view/types';
|
||||
import type { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||
import type { View } from '../../../src/view/view';
|
||||
import {
|
||||
createCameraManager,
|
||||
createCapabilities,
|
||||
@@ -42,3 +45,15 @@ export const createPopulatedAPI = (
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
||||
return api;
|
||||
};
|
||||
|
||||
// Apply the modifiers from the nth `setViewWithModifiers` call to the given
|
||||
// view, so that a caller can assert on the resulting view.
|
||||
export const applySetViewModifiers = (
|
||||
viewManager: ViewManagerInterface,
|
||||
view: View,
|
||||
n = 0,
|
||||
): View => {
|
||||
const mock = vi.mocked(viewManager.setViewWithModifiers).mock;
|
||||
expect(mock.calls.length).greaterThan(n);
|
||||
return applyViewModifiers(view, mock.calls[n][0]);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import type { CardController } from '../../../src/card-controller/controller';
|
||||
import type { ViewFactory } from '../../../src/card-controller/view/factory';
|
||||
import { MergeContextViewModifier } from '../../../src/card-controller/view/modifiers/merge-context';
|
||||
import { SetQueryViewModifier } from '../../../src/card-controller/view/modifiers/set-query';
|
||||
import type {
|
||||
QueryExecutorOptions,
|
||||
@@ -131,6 +132,26 @@ it('should set view with merged context', () => {
|
||||
expect(manager.getView()?.context).toEqual(context);
|
||||
});
|
||||
|
||||
it('should set view with modifiers', () => {
|
||||
const api = createInitializedCardAPI();
|
||||
const factory = mock<ViewFactory>();
|
||||
|
||||
const manager = new ViewManager(api, { viewFactory: factory });
|
||||
const modifier = new MergeContextViewModifier({ timeline: {} });
|
||||
|
||||
// Applying modifiers with no existing view does nothing.
|
||||
manager.setViewWithModifiers([modifier]);
|
||||
expect(manager.getView()).toBeNull();
|
||||
|
||||
factory.getViewDefault.mockReturnValue(createView({ view: 'live', camera: 'camera' }));
|
||||
manager.setViewDefault();
|
||||
manager.setViewWithModifiers([modifier]);
|
||||
|
||||
expect(manager.getView()?.camera).toBe('camera');
|
||||
expect(manager.getView()?.view).toBe('live');
|
||||
expect(manager.getView()?.context).toEqual({ timeline: {} });
|
||||
});
|
||||
|
||||
it('should return epoch', () => {
|
||||
const factory = mock<ViewFactory>();
|
||||
const manager = new ViewManager(createCardAPI(), { viewFactory: factory });
|
||||
|
||||
Reference in New Issue
Block a user