fix: Confirmations should apply to all actions (#1959)
This also refactors how generic HA actions are handled, and improves typing of actions. [skip ci]
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import {
|
||||
ActionsManager,
|
||||
@@ -8,13 +17,7 @@ import {
|
||||
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
||||
import { AdvancedCameraCardView } from '../../../src/config/types';
|
||||
import { createLogAction } from '../../../src/utils/action';
|
||||
import {
|
||||
createAction,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createHASS,
|
||||
createView,
|
||||
} from '../../test-utils';
|
||||
import { createCardAPI, createConfig, createHASS, createView } from '../../test-utils';
|
||||
|
||||
describe('ActionsManager', () => {
|
||||
describe('getMergedActions', () => {
|
||||
@@ -138,7 +141,7 @@ describe('ActionsManager', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle interaction', () => {
|
||||
it('should handle interaction', async () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
@@ -158,7 +161,7 @@ describe('ActionsManager', () => {
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleInteractionEvent(
|
||||
await manager.handleInteractionEvent(
|
||||
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
|
||||
);
|
||||
expect(consoleSpy).toBeCalled();
|
||||
@@ -203,7 +206,7 @@ describe('ActionsManager', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle event', () => {
|
||||
it('should handle event', async () => {
|
||||
const action = createLogAction('Hello, world!');
|
||||
const event = new CustomEvent('ll-custom', {
|
||||
detail: action,
|
||||
@@ -213,15 +216,15 @@ describe('ActionsManager', () => {
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleCustomActionEvent(event);
|
||||
await manager.handleCustomActionEvent(event);
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not handle event without detail', () => {
|
||||
it('should not handle event without detail', async () => {
|
||||
const manager = new ActionsManager(createCardAPI());
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleCustomActionEvent(new Event('ll-custom'));
|
||||
await manager.handleCustomActionEvent(new Event('ll-custom'));
|
||||
expect(consoleSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -250,6 +253,77 @@ describe('ActionsManager', () => {
|
||||
await manager.executeActions(createLogAction('Hello, world!'));
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute actions', async () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
await manager.executeActions(createLogAction('Hello, world!'));
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should render templates', async () => {
|
||||
const action = createLogAction('{{ acc.camera }}');
|
||||
|
||||
const templateRenderer = mock<TemplateRenderer>();
|
||||
templateRenderer.renderRecursively.mockReturnValue(action);
|
||||
|
||||
const api = createCardAPI();
|
||||
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, templateRenderer);
|
||||
const config = { entity: 'light.office' };
|
||||
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||
|
||||
await manager.executeActions(action, {
|
||||
config,
|
||||
triggerData,
|
||||
});
|
||||
|
||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||
conditionState,
|
||||
triggerData,
|
||||
});
|
||||
});
|
||||
|
||||
describe('should forward haptics', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should forward success haptic', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('haptic', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
await manager.executeActions({ action: 'none' });
|
||||
|
||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'success' }));
|
||||
});
|
||||
|
||||
it('should forward warning haptic', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('haptic', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
vi.stubGlobal('confirm', vi.fn().mockReturnValue(false));
|
||||
|
||||
await manager.executeActions({ action: 'none', confirmation: true });
|
||||
|
||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'warning' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninitialize', () => {
|
||||
@@ -266,18 +340,18 @@ describe('ActionsManager', () => {
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
const promise = manager.executeActions([
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
createAction({
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'sleep',
|
||||
duration: {
|
||||
m: 1,
|
||||
},
|
||||
})!,
|
||||
},
|
||||
createLogAction('Hello, world!'),
|
||||
]);
|
||||
|
||||
// Stop inflight actions.
|
||||
manager.uninitialize();
|
||||
await manager.uninitialize();
|
||||
|
||||
// Advance timers (causes the sleep to end).
|
||||
vi.runOnlyPendingTimers();
|
||||
@@ -288,34 +362,4 @@ describe('ActionsManager', () => {
|
||||
expect(consoleSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render templates', async () => {
|
||||
const action = createLogAction('{{ acc.camera }}');
|
||||
|
||||
const templateRenderer = mock<TemplateRenderer>();
|
||||
templateRenderer.renderRecursively.mockReturnValue(action);
|
||||
|
||||
const api = createCardAPI();
|
||||
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, templateRenderer);
|
||||
const config = { camera_image: 'camera-image' };
|
||||
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||
|
||||
await manager.executeActions(action, {
|
||||
config,
|
||||
triggerData,
|
||||
});
|
||||
|
||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||
conditionState,
|
||||
triggerData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,172 @@
|
||||
import { it } from 'vitest';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BaseAction } from '../../../../src/card-controller/actions/actions/base';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { createViewAction } from '../../../../src/utils/action';
|
||||
import { createCardAPI, createHASS, createUser } from '../../../test-utils';
|
||||
|
||||
it('should construct', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
describe('should handle base action', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
await action.execute(api);
|
||||
await action.stop();
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('confirm', vi.fn());
|
||||
});
|
||||
|
||||
// These methods have no observable effect on the base class, so this test is
|
||||
// currently only providing coverage and proof of no exceptions!
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should construct', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
await action.stop();
|
||||
|
||||
// These methods have no observable effect on the base class, so this test is
|
||||
// currently only providing coverage and proof of no exceptions!
|
||||
});
|
||||
|
||||
it('should not confirm when not necessary', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should continue execution when confirmed', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalled();
|
||||
});
|
||||
|
||||
it('should abort execution when not confirmed', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(false);
|
||||
|
||||
expect(async () => await action.execute(api)).rejects.toThrowError(/Aborted action/);
|
||||
});
|
||||
|
||||
it('should not confirm when exempted', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: {
|
||||
exemptions: [
|
||||
{
|
||||
user: 'user-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('should show correct confirmation text', () => {
|
||||
it('should show action name in confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith(
|
||||
'Are you sure you want to perform this action: more-info',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show advanced camera card action name in confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
...createViewAction('clips'),
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith(
|
||||
'Are you sure you want to perform this action: clips',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show configured confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
confirmation: {
|
||||
text: 'Test text',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith('Test text');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CallServiceAction } from '../../../../src/card-controller/actions/actions/call-service';
|
||||
import { createCardAPI, createHASS } from '../../../test-utils';
|
||||
|
||||
describe('CallServiceAction', () => {
|
||||
it('should call service', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new CallServiceAction(
|
||||
{},
|
||||
{
|
||||
action: 'call-service',
|
||||
service: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(
|
||||
'light',
|
||||
'turn_on',
|
||||
{
|
||||
brightness_pct: 80,
|
||||
},
|
||||
{ entity_id: 'light.office' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call service without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new CallServiceAction(
|
||||
{},
|
||||
{
|
||||
action: 'call-service',
|
||||
service: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
// No observable effect.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CustomAction } from '../../../../src/card-controller/actions/actions/custom';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('CustomAction', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should open the URL in a new window', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('ll-custom', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new CustomAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
foo: 'bar',
|
||||
1: 2,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: 'fire-dom-event', foo: 'bar', 1: 2 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCardAPI, createHASS, createLitElement } from '../../../test-utils.js';
|
||||
import { GenericAction } from '../../../../src/card-controller/actions/actions/generic.js';
|
||||
import { handleActionConfig } from '../../../../src/ha/handle-action.js';
|
||||
|
||||
vi.mock('../../../../src/ha/handle-action.js');
|
||||
|
||||
describe('should handle generic action', () => {
|
||||
it('without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new GenericAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handleActionConfig).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
it('with hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
const element = createLitElement();
|
||||
vi.mocked(api.getCardElementManager()).getElement.mockReturnValue(element);
|
||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
||||
const action = new GenericAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handleActionConfig).toBeCalledWith(
|
||||
element,
|
||||
hass,
|
||||
{},
|
||||
{ action: 'fire-dom-event' },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MoreInfoAction } from '../../../../src/card-controller/actions/actions/more-info';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('should handle more-info action', () => {
|
||||
it('should handle more-info with entity in action', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
entity: 'light.office',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { entityId: 'light.office' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle more-info with entity in auxilliary config', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
},
|
||||
{
|
||||
entity: 'light.office',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { entityId: 'light.office' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should take no action with any entity', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { NavigateAction } from '../../../../src/card-controller/actions/actions/navigate';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('should handle navigate action', () => {
|
||||
it('should handle navigate action', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('location-changed', handler);
|
||||
|
||||
const action = new NavigateAction(
|
||||
{},
|
||||
{
|
||||
action: 'navigate',
|
||||
navigation_path: '/path',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const historyLength = history.length;
|
||||
|
||||
await action.execute(createCardAPI());
|
||||
|
||||
expect(history.length).toBe(historyLength + 1);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { replace: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle navigate action that replaces', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('location-changed', handler);
|
||||
|
||||
const action = new NavigateAction(
|
||||
{},
|
||||
{
|
||||
action: 'navigate',
|
||||
navigation_path: '/path',
|
||||
navigation_replace: true,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const historyLength = history.length;
|
||||
|
||||
await action.execute(createCardAPI());
|
||||
|
||||
expect(history.length).toBe(historyLength);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { replace: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { it } from 'vitest';
|
||||
import { NoneAction } from '../../../../src/card-controller/actions/actions/none';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
it('should handle none action', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new NoneAction(
|
||||
{},
|
||||
{
|
||||
action: 'none' as const,
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
// No observable side effects.
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCardAPI, createHASS } from '../../../test-utils';
|
||||
import { PerformActionAction } from '../../../../src/card-controller/actions/actions/perform-action';
|
||||
|
||||
describe('PerformActionAction', () => {
|
||||
it('should perform action', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new PerformActionAction(
|
||||
{},
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(
|
||||
'light',
|
||||
'turn_on',
|
||||
{
|
||||
brightness_pct: 80,
|
||||
},
|
||||
{ entity_id: 'light.office' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not perform action without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new PerformActionAction(
|
||||
{},
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
// No observable effect.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ToggleAction } from '../../../../src/card-controller/actions/actions/toggle';
|
||||
import { createCardAPI, createHASS, createStateEntity } from '../../../test-utils';
|
||||
|
||||
describe('ToggleAction', () => {
|
||||
describe('should toggle entities', () => {
|
||||
it.each([
|
||||
['light.office' as const, 'off' as const, 'light' as const, 'turn_on' as const],
|
||||
['light.office' as const, 'on' as const, 'light' as const, 'turn_off' as const],
|
||||
[
|
||||
'cover.door' as const,
|
||||
'closed' as const,
|
||||
'cover' as const,
|
||||
'open_cover' as const,
|
||||
],
|
||||
['cover.door' as const, 'open' as const, 'cover' as const, 'close_cover' as const],
|
||||
['lock.door' as const, 'locked' as const, 'lock' as const, 'unlock' as const],
|
||||
['lock.door' as const, 'unlocked' as const, 'lock' as const, 'lock' as const],
|
||||
[
|
||||
'group.foo' as const,
|
||||
'off' as const,
|
||||
'homeassistant' as const,
|
||||
'turn_on' as const,
|
||||
],
|
||||
[
|
||||
'group.foo' as const,
|
||||
'on' as const,
|
||||
'homeassistant' as const,
|
||||
'turn_off' as const,
|
||||
],
|
||||
])(
|
||||
'%s %s',
|
||||
async (
|
||||
entityID: string,
|
||||
state: string,
|
||||
expectedServiceDomain: string,
|
||||
expectedService: string,
|
||||
) => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({
|
||||
[entityID]: createStateEntity({ entity_id: entityID, state: state }),
|
||||
});
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction({}, { action: 'toggle' }, { entity: entityID });
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(expectedServiceDomain, expectedService, {
|
||||
entity_id: entityID,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing without an entity ID', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction({}, { action: 'toggle' }, {});
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without an entity state', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction(
|
||||
{},
|
||||
{ action: 'toggle' },
|
||||
{ entity: 'light.NOT_FOUND' },
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { URLAction } from '../../../../src/card-controller/actions/actions/url';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('URLAction', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should open the URL in a new window', async () => {
|
||||
const urlAction = new URLAction(
|
||||
{},
|
||||
{
|
||||
action: 'url',
|
||||
url_path: 'https://example.com',
|
||||
},
|
||||
);
|
||||
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
await urlAction.execute(createCardAPI());
|
||||
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith('https://example.com');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CallServiceAction } from '../../../src/card-controller/actions/actions/call-service';
|
||||
import { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
|
||||
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
|
||||
import { CustomAction } from '../../../src/card-controller/actions/actions/custom';
|
||||
import { DefaultAction } from '../../../src/card-controller/actions/actions/default';
|
||||
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
|
||||
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
|
||||
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
|
||||
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
|
||||
import { GenericAction } from '../../../src/card-controller/actions/actions/generic';
|
||||
import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
|
||||
import { LogAction } from '../../../src/card-controller/actions/actions/log';
|
||||
import { MediaPlayerAction } from '../../../src/card-controller/actions/actions/media-player';
|
||||
@@ -15,8 +16,12 @@ import { MicrophoneConnectAction } from '../../../src/card-controller/actions/ac
|
||||
import { MicrophoneDisconnectAction } from '../../../src/card-controller/actions/actions/microphone-disconnect';
|
||||
import { MicrophoneMuteAction } from '../../../src/card-controller/actions/actions/microphone-mute';
|
||||
import { MicrophoneUnmuteAction } from '../../../src/card-controller/actions/actions/microphone-unmute';
|
||||
import { MoreInfoAction } from '../../../src/card-controller/actions/actions/more-info';
|
||||
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
|
||||
import { NavigateAction } from '../../../src/card-controller/actions/actions/navigate';
|
||||
import { NoneAction } from '../../../src/card-controller/actions/actions/none';
|
||||
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
|
||||
import { PerformActionAction } from '../../../src/card-controller/actions/actions/perform-action';
|
||||
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
||||
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
||||
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
||||
@@ -28,13 +33,12 @@ import { StatusBarAction } from '../../../src/card-controller/actions/actions/st
|
||||
import { SubstreamOffAction } from '../../../src/card-controller/actions/actions/substream-off';
|
||||
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
|
||||
import { SubstreamSelectAction } from '../../../src/card-controller/actions/actions/substream-select';
|
||||
import { ToggleAction } from '../../../src/card-controller/actions/actions/toggle';
|
||||
import { UnmuteAction } from '../../../src/card-controller/actions/actions/unmute';
|
||||
import { URLAction } from '../../../src/card-controller/actions/actions/url';
|
||||
import { ViewAction } from '../../../src/card-controller/actions/actions/view';
|
||||
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
||||
import {
|
||||
AdvancedCameraCardCustomAction,
|
||||
INTERNAL_CALLBACK_ACTION,
|
||||
} from '../../../src/config/types';
|
||||
import { ActionConfig, INTERNAL_CALLBACK_ACTION } from '../../../src/config/types';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('ActionFactory', () => {
|
||||
@@ -55,23 +59,26 @@ describe('ActionFactory', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
describe('generic', () => {
|
||||
it('non advanced camera card action', () => {
|
||||
describe('stock actions', () => {
|
||||
it.each([
|
||||
[{ action: 'more-info' as const }, MoreInfoAction],
|
||||
[{ action: 'toggle' as const }, ToggleAction],
|
||||
[{ action: 'navigate' as const, navigation_path: '/foo' }, NavigateAction],
|
||||
[{ action: 'url' as const, url_path: 'https://card.camera' }, URLAction],
|
||||
[
|
||||
{ action: 'perform-action' as const, perform_action: 'action' },
|
||||
PerformActionAction,
|
||||
],
|
||||
[{ action: 'call-service' as const, service: 'service' }, CallServiceAction],
|
||||
[{ action: 'none' as const }, NoneAction],
|
||||
[{ action: 'fire-dom-event' as const }, CustomAction],
|
||||
])('action: $action', (action: ActionConfig, classObject: object) => {
|
||||
const factory = new ActionFactory();
|
||||
expect(factory.createAction({}, { action: 'fire-dom-event' })).toBeInstanceOf(
|
||||
GenericAction,
|
||||
);
|
||||
});
|
||||
|
||||
it('non fire-dom-event', () => {
|
||||
const factory = new ActionFactory();
|
||||
expect(factory.createAction({}, { action: 'more-info' })).toBeInstanceOf(
|
||||
GenericAction,
|
||||
);
|
||||
expect(factory.createAction({}, action)).toBeInstanceOf(classObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('custom actions', () => {
|
||||
it.each([
|
||||
[{ advanced_camera_card_action: 'camera_select' as const }, CameraSelectAction],
|
||||
[{ advanced_camera_card_action: 'camera_ui' as const }, CameraUIAction],
|
||||
@@ -178,10 +185,10 @@ describe('ActionFactory', () => {
|
||||
],
|
||||
])(
|
||||
'advanced_camera_card_action: $advanced_camera_card_action',
|
||||
(action: Partial<AdvancedCameraCardCustomAction>, classObject: object) => {
|
||||
(action: Partial<ActionConfig>, classObject: object) => {
|
||||
const factory = new ActionFactory();
|
||||
expect(
|
||||
factory.createAction({}, { action: 'fire-dom-event', ...action }),
|
||||
factory.createAction({}, { ...action, action: 'fire-dom-event' }),
|
||||
).toBeInstanceOf(classObject);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user