Files
advanced-camera-card/tests/condition-trigger/triggers/triggers/template.test.ts
T

215 lines
6.3 KiB
TypeScript

import {
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
type Mock,
} from 'vitest';
import { TemplateManager } from '../../../../src/card-controller/templates';
import { ConditionStateManager } from '../../../../src/condition-trigger/conditions/state-manager';
import { TemplateTrigger } from '../../../../src/condition-trigger/triggers/triggers/template';
import type { TriggerOfType } from '../../../../src/condition-trigger/triggers/triggers/types';
import {
createHASS,
createStateEntity,
stubConnectedHomeAssistant,
} from '../../../test-utils';
import { createTriggerEvaluatorContext } from './test-utils';
const templateManager = new TemplateManager();
beforeAll(async () => {
stubConnectedHomeAssistant();
await templateManager.loadRenderer();
});
const ENTITY_ONE = 'sensor.foo';
const ENTITY_TWO = 'sensor.bar';
const ENTITY_ONE_ON_TEMPLATE = `{{ is_state("${ENTITY_ONE}", "on") }}`;
// @vitest-environment jsdom
describe('TemplateTrigger', () => {
const create = (
config: TriggerOfType<'template'>,
): {
trigger: TemplateTrigger;
stateManager: ConditionStateManager;
callback: Mock;
} => {
const stateManager = new ConditionStateManager();
const callback = vi.fn();
const trigger = new TemplateTrigger(
config,
createTriggerEvaluatorContext({ stateManager, templateRenderer: templateManager }),
);
return { trigger, stateManager, callback };
};
const setSensor = (manager: ConditionStateManager, state: string): void => {
manager.setState({
hass: createHASS({ [ENTITY_ONE]: createStateEntity({ state }) }),
});
};
it('should trigger on the rising edge to true', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
});
trigger.subscribe(callback);
setSensor(stateManager, 'off');
expect(callback).not.toHaveBeenCalled();
setSensor(stateManager, 'on');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({ platform: 'template' });
});
it('should not trigger when the template is already true at subscribe', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
});
setSensor(stateManager, 'on');
trigger.subscribe(callback);
// A later change that keeps the template true must not trigger.
stateManager.setState({
hass: createHASS({
[ENTITY_ONE]: createStateEntity({ state: 'on' }),
[ENTITY_TWO]: createStateEntity({ state: 'x' }),
}),
});
expect(callback).not.toHaveBeenCalled();
});
it('should not re-trigger while the template remains true', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
expect(callback).toHaveBeenCalledTimes(1);
stateManager.setState({
hass: createHASS({
[ENTITY_ONE]: createStateEntity({ state: 'on' }),
[ENTITY_TWO]: createStateEntity({ state: 'y' }),
}),
});
expect(callback).toHaveBeenCalledTimes(1);
});
it('should trigger again after returning to false then true', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
setSensor(stateManager, 'off');
setSensor(stateManager, 'on');
expect(callback).toHaveBeenCalledTimes(2);
});
it('should trigger immediately when "for" is zero', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
for: 0,
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
expect(callback).toHaveBeenCalledTimes(1);
});
describe('for', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should trigger only after the template has held true for the duration', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
for: '00:00:05',
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
vi.advanceTimersByTime(4999);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should cancel the pending trigger when the template goes false before the duration', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
for: '00:00:05',
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
vi.advanceTimersByTime(3000);
setSensor(stateManager, 'off');
vi.advanceTimersByTime(5000);
expect(callback).not.toHaveBeenCalled();
});
it('should not trigger when "for" is unparseable', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
for: 'not-a-duration',
});
trigger.subscribe(callback);
setSensor(stateManager, 'on');
vi.advanceTimersByTime(100000);
expect(callback).not.toHaveBeenCalled();
});
});
it('should stop triggering after destroy', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
value_template: ENTITY_ONE_ON_TEMPLATE,
});
trigger.subscribe(callback);
trigger.destroy();
setSensor(stateManager, 'on');
expect(callback).not.toHaveBeenCalled();
});
it('should trigger on a string-truthy template value for HA symmetry', () => {
const { trigger, stateManager, callback } = create({
trigger: 'template',
// Renders the string "on"/"off", not a boolean; HA treats "on" as true.
value_template: `{{ "on" if is_state("${ENTITY_ONE}", "on") else "off" }}`,
});
trigger.subscribe(callback);
setSensor(stateManager, 'off');
expect(callback).not.toHaveBeenCalled();
setSensor(stateManager, 'on');
expect(callback).toHaveBeenCalledTimes(1);
});
});