The `initialized` state (used in conditions/triggers) was written once and never cleared, so it meant "has this card ever been initialized" while everything reading it took it as "is this card usable now". Home Assistant takes a card off the page and puts it back whenever its dashboard tab is left and returned to, so the card initialized again while the state claimed it was initialized throughout: `trigger: initialized` fired once per card rather than once per startup, and automations were dropped in between. The card lifecycle is now an explicit state machine (`SessionManager`), the only writer of `initialized`, which separates a card that is starting up from one initializing part of itself again while it runs. A new `ever` parameter (conditions/triggers) selects the old latched behaviour. `remote_control` uses that parameter to keep its two camera priorities correct under repeated starts. With `camera_priority: entity` the card now re-reads the entity every time it starts, so a camera selected while the card was away is picked up on return. With `camera_priority: card` the card writes the entity on its first start only, unchanged, since repeating that write would overwrite a camera the user had selected. Closes: #2642 BREAKING CHANGE: `condition: initialized` is now `false` whenever the card is not usable, and `trigger: initialized` fires each time the card starts up rather than only the first time. Set `ever: true` on either to keep the previous behaviour.
782 lines
25 KiB
TypeScript
782 lines
25 KiB
TypeScript
import { assert, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
|
|
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
|
|
import type { CardController } from '../../../src/card-controller/controller';
|
|
import { ConditionStateManager } from '../../../src/condition-trigger/conditions/state-manager';
|
|
import {
|
|
GENERATED_ACTION,
|
|
type ActionGenerator,
|
|
type GeneratedActionConfig,
|
|
} from '../../../src/config/schema/actions/custom/generated-action';
|
|
import {
|
|
INTERNAL_CALLBACK_ACTION,
|
|
type InternalCallbackActionConfig,
|
|
} from '../../../src/config/schema/actions/custom/internal';
|
|
import type { ActionConfig } from '../../../src/config/schema/actions/types';
|
|
import {
|
|
createCameraAction,
|
|
isAdvancedCameraCardCustomAction,
|
|
} from '../../../src/utils/action';
|
|
import { arrayify } from '../../../src/utils/basic';
|
|
import { createStore } from '../../camera-manager/test-utils';
|
|
import { createConfig } from '../../config/test-utils';
|
|
import { createCardAPI, createHASS, createStateEntity } from '../../test-utils';
|
|
import { createView } from '../../view/test-utils';
|
|
|
|
const isGeneratedAction = (action: ActionConfig): action is GeneratedActionConfig =>
|
|
'advanced_camera_card_action' in action &&
|
|
action.advanced_camera_card_action === GENERATED_ACTION;
|
|
|
|
describe('setRemoteControlEntityFromConfig', () => {
|
|
it('without control entity', () => {
|
|
const api = createCardAPI();
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
expect(api.getAutomationsManager().deleteAutomations).toHaveBeenCalled();
|
|
expect(api.getAutomationsManager().addAutomations).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('with control entity and card priority', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
expect(api.getAutomationsManager().deleteAutomations).toHaveBeenCalled();
|
|
expect(api.getAutomationsManager().addAutomations).toHaveBeenCalledWith([
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
|
callback: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'config',
|
|
paths: ['cameras', 'remote_control.entities.camera'],
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
|
callback: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'camera',
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
|
callback: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'initialized',
|
|
|
|
// Writes the entity, so it asserts the card's camera once and does
|
|
// not repeat on a later initialization.
|
|
ever: true,
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__GENERATED_ACTION__',
|
|
generator: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'state',
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('with control entity and entity priority', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'entity',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
expect(api.getAutomationsManager().deleteAutomations).toHaveBeenCalled();
|
|
expect(api.getAutomationsManager().addAutomations).toHaveBeenCalledWith([
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
|
callback: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'config',
|
|
paths: ['cameras', 'remote_control.entities.camera'],
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
|
callback: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'camera',
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__GENERATED_ACTION__',
|
|
generator: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'initialized',
|
|
|
|
// Only updates the card, so it repeats on every initialization and
|
|
// picks up an entity change made while the card could not act.
|
|
ever: false,
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
{
|
|
actions: [
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__GENERATED_ACTION__',
|
|
generator: expect.any(Function),
|
|
},
|
|
],
|
|
triggers: [
|
|
{
|
|
trigger: 'state',
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
],
|
|
tag: setRemoteControlEntityFromConfig,
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe('should set options', () => {
|
|
it('should set options when they are incorrect', () => {
|
|
const hass = createHASS();
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const store = createStore([
|
|
{
|
|
cameraID: 'camera.one',
|
|
},
|
|
{
|
|
cameraID: 'camera.two',
|
|
},
|
|
]);
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(addOptionsAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
addOptionsAction.callback(api);
|
|
expect(hass.callService).toHaveBeenCalledWith(
|
|
'input_select',
|
|
'set_options',
|
|
{
|
|
options: ['camera.one', 'camera.two'],
|
|
},
|
|
{
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should not set options when they are already correct', () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
attributes: { options: ['camera.one', 'camera.two'] },
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const store = createStore([
|
|
{
|
|
cameraID: 'camera.one',
|
|
},
|
|
{
|
|
cameraID: 'camera.two',
|
|
},
|
|
]);
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(addOptionsAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
addOptionsAction.callback(api);
|
|
expect(hass.callService).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw when hass is undefined setting options', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const store = createStore([
|
|
{
|
|
cameraID: 'camera.one',
|
|
},
|
|
{
|
|
cameraID: 'camera.two',
|
|
},
|
|
]);
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(addOptionsAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
// Should not throw
|
|
addOptionsAction.callback(api);
|
|
});
|
|
});
|
|
|
|
describe('should select option on entity', () => {
|
|
it('should select option when camera differs from entity state', async () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
state: 'camera.one',
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([{ cameraID: 'camera.two' }]),
|
|
);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: 'camera.two',
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
// Get the camera sync callback (automation index 1)
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(cameraSyncAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
await cameraSyncAction.callback(api);
|
|
expect(hass.callService).toHaveBeenCalledWith(
|
|
'input_select',
|
|
'select_option',
|
|
{
|
|
option: 'camera.two',
|
|
},
|
|
{
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should not select option when camera matches entity state', () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
state: 'camera.one',
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([{ cameraID: 'camera.one' }]),
|
|
);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: 'camera.one',
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
// Get the camera sync callback (automation index 1)
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(cameraSyncAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
cameraSyncAction.callback(api);
|
|
|
|
// Should NOT call select_option since entity already shows camera.one
|
|
expect(hass.callService).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not select option when camera is undefined', () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
state: 'camera.one',
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(null);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
// Get the camera sync callback (automation index 1)
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(cameraSyncAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
cameraSyncAction.callback(api);
|
|
|
|
// Should NOT call select_option since camera is undefined
|
|
expect(hass.callService).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not select option when view exists but has no camera', () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
state: 'camera.one',
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: null,
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
// Test the 'camera' condition callback (automation index 1)
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
cameraSyncAction.callback(api);
|
|
expect(hass.callService).not.toHaveBeenCalled();
|
|
|
|
// Also test the 'initialized' condition callback (automation index 2)
|
|
const initializedSyncAction = vi.mocked(api.getAutomationsManager().addAutomations)
|
|
.mock.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
|
|
initializedSyncAction.callback(api);
|
|
expect(hass.callService).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not throw when hass is undefined', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: 'camera.two',
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(cameraSyncAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
// Should not throw and obviously not call service (as hass is null)
|
|
await cameraSyncAction.callback(api);
|
|
});
|
|
|
|
it('should select option when entity state is undefined', async () => {
|
|
const hass = createHASS({});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([{ cameraID: 'camera.two' }]),
|
|
);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: 'camera.two',
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(cameraSyncAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
|
INTERNAL_CALLBACK_ACTION,
|
|
);
|
|
|
|
await cameraSyncAction.callback(api);
|
|
expect(hass.callService).toHaveBeenCalledWith(
|
|
'input_select',
|
|
'select_option',
|
|
{
|
|
option: 'camera.two',
|
|
},
|
|
{
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should select option on initialization with card priority', async () => {
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({
|
|
state: 'camera.one',
|
|
}),
|
|
});
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
createStore([{ cameraID: 'camera.two' }]),
|
|
);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
createView({
|
|
camera: 'camera.two',
|
|
view: 'live',
|
|
}),
|
|
);
|
|
|
|
setRemoteControlEntityFromConfig(api);
|
|
|
|
// Get the initialization callback (automation index 2)
|
|
const initAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
|
|
expect(initAction).toBeTruthy();
|
|
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
|
|
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
|
|
|
|
await initAction.callback(api);
|
|
expect(hass.callService).toHaveBeenCalledWith(
|
|
'input_select',
|
|
'select_option',
|
|
{
|
|
option: 'camera.two',
|
|
},
|
|
{
|
|
entity_id: 'input_select.camera',
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('should wire the firing trigger into the camera action', () => {
|
|
it('should carry the new entity state to the camera-select action', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: 'card',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
// Capture the automations the loader registers, then drive them through a
|
|
// real AutomationsManager so the state trigger actually fires. The
|
|
// registered action is a generated action whose generator reads the
|
|
// firing trigger's `to_state` to pick the camera, so this verifies that
|
|
// the generator and the exact triggering value are wired through together
|
|
// (the other tests only string-match the registered action shape, so a
|
|
// context mis-wire would silently break remote control).
|
|
setRemoteControlEntityFromConfig(api);
|
|
const automations = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0];
|
|
|
|
vi.mocked(api.getHASSManager().hasHASS).mockReturnValue(true);
|
|
vi.mocked(
|
|
api.getInitializationManager().areMandatoryAspectsInitialized,
|
|
).mockReturnValue(true);
|
|
const stateManager = new ConditionStateManager();
|
|
vi.mocked(api.getConditionStateManager).mockReturnValue(stateManager);
|
|
|
|
new AutomationsManager(api).addAutomations(automations);
|
|
|
|
stateManager.setState({
|
|
hass: createHASS({
|
|
'input_select.camera': createStateEntity({ state: 'camera.one' }),
|
|
}),
|
|
});
|
|
const hass = createHASS({
|
|
'input_select.camera': createStateEntity({ state: 'camera.two' }),
|
|
});
|
|
stateManager.setState({ hass });
|
|
|
|
const captured = vi
|
|
.mocked(api.getActionsManager().executeActions)
|
|
.mock.calls.at(-1)?.[0];
|
|
assert(captured);
|
|
|
|
expect(captured.actions).toEqual([
|
|
{
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: '__GENERATED_ACTION__',
|
|
generator: expect.any(Function),
|
|
},
|
|
]);
|
|
expect(captured.triggerData?.platform).toBe('state');
|
|
expect(captured.triggerData?.entity_id).toBe('input_select.camera');
|
|
expect(captured.triggerData?.to_state?.state).toBe('camera.two');
|
|
|
|
// Running the generated action's generator with the firing trigger
|
|
// selects the camera named by the new state.
|
|
const generated = arrayify(captured.actions)[0];
|
|
assert(isGeneratedAction(generated));
|
|
expect(generated.generator({ api, triggerData: captured.triggerData })).toEqual({
|
|
action: 'fire-dom-event',
|
|
advanced_camera_card_action: 'camera_select',
|
|
camera: 'camera.two',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('should generate a camera-select action only when a camera is named', () => {
|
|
// Register the automations for a given camera priority, then pull the
|
|
// generator off the generated action on the named trigger's automation.
|
|
const getGenerator = (
|
|
api: CardController,
|
|
cameraPriority: 'card' | 'entity',
|
|
triggerName: string,
|
|
): ActionGenerator => {
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
createConfig({
|
|
remote_control: {
|
|
entities: {
|
|
camera: 'input_select.camera',
|
|
camera_priority: cameraPriority,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
setRemoteControlEntityFromConfig(api);
|
|
const automations = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
|
.calls[0][0];
|
|
const automation = automations.find((candidate) =>
|
|
candidate.triggers.some((trigger) => trigger.trigger === triggerName),
|
|
);
|
|
assert(automation);
|
|
const action = arrayify(automation.actions)[0];
|
|
assert(isGeneratedAction(action));
|
|
return action.generator;
|
|
};
|
|
|
|
it('should select the camera named by the entity state on initialize', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
|
createHASS({
|
|
'input_select.camera': createStateEntity({ state: 'camera.kitchen' }),
|
|
}),
|
|
);
|
|
|
|
expect(getGenerator(api, 'entity', 'initialized')({ api })).toEqual(
|
|
createCameraAction('camera.kitchen'),
|
|
);
|
|
});
|
|
|
|
it('should generate nothing on initialize when the entity has no state', () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS({}));
|
|
|
|
expect(getGenerator(api, 'entity', 'initialized')({ api })).toBeNull();
|
|
});
|
|
|
|
it('should generate nothing for a state trigger that carries no to_state', () => {
|
|
const api = createCardAPI();
|
|
|
|
expect(
|
|
getGenerator(
|
|
api,
|
|
'card',
|
|
'state',
|
|
)({
|
|
api,
|
|
triggerData: { platform: 'state', entity_id: 'input_select.camera' },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
});
|