721 lines
22 KiB
TypeScript
721 lines
22 KiB
TypeScript
import {
|
|
afterAll,
|
|
afterEach,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from 'vitest';
|
|
|
|
import {
|
|
ActionsManager,
|
|
type Interaction,
|
|
type InteractionName,
|
|
} from '../../../src/card-controller/actions/actions-manager';
|
|
import type { CardController } from '../../../src/card-controller/controller';
|
|
import { TemplateManager } from '../../../src/card-controller/templates';
|
|
import type { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
|
import {
|
|
createInternalCallbackAction,
|
|
createLogAction,
|
|
} from '../../../src/utils/action';
|
|
import { arrayify } from '../../../src/utils/basic';
|
|
import { createConfig } from '../../config/test-utils';
|
|
import {
|
|
createCardAPI,
|
|
createHASS,
|
|
createMockTemplateRenderer,
|
|
stubConnectedHomeAssistant,
|
|
} from '../../test-utils';
|
|
import { createView } from '../../view/test-utils';
|
|
|
|
const createAPI = (): CardController => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getLockManager().getAllowedActions).mockImplementation((actions) =>
|
|
arrayify(actions),
|
|
);
|
|
return api;
|
|
};
|
|
|
|
// @vitest-environment jsdom
|
|
describe('ActionsManager', () => {
|
|
describe('getMergedActions', () => {
|
|
const config = {
|
|
view: {
|
|
actions: {
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '1',
|
|
},
|
|
},
|
|
},
|
|
live: {
|
|
actions: {
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '2',
|
|
},
|
|
},
|
|
},
|
|
media_gallery: {
|
|
actions: {
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '3',
|
|
},
|
|
},
|
|
},
|
|
media_viewer: {
|
|
actions: {
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '4',
|
|
},
|
|
},
|
|
},
|
|
image: {
|
|
actions: {
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '5',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
afterAll(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should get no merged actions with an issue', () => {
|
|
const api = createAPI();
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({ view: 'live' }),
|
|
);
|
|
vi.mocked(
|
|
api.getIssueManager().getStateManager().hasFullCardIssue,
|
|
).mockReturnValue(true);
|
|
|
|
const manager = new ActionsManager(api);
|
|
|
|
expect(manager.getMergedActions()).toEqual({});
|
|
});
|
|
|
|
describe('should get merged actions with view', () => {
|
|
it.each([
|
|
[
|
|
'live' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '2',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'clips' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '3',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'folder' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
// Folders also uses the media viewer.
|
|
navigation_path: '4',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'folders' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
// Folders also uses the media gallery.
|
|
navigation_path: '3',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'clip' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '4',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'image' as const,
|
|
{
|
|
tap_action: {
|
|
action: 'navigate',
|
|
navigation_path: '5',
|
|
},
|
|
},
|
|
],
|
|
['timeline' as const, {}],
|
|
])('%s', (viewName: AdvancedCameraCardView, result: Record<string, unknown>) => {
|
|
const api = createAPI();
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({ view: viewName }),
|
|
);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig(config),
|
|
);
|
|
|
|
const manager = new ActionsManager(api);
|
|
|
|
expect(manager.getMergedActions()).toEqual(result);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('handleInteractionEvent', () => {
|
|
// Templated actions render through ha-nunjucks, which polls (via setTimeout)
|
|
// for a connected `home-assistant` element until ready. Stubbing one makes
|
|
// it resolve synchronously, so no retry timer leaks past test teardown.
|
|
beforeAll(() => {
|
|
stubConnectedHomeAssistant();
|
|
});
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should handle interaction', async () => {
|
|
const api = createAPI();
|
|
const element = document.createElement('div');
|
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
view: {
|
|
actions: {
|
|
tap_action: createLogAction('Hello, world!'),
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const manager = new ActionsManager(api);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.handleInteractionEvent(
|
|
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
|
|
);
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
describe('should handle unexpected interactions', () => {
|
|
it.each([['malformed_type_of_tap' as const], ['double_tap' as const]])(
|
|
'%s',
|
|
(interaction: string) => {
|
|
const api = createAPI();
|
|
const element = document.createElement('div');
|
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
view: {
|
|
actions: {
|
|
tap_action: createLogAction('Hello, world!'),
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const manager = new ActionsManager(api);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
manager.handleInteractionEvent(
|
|
new CustomEvent<Interaction>('event', {
|
|
detail: { action: interaction as unknown as InteractionName },
|
|
}),
|
|
);
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('handleCustomActionEvent', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should handle advanced camera card event', async () => {
|
|
const action = createLogAction('Hello, world!');
|
|
const event = new CustomEvent('ll-custom', {
|
|
detail: action,
|
|
});
|
|
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.handleCustomActionEvent(event);
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not handle generic event', async () => {
|
|
const event = new CustomEvent('ll-custom', {
|
|
detail: {
|
|
type: 'fire-dom-event',
|
|
foo: 'bar',
|
|
},
|
|
});
|
|
|
|
const card = document.createElement('div');
|
|
const handler = vi.fn();
|
|
card.addEventListener('ll-custom', handler);
|
|
|
|
const api = createAPI();
|
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(card);
|
|
const manager = new ActionsManager(api);
|
|
|
|
await manager.handleCustomActionEvent(event);
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not handle event without detail', async () => {
|
|
const manager = new ActionsManager(createAPI());
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.handleCustomActionEvent(new Event('ll-custom'));
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('handleActionExecutionRequestEvent', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should execute actions', async () => {
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.handleActionExecutionRequestEvent(
|
|
new CustomEvent('advanced-camera-card:action:execution-request', {
|
|
detail: { actions: createLogAction('Hello, world!') },
|
|
}),
|
|
);
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('executeAction', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should execute actions', async () => {
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.executeActions({ actions: createLogAction('Hello, world!') });
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should execute actions', async () => {
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
await manager.executeActions({ actions: createLogAction('Hello, world!') });
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should render templates', async () => {
|
|
const action = createLogAction('{{ acc.camera }}');
|
|
|
|
const api = createAPI();
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(createMockTemplateRenderer());
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const conditionState = {
|
|
camera: 'camera',
|
|
};
|
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue(conditionState);
|
|
|
|
const manager = new ActionsManager(api);
|
|
const config = { entity: 'light.office' };
|
|
const triggerData = {
|
|
platform: 'acc',
|
|
type: 'view',
|
|
from_acc: { view: 'previous-view' },
|
|
to_acc: { view: 'view' },
|
|
};
|
|
vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
|
|
await manager.executeActions({ actions: action, config, triggerData });
|
|
|
|
expect(
|
|
vi.mocked(api.getTemplateManager().renderRecursivelyAsType),
|
|
).toHaveBeenCalledWith(hass, action, {
|
|
conditionState,
|
|
triggerData,
|
|
});
|
|
});
|
|
|
|
it('should filter actions through the lock manager before rendering them', async () => {
|
|
const rawRan = vi.fn();
|
|
const allowedRan = vi.fn();
|
|
const rawAction = createInternalCallbackAction(async () => {
|
|
rawRan();
|
|
});
|
|
const allowedAction = createInternalCallbackAction(async () => {
|
|
allowedRan();
|
|
});
|
|
|
|
const api = createAPI();
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(createMockTemplateRenderer());
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
vi.mocked(api.getLockManager().getAllowedActions).mockReturnValue([allowedAction]);
|
|
|
|
const manager = new ActionsManager(api);
|
|
|
|
await manager.executeActions({ actions: rawAction });
|
|
|
|
// The lock manager sees the raw (unrendered) action; only the action it
|
|
// returns is rendered and run.
|
|
expect(api.getLockManager().getAllowedActions).toHaveBeenCalledWith([rawAction]);
|
|
expect(allowedRan).toHaveBeenCalled();
|
|
expect(rawRan).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should render each action against the state at its turn', async () => {
|
|
let camera = 'first';
|
|
|
|
const api = createAPI();
|
|
// The mock renderer passes values through, so assert on the render
|
|
// *inputs*, not a swapped output.
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(createMockTemplateRenderer());
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
vi.mocked(api.getConditionStateManager().getState).mockImplementation(() => ({
|
|
camera,
|
|
}));
|
|
|
|
const manager = new ActionsManager(api);
|
|
|
|
await manager.executeActions({
|
|
actions: [
|
|
// The first action changes the camera...
|
|
createInternalCallbackAction(async () => {
|
|
camera = 'second';
|
|
}),
|
|
// ...the second is rendered afterwards.
|
|
{ action: 'none' },
|
|
],
|
|
});
|
|
|
|
// Each action renders with the state as it is at its turn: the second
|
|
// sees the camera the first action set.
|
|
expect(
|
|
vi.mocked(api.getTemplateManager().renderRecursivelyAsType),
|
|
).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.anything(),
|
|
expect.anything(),
|
|
expect.objectContaining({ conditionState: { camera: 'first' } }),
|
|
);
|
|
expect(
|
|
vi.mocked(api.getTemplateManager().renderRecursivelyAsType),
|
|
).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.anything(),
|
|
expect.anything(),
|
|
expect.objectContaining({ conditionState: { camera: 'second' } }),
|
|
);
|
|
});
|
|
|
|
it('should render against the hass available at each step', async () => {
|
|
const ran: string[] = [];
|
|
|
|
const api = createAPI();
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(createMockTemplateRenderer());
|
|
// No HASS for the first action's render; HASS thereafter.
|
|
vi.mocked(api.getHASSManager().getHASS)
|
|
.mockReturnValueOnce(null)
|
|
.mockReturnValue(createHASS());
|
|
|
|
const manager = new ActionsManager(api);
|
|
|
|
await manager.executeActions({
|
|
actions: [
|
|
createInternalCallbackAction(async () => {
|
|
ran.push('one');
|
|
}),
|
|
createInternalCallbackAction(async () => {
|
|
ran.push('two');
|
|
}),
|
|
],
|
|
});
|
|
|
|
// Both actions ran; only the second was rendered -- the first saw no
|
|
// HASS, so HASS is read per action rather than captured once.
|
|
expect(ran).toEqual(['one', 'two']);
|
|
expect(
|
|
vi.mocked(api.getTemplateManager().renderRecursivelyAsType),
|
|
).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should abort the remaining actions when one fails to render', async () => {
|
|
const ran: string[] = [];
|
|
|
|
const api = createAPI();
|
|
const renderer = createMockTemplateRenderer();
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(renderer);
|
|
|
|
// The second action's render throws, to exercise a mid-sequence failure.
|
|
vi.mocked(renderer.renderRecursivelyAsType)
|
|
.mockImplementationOnce((_hass, data) => data)
|
|
.mockImplementationOnce(() => {
|
|
throw new Error('bad template');
|
|
});
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = new ActionsManager(api);
|
|
const warnSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
|
|
await manager.executeActions({
|
|
actions: [
|
|
createInternalCallbackAction(async () => {
|
|
ran.push('first');
|
|
}),
|
|
createInternalCallbackAction(async () => {
|
|
ran.push('second');
|
|
}),
|
|
createInternalCallbackAction(async () => {
|
|
ran.push('third');
|
|
}),
|
|
],
|
|
});
|
|
|
|
// The first action ran; the second's render threw, aborting the rest. The
|
|
// error was caught by executeActions().
|
|
expect(ran).toEqual(['first']);
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should hand an if-action branch to the executor unrendered, with the trigger data', async () => {
|
|
const api = createAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({
|
|
fullscreen: true,
|
|
});
|
|
|
|
// The if-action renders its own (non-branch) fields, so give it an
|
|
// identity renderer that passes the config through unchanged.
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(createMockTemplateRenderer());
|
|
|
|
const manager = new ActionsManager(api);
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
|
|
const thenAction = createLogAction('{{ trigger.entity_id }}');
|
|
await manager.executeActions({
|
|
actions: {
|
|
if: [{ condition: 'fullscreen', fullscreen: true }],
|
|
then: [thenAction],
|
|
else: [createLogAction('unused else')],
|
|
},
|
|
triggerData: { platform: 'state', entity_id: 'binary_sensor.door' },
|
|
});
|
|
|
|
// The branch is left raw (template intact) and forwarded with the trigger
|
|
// data, so the nested executor renders it per-step when it runs -- not
|
|
// frozen against the state at the `if` step.
|
|
expect(api.getActionsManager().executeNestedActions).toHaveBeenCalledWith({
|
|
actions: [thenAction],
|
|
config: undefined,
|
|
triggerData: { platform: 'state', entity_id: 'binary_sensor.door' },
|
|
});
|
|
|
|
// The log action is handed to the (mocked) nested executor, not run here,
|
|
// so it must not actually log.
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should render if-action branch actions per-step', async () => {
|
|
let camera = 'before';
|
|
|
|
// This case renders a real branch template, so load the lazily-imported
|
|
// engine for the synchronous renderer.
|
|
const templateManager = new TemplateManager();
|
|
await templateManager.loadRenderer();
|
|
|
|
const api = createAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
vi.mocked(api.getConditionStateManager().getState).mockImplementation(() => ({
|
|
camera,
|
|
fullscreen: true,
|
|
}));
|
|
vi.mocked(api.getTemplateManager).mockReturnValue(templateManager);
|
|
|
|
const manager = new ActionsManager(api);
|
|
// The if-action's nested executor is the same (real) manager.
|
|
vi.mocked(api.getActionsManager).mockReturnValue(manager);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
|
|
await manager.executeActions({
|
|
actions: {
|
|
if: [{ condition: 'fullscreen', fullscreen: true }],
|
|
then: [
|
|
// The first branch action changes the camera...
|
|
createInternalCallbackAction(async () => {
|
|
camera = 'after';
|
|
}),
|
|
// ...the second logs `{{ acc.camera }}`, rendered at its own turn.
|
|
createLogAction('{{ acc.camera }}'),
|
|
],
|
|
},
|
|
});
|
|
|
|
// The log rendered against the camera the first branch action set, so it
|
|
// logs 'after' -- proving the branch renders per-step. Frozen-at-the-`if`
|
|
// rendering would log 'before'.
|
|
expect(consoleSpy).toHaveBeenCalledWith('after');
|
|
});
|
|
|
|
it('should drop an action whose templated discriminator cannot be classified', async () => {
|
|
const api = createAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = new ActionsManager(api);
|
|
const warnSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
|
|
await manager.executeActions({
|
|
actions: {
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '{{ acc.view }}',
|
|
},
|
|
});
|
|
|
|
// The discriminator is classified on the raw action (templates render
|
|
// only afterwards), so a templated `advanced_camera_card_action` matches
|
|
// no action type and is dropped with a warning rather than executed.
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('unknown card action'),
|
|
);
|
|
});
|
|
|
|
it('should not execute actions when the lock manager rejects them', async () => {
|
|
const api = createAPI();
|
|
vi.mocked(api.getLockManager().getAllowedActions).mockReturnValue([]);
|
|
|
|
const manager = new ActionsManager(api);
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
|
|
await manager.executeActions({ actions: createLogAction('Blocked') });
|
|
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe('should forward haptics', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('should forward success haptic', async () => {
|
|
const handler = vi.fn();
|
|
window.addEventListener('haptic', handler);
|
|
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
await manager.executeActions({ actions: { action: 'none' } });
|
|
|
|
expect(handler).toHaveBeenCalledWith(
|
|
expect.objectContaining({ detail: 'success' }),
|
|
);
|
|
});
|
|
|
|
it('should forward warning haptic', async () => {
|
|
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
|
|
const handler = vi.fn();
|
|
window.addEventListener('haptic', handler);
|
|
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
vi.stubGlobal('confirm', vi.fn().mockReturnValue(false));
|
|
|
|
await manager.executeActions({
|
|
actions: { action: 'none', confirmation: true },
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalledWith(
|
|
expect.objectContaining({ detail: 'warning' }),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('uninitialize', () => {
|
|
beforeAll(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
afterAll(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should stop actions', async () => {
|
|
const api = createAPI();
|
|
const manager = new ActionsManager(api);
|
|
|
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
|
const promise = manager.executeActions({
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: 'sleep',
|
|
duration: {
|
|
m: 1,
|
|
},
|
|
},
|
|
createLogAction('Hello, world!'),
|
|
],
|
|
});
|
|
|
|
// Stop inflight actions.
|
|
await manager.uninitialize();
|
|
|
|
// Advance timers (causes the sleep to end).
|
|
vi.runOnlyPendingTimers();
|
|
|
|
await promise;
|
|
|
|
// Action set will not continue.
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|