Add initial support for automations.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { AutomationsController, AutomationsControllerError } from '../src/automations';
|
||||
import { ConditionController } from '../src/conditions';
|
||||
import { automationsSchema, FrigateCardError } from '../src/types';
|
||||
import { frigateCardHandleAction } from '../src/utils/action.js';
|
||||
import { createHASS } from './test-utils';
|
||||
|
||||
vi.mock('../src/utils/action.js');
|
||||
|
||||
describe('AutomationsController', () => {
|
||||
const actions = [
|
||||
{
|
||||
action: 'custom:frigate-card-action',
|
||||
frigate_card_action: 'clips',
|
||||
},
|
||||
];
|
||||
const conditions = { fullscreen: true };
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should do nothing without automations', () => {
|
||||
const automationController = new AutomationsController(undefined);
|
||||
automationController.execute(
|
||||
mock<HTMLElement>(),
|
||||
createHASS(),
|
||||
new ConditionController(),
|
||||
);
|
||||
expect(frigateCardHandleAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute actions', () => {
|
||||
const automations = automationsSchema.parse([
|
||||
{
|
||||
conditions: conditions,
|
||||
actions: actions,
|
||||
},
|
||||
]);
|
||||
|
||||
const automationController = new AutomationsController(automations);
|
||||
const conditionController = new ConditionController();
|
||||
const element = mock<HTMLElement>();
|
||||
const hass = createHASS();
|
||||
|
||||
automationController.execute(element, hass, conditionController);
|
||||
expect(frigateCardHandleAction).not.toBeCalled();
|
||||
|
||||
conditionController.setState({ fullscreen: true });
|
||||
automationController.execute(element, hass, conditionController);
|
||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||
|
||||
// Automation will not re-fire when condition continues to evaluate the
|
||||
// same.
|
||||
automationController.execute(element, hass, conditionController);
|
||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||
|
||||
conditionController.setState({ fullscreen: false });
|
||||
automationController.execute(element, hass, conditionController);
|
||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||
|
||||
conditionController.setState({ fullscreen: true });
|
||||
automationController.execute(element, hass, conditionController);
|
||||
expect(frigateCardHandleAction).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should execute actions_not', () => {
|
||||
const automations = automationsSchema.parse([
|
||||
{
|
||||
conditions: conditions,
|
||||
actions_not: actions,
|
||||
},
|
||||
]);
|
||||
|
||||
const automationController = new AutomationsController(automations);
|
||||
automationController.execute(
|
||||
mock<HTMLElement>(),
|
||||
createHASS(),
|
||||
new ConditionController(),
|
||||
);
|
||||
expect(frigateCardHandleAction).toBeCalled();
|
||||
});
|
||||
|
||||
it('should prevent automation loops', () => {
|
||||
const automations = automationsSchema.parse([
|
||||
{
|
||||
conditions: { fullscreen: true },
|
||||
actions: actions,
|
||||
},
|
||||
{
|
||||
conditions: { fullscreen: false },
|
||||
actions: actions,
|
||||
},
|
||||
]);
|
||||
|
||||
const automationController = new AutomationsController(automations);
|
||||
const conditionController = new ConditionController();
|
||||
const element = mock<HTMLElement>();
|
||||
const hass = createHASS();
|
||||
|
||||
// Create a setup where one automation action causes another...
|
||||
let fullscreen = true;
|
||||
vi.mocked(frigateCardHandleAction).mockImplementation(() => {
|
||||
fullscreen = !fullscreen;
|
||||
conditionController.setState({ fullscreen: fullscreen });
|
||||
automationController.execute(element, hass, conditionController);
|
||||
});
|
||||
|
||||
conditionController.setState({ fullscreen: fullscreen });
|
||||
|
||||
expect(() =>
|
||||
automationController.execute(element, hass, conditionController),
|
||||
).toThrowError(/Too many nested automation calls/);
|
||||
expect(frigateCardHandleAction).toBeCalledTimes(10);
|
||||
});
|
||||
|
||||
it('should be able to construct error', () => {
|
||||
const error = new AutomationsControllerError('message');
|
||||
expect(error).toBeTruthy();
|
||||
expect(error instanceof FrigateCardError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,14 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
||||
import { FrigateCameraManagerEngine } from '../../src/camera-manager/frigate/engine-frigate';
|
||||
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic';
|
||||
import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye/engine-motioneye';
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { CardWideConfig } from '../../src/types.js';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
||||
import { EntityCache } from '../../src/utils/ha/entity-registry/cache';
|
||||
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
||||
import { CameraConfig, cameraConfigSchema, CardWideConfig } from '../../src/types.js';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { Entity } from '../../src/utils/ha/entity-registry/types.js';
|
||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic';
|
||||
import { FrigateCameraManagerEngine } from '../../src/camera-manager/frigate/engine-frigate';
|
||||
import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye/engine-motioneye';
|
||||
import { HassEntities } from 'home-assistant-js-websocket';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../test-utils';
|
||||
|
||||
vi.mock('../../src/utils/ha/entity-registry');
|
||||
vi.mock('../../src/utils/ha/entity-registry/cache');
|
||||
@@ -28,32 +25,6 @@ const createFactory = (options?: {
|
||||
);
|
||||
};
|
||||
|
||||
const createCameraConfig = (config: Partial<CameraConfig>): CameraConfig => {
|
||||
return cameraConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
const createHASS = (states?: HassEntities): HomeAssistant => {
|
||||
const hass = mock<HomeAssistant>();
|
||||
if (states) {
|
||||
hass.states = states;
|
||||
}
|
||||
return hass;
|
||||
};
|
||||
|
||||
const createEntity = (entity: Partial<Entity>): Entity => {
|
||||
return {
|
||||
...entity,
|
||||
config_entry_id: entity.config_entry_id ?? null,
|
||||
device_id: entity.device_id ?? null,
|
||||
disabled_by: entity.disabled_by ?? null,
|
||||
entity_id: entity.entity_id ?? 'entity_id',
|
||||
hidden_by: entity.hidden_by ?? null,
|
||||
platform: entity.platform ?? 'platform',
|
||||
translation_key: entity.translation_key ?? null,
|
||||
unique_id: entity.unique_id ?? 'unique_id',
|
||||
};
|
||||
};
|
||||
|
||||
describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
||||
it('should get frigate engine from config', async () => {
|
||||
const config = createCameraConfig({ engine: 'frigate' });
|
||||
@@ -79,7 +50,9 @@ describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(createEntity({ entity_id: 'camera.foo', platform: 'frigate' }));
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'frigate' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -94,7 +67,7 @@ describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
@@ -109,7 +82,9 @@ describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(createEntity({ entity_id: 'camera.foo', platform: 'generic' }));
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'generic' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
ConditionController,
|
||||
ConditionEvaluateRequestEvent,
|
||||
evaluateConditionViaEvent,
|
||||
getOverriddenConfig,
|
||||
getOverridesByKey,
|
||||
} from '../src/conditions';
|
||||
import { createCondition, createConfig, createStateEntity } from './test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('ConditionEvaluateRequestEvent', () => {
|
||||
it('should construct', () => {
|
||||
const condition = createCondition({ fullscreen: true });
|
||||
const event = new ConditionEvaluateRequestEvent(condition, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
|
||||
expect(event.type).toBe('frigate-card:condition:evaluate');
|
||||
expect(event.condition).toBe(condition);
|
||||
expect(event.bubbles).toBeTruthy();
|
||||
expect(event.composed).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateConditionViaEvent', () => {
|
||||
it('should evaluate true without condition', () => {
|
||||
const element = document.createElement('div');
|
||||
expect(evaluateConditionViaEvent(element)).toBeTruthy();
|
||||
});
|
||||
it('should dispatch event with condition and evaluate true', () => {
|
||||
const element = document.createElement('div');
|
||||
const condition = createCondition({ fullscreen: true });
|
||||
const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => {
|
||||
expect(ev.condition).toBe(condition);
|
||||
ev.evaluation = true;
|
||||
});
|
||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
||||
|
||||
expect(evaluateConditionViaEvent(element, condition)).toBeTruthy();
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
it('should dispatch event with condition and evaluate false', () => {
|
||||
const element = document.createElement('div');
|
||||
const condition = createCondition({ fullscreen: true });
|
||||
const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => {
|
||||
expect(ev.condition).toBe(condition);
|
||||
ev.evaluation = false;
|
||||
});
|
||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
||||
|
||||
expect(evaluateConditionViaEvent(element, condition)).toBeFalsy();
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
it('should dispatch event evaluate false if no evaluation', () => {
|
||||
const element = document.createElement('div');
|
||||
const condition = createCondition({ fullscreen: true });
|
||||
const handler = vi.fn();
|
||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
||||
|
||||
expect(evaluateConditionViaEvent(element, condition)).toBeFalsy();
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverriddenConfig', () => {
|
||||
const config = {
|
||||
menu: {
|
||||
style: 'none',
|
||||
},
|
||||
};
|
||||
const overrides = [
|
||||
{
|
||||
overrides: {
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
},
|
||||
conditions: {
|
||||
fullscreen: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should not override config', () => {
|
||||
const controller = new ConditionController();
|
||||
expect(getOverriddenConfig(controller, config, overrides)).toBe(config);
|
||||
});
|
||||
|
||||
it('should override config', () => {
|
||||
const controller = new ConditionController();
|
||||
controller.setState({ fullscreen: true });
|
||||
|
||||
expect(getOverriddenConfig(controller, config, overrides)).toEqual({
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverridesByKey', () => {
|
||||
const condition = {
|
||||
fullscreen: true,
|
||||
};
|
||||
const override = {
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
};
|
||||
const overrides = [
|
||||
{
|
||||
overrides: override,
|
||||
conditions: condition,
|
||||
},
|
||||
];
|
||||
|
||||
it('should get overrides', () => {
|
||||
expect(getOverridesByKey('menu', overrides)).toEqual([
|
||||
{ conditions: condition, overrides: { style: 'above' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get no overrides', () => {
|
||||
expect(getOverridesByKey('live', overrides)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should get no overrides when undefined', () => {
|
||||
expect(getOverridesByKey('live')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConditionController', () => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [],
|
||||
elements: [
|
||||
{
|
||||
type: 'custom:frigate-card-conditional',
|
||||
conditions: {
|
||||
fullscreen: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: 'custom:nested-unknown-object',
|
||||
unknown_key: {
|
||||
type: 'custom:frigate-card-conditional',
|
||||
conditions: {
|
||||
media_query: 'media query goes here',
|
||||
},
|
||||
elements: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
overrides: {
|
||||
menu: {
|
||||
style: 'overlay',
|
||||
},
|
||||
},
|
||||
conditions: {
|
||||
fullscreen: true,
|
||||
state: [
|
||||
{
|
||||
entity: 'binary_sensor.foo',
|
||||
state: 'on',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should add listener', () => {
|
||||
const controller = new ConditionController();
|
||||
const handler = vi.fn();
|
||||
controller.addStateListener(handler);
|
||||
controller.setState({ fullscreen: true });
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
|
||||
it('should remove listener', () => {
|
||||
const controller = new ConditionController();
|
||||
const handler = vi.fn();
|
||||
controller.addStateListener(handler);
|
||||
controller.removeStateListener(handler);
|
||||
controller.setState({ fullscreen: true });
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should get wrapper', () => {
|
||||
const controller = new ConditionController();
|
||||
const wrapper_1 = controller.getEpoch();
|
||||
expect(wrapper_1).toEqual({ controller: controller });
|
||||
|
||||
controller.setState({ fullscreen: true });
|
||||
|
||||
const wrapper_2 = controller.getEpoch();
|
||||
expect(wrapper_2).toEqual({ controller: controller });
|
||||
|
||||
// Since the state was set the wrappers should be different.
|
||||
expect(wrapper_1).not.toBe(wrapper_2);
|
||||
});
|
||||
|
||||
it('should not return hasHAStateConditions without HA state conditions', () => {
|
||||
const controller = new ConditionController();
|
||||
expect(controller.hasHAStateConditions).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return hasHAStateConditions with HA state conditions', () => {
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
|
||||
matches: false,
|
||||
addEventListener: vi.fn(),
|
||||
} as unknown as MediaQueryList);
|
||||
const controller = new ConditionController(createConfig(config));
|
||||
expect(controller.hasHAStateConditions).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with a view', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = { view: ['foo'] };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ view: 'foo' });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with fullscreen', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = { fullscreen: true };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ fullscreen: true });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({ fullscreen: false });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with expand', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = { expand: true };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ expand: true });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({ expand: false });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with camera', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = { camera: ['bar'] };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ camera: 'bar' });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({ camera: 'will-not-match' });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with ha state positive check', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = {
|
||||
state: [
|
||||
{
|
||||
entity: 'binary_sensor.foo',
|
||||
state: 'on',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
});
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with ha state negative check', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = {
|
||||
state: [
|
||||
{
|
||||
entity: 'binary_sensor.foo',
|
||||
state_not: 'on',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({
|
||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
||||
});
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with media_loaded', () => {
|
||||
const controller = new ConditionController();
|
||||
const condition = { media_loaded: true };
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
controller.setState({ media_loaded: true });
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
controller.setState({ media_loaded: false });
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should evaluate conditions with media query', () => {
|
||||
vi.spyOn(window, 'matchMedia')
|
||||
.mockReturnValueOnce(<MediaQueryList>{ matches: true })
|
||||
.mockReturnValueOnce(<MediaQueryList>{ matches: false });
|
||||
|
||||
const controller = new ConditionController();
|
||||
const condition = { media_query: 'whatever' };
|
||||
expect(controller.evaluateCondition(condition)).toBeTruthy();
|
||||
expect(controller.evaluateCondition(condition)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should trigger on changes to media query conditions', () => {
|
||||
const addEventListener = vi.fn();
|
||||
const removeEventListener = vi.fn();
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
|
||||
matches: true,
|
||||
addEventListener: addEventListener,
|
||||
removeEventListener: removeEventListener,
|
||||
} as unknown as MediaQueryList);
|
||||
|
||||
const controller = new ConditionController(createConfig(config));
|
||||
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||
|
||||
const callback = vi.fn();
|
||||
controller.addStateListener(callback);
|
||||
|
||||
// Call the media query callback and use it to pretend a match happened. The
|
||||
// callback is the 0th mock innvocation and the 1st argument.
|
||||
addEventListener.mock.calls[0][1]();
|
||||
|
||||
// This should result in a callback to our state listener.
|
||||
expect(callback).toBeCalled();
|
||||
|
||||
// Destroy the controller, which should remove the media query listener.
|
||||
controller.destroy();
|
||||
expect(removeEventListener).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateCardCondition,
|
||||
FrigateCardConfig,
|
||||
cameraConfigSchema,
|
||||
frigateCardConditionSchema,
|
||||
frigateCardConfigSchema,
|
||||
} from '../src/types';
|
||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||
|
||||
export const createCameraConfig = (config: Partial<CameraConfig>): CameraConfig => {
|
||||
return cameraConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
export const createCondition = (
|
||||
condition?: Partial<FrigateCardCondition>,
|
||||
): FrigateCardCondition => {
|
||||
return frigateCardConditionSchema.parse(condition ?? {});
|
||||
};
|
||||
|
||||
export const createConfig = (config?: Partial<FrigateCardConfig>): FrigateCardConfig => {
|
||||
return frigateCardConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
export const createHASS = (states?: HassEntities): HomeAssistant => {
|
||||
const hass = mock<HomeAssistant>();
|
||||
if (states) {
|
||||
hass.states = states;
|
||||
}
|
||||
return hass;
|
||||
};
|
||||
|
||||
export const createRegistryEntity = (entity?: Partial<Entity>): Entity => {
|
||||
return {
|
||||
config_entry_id: entity?.config_entry_id ?? null,
|
||||
device_id: entity?.device_id ?? null,
|
||||
disabled_by: entity?.disabled_by ?? null,
|
||||
entity_id: entity?.entity_id ?? 'entity_id',
|
||||
hidden_by: entity?.hidden_by ?? null,
|
||||
platform: entity?.platform ?? 'platform',
|
||||
translation_key: entity?.translation_key ?? null,
|
||||
unique_id: entity?.unique_id ?? 'unique_id',
|
||||
};
|
||||
};
|
||||
|
||||
export const createStateEntity = (entity?: Partial<HassEntity>): HassEntity => {
|
||||
return {
|
||||
entity_id: entity?.entity_id ?? 'entity_id',
|
||||
state: entity?.state ?? 'on',
|
||||
last_changed: entity?.last_changed ?? 'never',
|
||||
last_updated: entity?.last_updated ?? 'never',
|
||||
attributes: entity?.attributes ?? {},
|
||||
context: entity?.context ?? {
|
||||
id: 'id',
|
||||
parent_id: 'parent_id',
|
||||
user_id: 'user_id',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
import { handleActionConfig, hasAction } from 'custom-card-helpers';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { actionSchema } from '../../src/types';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
createFrigateCardCustomAction,
|
||||
frigateCardHandleActionConfig,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../../src/utils/action';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
vi.mock('custom-card-helpers');
|
||||
|
||||
describe('convertActionToFrigateCardCustomAction', () => {
|
||||
it('should skip null action', () => {
|
||||
expect(convertActionToFrigateCardCustomAction(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should parse valid', () => {
|
||||
expect(
|
||||
convertActionToFrigateCardCustomAction({
|
||||
action: 'custom:frigate-card-action',
|
||||
frigate_card_action: 'download',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'download',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse invalid', () => {
|
||||
expect(convertActionToFrigateCardCustomAction('this is garbage')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFrigateCardCustomAction', () => {
|
||||
it('should create camera_select', () => {
|
||||
expect(
|
||||
createFrigateCardCustomAction('camera_select', {
|
||||
camera: 'camera',
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
camera: 'camera',
|
||||
frigate_card_action: 'camera_select',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create camera_select without camera', () => {
|
||||
expect(createFrigateCardCustomAction('camera_select')).toBeNull();
|
||||
});
|
||||
|
||||
it('should create media_player', () => {
|
||||
expect(
|
||||
createFrigateCardCustomAction('media_player', {
|
||||
media_player: 'device',
|
||||
media_player_action: 'play',
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'media_player',
|
||||
media_player: 'device',
|
||||
media_player_action: 'play',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create media_player without player or action', () => {
|
||||
expect(
|
||||
createFrigateCardCustomAction('media_player', {
|
||||
media_player_action: 'play',
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
createFrigateCardCustomAction('media_player', {
|
||||
media_player: 'device',
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should create general action', () => {
|
||||
expect(
|
||||
createFrigateCardCustomAction('clips', {
|
||||
cardID: 'card_id',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'clips',
|
||||
card_id: 'card_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionConfigGivenAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'clips',
|
||||
});
|
||||
|
||||
it('should not handle undefined arguments', () => {
|
||||
expect(getActionConfigGivenAction()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not handle unknown interactions', () => {
|
||||
expect(
|
||||
getActionConfigGivenAction('triple_poke', { triple_poke_action: action }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle tap actions', () => {
|
||||
expect(getActionConfigGivenAction('tap', { tap_action: action })).toBe(action);
|
||||
});
|
||||
|
||||
it('should handle hold actions', () => {
|
||||
expect(getActionConfigGivenAction('hold', { hold_action: action })).toBe(action);
|
||||
});
|
||||
|
||||
it('should handle double_tap actions', () => {
|
||||
expect(getActionConfigGivenAction('double_tap', { double_tap_action: action })).toBe(
|
||||
action,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle end_tap actions', () => {
|
||||
expect(getActionConfigGivenAction('end_tap', { end_tap_action: action })).toBe(
|
||||
action,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle start_tap actions', () => {
|
||||
expect(getActionConfigGivenAction('start_tap', { start_tap_action: action })).toBe(
|
||||
action,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('frigateCardHandleActionConfig', () => {
|
||||
const element = document.createElement('div');
|
||||
const action = actionSchema.parse({
|
||||
action: 'none',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not handle missing arguments', () => {
|
||||
expect(
|
||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'triple_poke'),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should handle simple case', () => {
|
||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', action);
|
||||
expect(handleActionConfig).toBeCalled();
|
||||
});
|
||||
|
||||
it('should handle array case', () => {
|
||||
frigateCardHandleActionConfig(element, createHASS(), {}, 'tap', [
|
||||
action,
|
||||
action,
|
||||
action,
|
||||
]);
|
||||
expect(handleActionConfig).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frigateCardHasAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'toggle',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle non-array case', () => {
|
||||
expect(frigateCardHasAction(action)).toBeFalsy();
|
||||
expect(hasAction).toBeCalledTimes(1);
|
||||
});
|
||||
it('should handle array case', () => {
|
||||
expect(frigateCardHasAction([action, action, action])).toBeFalsy();
|
||||
expect(hasAction).toBeCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('stopEventFromActivatingCardWideActions', () => {
|
||||
it('should stop event from propogating', () => {
|
||||
const event = mock<Event>();
|
||||
stopEventFromActivatingCardWideActions(event);
|
||||
expect(event.stopPropagation).toBeCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user