Files

235 lines
7.5 KiB
TypeScript

import { beforeAll, 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 { TriggersManager } from '../../../src/condition-trigger/triggers/manager';
import type { Trigger } from '../../../src/config/schema/condition-trigger/triggers/types';
import {
createHASS,
createHASSManager,
createMockTemplateRenderer,
createStateEntity,
stubConnectedHomeAssistant,
} from '../../test-utils';
const templateManager = createMockTemplateRenderer();
// @vitest-environment jsdom
describe('TriggersManager', () => {
const create = (
triggers: Trigger[],
): {
manager: TriggersManager;
stateManager: ConditionStateManager;
listener: Mock;
} => {
const stateManager = new ConditionStateManager();
const manager = new TriggersManager(
triggers,
stateManager,
createHASSManager(),
templateManager,
);
manager.subscribe();
const listener = vi.fn();
return { manager, stateManager, listener };
};
it('should notify a listener when any of its trigger evaluators triggers', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
{ trigger: 'view', views: ['live'] },
]);
manager.addListener(listener);
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({ platform: 'acc', type: 'camera' }),
);
stateManager.setState({ view: 'live' });
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({ platform: 'acc', type: 'view' }),
);
});
it('should notify every registered listener', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
]);
const other = vi.fn();
manager.addListener(listener);
manager.addListener(other);
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
expect(other).toHaveBeenCalledTimes(1);
});
it('should register the same listener only once', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
]);
manager.addListener(listener);
manager.addListener(listener);
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
});
it('should not notify a removed listener', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
]);
manager.addListener(listener);
manager.removeListener(listener);
stateManager.setState({ camera: 'front' });
expect(listener).not.toHaveBeenCalled();
});
it('should stop triggering after destroy', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
]);
manager.addListener(listener);
manager.destroy();
stateManager.setState({ camera: 'front' });
expect(listener).not.toHaveBeenCalled();
});
it('should subscribe its evaluators only once', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'] },
]);
manager.addListener(listener);
// A repeat subscribe is ignored, so the evaluator is not attached twice and
// the listener fires once rather than once per subscription.
manager.subscribe();
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
});
describe('enabled', () => {
const ENABLED_TEMPLATE = '{{ is_state("binary_sensor.flag", "on") }}';
// These cases gate on a rendered `enabled` template, so use a real engine
// loaded for the synchronous renderer (rather than the shared mock).
const templateManager = new TemplateManager();
beforeAll(async () => {
stubConnectedHomeAssistant();
await templateManager.loadRenderer();
});
const createWithFlag = (
enabled: string,
state: string | null,
): { stateManager: ConditionStateManager; listener: Mock } => {
const stateManager = new ConditionStateManager();
if (state !== null) {
stateManager.setState({
hass: createHASS({
'binary_sensor.flag': createStateEntity({ state }),
}),
});
}
const manager = new TriggersManager(
[{ trigger: 'camera', cameras: ['front'], enabled }],
stateManager,
createHASSManager(),
templateManager,
);
manager.subscribe();
const listener = vi.fn();
manager.addListener(listener);
return { stateManager, listener };
};
it('should not trigger a disabled trigger', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'], enabled: false },
]);
manager.addListener(listener);
stateManager.setState({ camera: 'front' });
expect(listener).not.toHaveBeenCalled();
});
it('should trigger an explicitly enabled trigger', () => {
const { manager, stateManager, listener } = create([
{ trigger: 'camera', cameras: ['front'], enabled: true },
]);
manager.addListener(listener);
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
});
it('should not trigger a trigger whose enabled template does not render true', () => {
const { stateManager, listener } = createWithFlag(ENABLED_TEMPLATE, 'off');
stateManager.setState({ camera: 'front' });
expect(listener).not.toHaveBeenCalled();
});
it('should trigger a trigger whose enabled template renders true', () => {
const { stateManager, listener } = createWithFlag(ENABLED_TEMPLATE, 'on');
stateManager.setState({ camera: 'front' });
expect(listener).toHaveBeenCalledTimes(1);
});
it('should not trigger a trigger whose enabled template cannot render without hass', () => {
const { stateManager, listener } = createWithFlag(ENABLED_TEMPLATE, null);
stateManager.setState({ camera: 'front' });
// With no hass the enabled template cannot render, so the trigger fails
// closed and does not fire.
expect(listener).not.toHaveBeenCalled();
});
it('should re-evaluate the enabled template on each trigger', () => {
const stateManager = new ConditionStateManager();
stateManager.setState({
hass: createHASS({ 'binary_sensor.flag': createStateEntity({ state: 'off' }) }),
});
const manager = new TriggersManager(
[{ trigger: 'camera', cameras: ['front', 'back'], enabled: ENABLED_TEMPLATE }],
stateManager,
createHASSManager(),
templateManager,
);
manager.subscribe();
const listener = vi.fn();
manager.addListener(listener);
// Flag off: the enabled template renders false, so the camera change is
// suppressed and the listener is not notified.
stateManager.setState({ camera: 'front' });
expect(listener).not.toHaveBeenCalled();
// Flag flips on: a subsequent trigger is now allowed through.
stateManager.setState({
hass: createHASS({ 'binary_sensor.flag': createStateEntity({ state: 'on' }) }),
});
stateManager.setState({ camera: 'back' });
expect(listener).toHaveBeenCalledTimes(1);
});
});
});