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,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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user