refactor: Restructure condition evaluation into per-type evaluator classes (#2518)
This commit is contained in:
committed by
dermotduffy
parent
3480a55a87
commit
12ae3b216c
@@ -1,13 +1,11 @@
|
|||||||
import { TemplateRenderer } from '../card-controller/templates';
|
import { TemplateRenderer } from '../card-controller/templates';
|
||||||
import { getConfigValue } from '../config/management';
|
|
||||||
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
|
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
|
||||||
import { isBeingCasted } from '../utils/casting';
|
import { ConditionEvaluator } from './conditions/types';
|
||||||
import { isCompanionApp } from '../utils/companion';
|
import { createConditionEvaluator } from './factory';
|
||||||
import {
|
import {
|
||||||
ConditionsEvaluationResult,
|
ConditionsEvaluationResult,
|
||||||
ConditionsListener,
|
ConditionsListener,
|
||||||
ConditionsManagerReadonlyInterface,
|
ConditionsManagerReadonlyInterface,
|
||||||
ConditionState,
|
|
||||||
ConditionStateChange,
|
ConditionStateChange,
|
||||||
ConditionStateManagerReadonlyInterface,
|
ConditionStateManagerReadonlyInterface,
|
||||||
ConditionsTriggerData,
|
ConditionsTriggerData,
|
||||||
@@ -19,29 +17,29 @@ import {
|
|||||||
* associated with a result).
|
* associated with a result).
|
||||||
*/
|
*/
|
||||||
export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||||
private _conditions: AdvancedCameraCardCondition[];
|
|
||||||
private _stateManager: ConditionStateManagerReadonlyInterface | null;
|
private _stateManager: ConditionStateManagerReadonlyInterface | null;
|
||||||
|
private _evaluators: ConditionEvaluator[];
|
||||||
|
|
||||||
private _listeners: ConditionsListener[] = [];
|
private _listeners: ConditionsListener[] = [];
|
||||||
private _mediaQueries: MediaQueryList[] = [];
|
|
||||||
private _evaluation: ConditionsEvaluationResult = { result: false };
|
private _evaluation: ConditionsEvaluationResult = { result: false };
|
||||||
private _templateRenderer: TemplateRenderer = new TemplateRenderer();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
conditions: AdvancedCameraCardCondition[],
|
conditions: AdvancedCameraCardCondition[],
|
||||||
stateManager?: ConditionStateManagerReadonlyInterface | null,
|
stateManager?: ConditionStateManagerReadonlyInterface | null,
|
||||||
) {
|
) {
|
||||||
this._conditions = conditions;
|
const context = { templateRenderer: new TemplateRenderer() };
|
||||||
conditions.forEach((condition) => {
|
this._evaluators = conditions.map((condition) =>
|
||||||
if (condition.condition === 'screen') {
|
createConditionEvaluator(condition, context),
|
||||||
const mql = window.matchMedia(condition.media_query);
|
);
|
||||||
mql.addEventListener('change', this._mediaQueryHandler);
|
|
||||||
this._mediaQueries.push(mql);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._stateManager = stateManager ?? null;
|
this._stateManager = stateManager ?? null;
|
||||||
|
|
||||||
|
// Subscribe evaluators that have external change sources (currently
|
||||||
|
// `screen`), including nested evaluators inside composites.
|
||||||
|
this._evaluators.forEach((evaluator) =>
|
||||||
|
evaluator.subscribe?.(() => this._evaluate()),
|
||||||
|
);
|
||||||
|
|
||||||
// Do an initial condition evaluation, but without calling listeners.
|
// Do an initial condition evaluation, but without calling listeners.
|
||||||
this._evaluate({ callListeners: false });
|
this._evaluate({ callListeners: false });
|
||||||
|
|
||||||
@@ -53,11 +51,8 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
|
|
||||||
this._listeners.forEach((l) => this.removeListener(l));
|
this._listeners.forEach((l) => this.removeListener(l));
|
||||||
|
|
||||||
this._mediaQueries.forEach((mql) =>
|
this._evaluators.forEach((evaluator) => evaluator.destroy?.());
|
||||||
mql.removeEventListener('change', this._mediaQueryHandler),
|
this._evaluators = [];
|
||||||
);
|
|
||||||
this._mediaQueries = [];
|
|
||||||
this._conditions = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addListener(listener: ConditionsListener): void {
|
public addListener(listener: ConditionsListener): void {
|
||||||
@@ -74,8 +69,6 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
return this._evaluation;
|
return this._evaluation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _mediaQueryHandler = () => this._evaluate();
|
|
||||||
|
|
||||||
private _stateManagerHandler = (stateChange: ConditionStateChange): void => {
|
private _stateManagerHandler = (stateChange: ConditionStateChange): void => {
|
||||||
this._evaluate({ stateChange });
|
this._evaluate({ stateChange });
|
||||||
};
|
};
|
||||||
@@ -89,12 +82,8 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
let result = true;
|
let result = true;
|
||||||
let triggerData: ConditionsTriggerData = {};
|
let triggerData: ConditionsTriggerData = {};
|
||||||
|
|
||||||
for (const condition of this._conditions) {
|
for (const evaluator of this._evaluators) {
|
||||||
const evaluation = this._evaluateCondition(
|
const evaluation = evaluator.evaluate(state, options?.stateChange?.old);
|
||||||
condition,
|
|
||||||
state,
|
|
||||||
options?.stateChange?.old,
|
|
||||||
);
|
|
||||||
if (!evaluation.result) {
|
if (!evaluation.result) {
|
||||||
result = false;
|
result = false;
|
||||||
break;
|
break;
|
||||||
@@ -121,242 +110,4 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _evaluateCondition(
|
|
||||||
condition: AdvancedCameraCardCondition,
|
|
||||||
newState?: ConditionState,
|
|
||||||
oldState?: ConditionState,
|
|
||||||
): ConditionsEvaluationResult {
|
|
||||||
switch (condition.condition) {
|
|
||||||
case undefined:
|
|
||||||
case 'state': {
|
|
||||||
const fromState = oldState?.hass?.states?.[condition.entity]?.state;
|
|
||||||
const toState = newState?.hass?.states?.[condition.entity]?.state;
|
|
||||||
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
(!condition.state && !condition.state_not && toState !== fromState) ||
|
|
||||||
((!!condition.state || !!condition.state_not) &&
|
|
||||||
!!toState &&
|
|
||||||
(!condition.state ||
|
|
||||||
(Array.isArray(condition.state)
|
|
||||||
? condition.state.includes(toState)
|
|
||||||
: condition.state === toState)) &&
|
|
||||||
(!condition.state_not ||
|
|
||||||
(Array.isArray(condition.state_not)
|
|
||||||
? !condition.state_not.includes(toState)
|
|
||||||
: condition.state_not !== toState))),
|
|
||||||
...(fromState !== toState && {
|
|
||||||
triggerData: {
|
|
||||||
state: {
|
|
||||||
entity: condition.entity,
|
|
||||||
...(fromState && { from: fromState }),
|
|
||||||
...(toState && { to: toState }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'view': {
|
|
||||||
const oldView = oldState?.view;
|
|
||||||
const newView = newState?.view;
|
|
||||||
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
(!!newView && condition.views?.includes(newView)) ||
|
|
||||||
(newView !== oldView && !condition.views?.length),
|
|
||||||
...(oldView !== newView && {
|
|
||||||
triggerData: {
|
|
||||||
...((oldState?.view || newState?.view) && {
|
|
||||||
view: {
|
|
||||||
...(oldState?.view && { from: oldState.view }),
|
|
||||||
...(newState?.view && { to: newState.view }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'fullscreen':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
newState?.fullscreen !== undefined &&
|
|
||||||
condition.fullscreen === newState.fullscreen,
|
|
||||||
};
|
|
||||||
case 'expand':
|
|
||||||
return {
|
|
||||||
result: newState?.expand !== undefined && condition.expand === newState.expand,
|
|
||||||
};
|
|
||||||
case 'camera': {
|
|
||||||
const oldCamera = oldState?.camera;
|
|
||||||
const newCamera = newState?.camera;
|
|
||||||
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
(!!newCamera && !!condition.cameras?.includes(newCamera)) ||
|
|
||||||
(newCamera !== oldCamera && !condition.cameras?.length),
|
|
||||||
...(newCamera !== oldCamera && {
|
|
||||||
triggerData: {
|
|
||||||
...((oldState?.camera || newState?.camera) && {
|
|
||||||
camera: {
|
|
||||||
...(oldState?.camera && { from: oldState?.camera }),
|
|
||||||
...(newState?.camera && { to: newState?.camera }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'numeric_state':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.hass?.states &&
|
|
||||||
condition.entity in newState.hass?.states &&
|
|
||||||
newState.hass.states[condition.entity].state !== undefined &&
|
|
||||||
(condition.above === undefined ||
|
|
||||||
Number(newState.hass.states[condition.entity].state) > condition.above) &&
|
|
||||||
(condition.below === undefined ||
|
|
||||||
Number(newState.hass.states[condition.entity].state) < condition.below),
|
|
||||||
};
|
|
||||||
case 'user':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.hass?.user && condition.users.includes(newState.hass.user.id),
|
|
||||||
};
|
|
||||||
case 'media_loaded':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
newState?.mediaLoadedInfo !== undefined &&
|
|
||||||
condition.media_loaded === !!newState.mediaLoadedInfo,
|
|
||||||
};
|
|
||||||
case 'screen':
|
|
||||||
return { result: window.matchMedia(condition.media_query).matches };
|
|
||||||
case 'display_mode':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.displayMode && condition.display_mode === newState.displayMode,
|
|
||||||
};
|
|
||||||
case 'triggered':
|
|
||||||
return {
|
|
||||||
result: condition.triggered.some((triggeredCameraID) =>
|
|
||||||
newState?.triggered?.has(triggeredCameraID),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
case 'interaction':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
newState?.interaction !== undefined &&
|
|
||||||
condition.interaction === newState.interaction,
|
|
||||||
};
|
|
||||||
case 'microphone':
|
|
||||||
return {
|
|
||||||
result: newState?.microphone?.muted === condition.muted,
|
|
||||||
};
|
|
||||||
case 'call':
|
|
||||||
return {
|
|
||||||
result: (condition.call ?? true) === (newState?.call ?? false),
|
|
||||||
};
|
|
||||||
case 'key':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.keys &&
|
|
||||||
condition.key in newState.keys &&
|
|
||||||
(condition.state ?? 'down') === newState.keys[condition.key].state &&
|
|
||||||
(condition.ctrl === undefined ||
|
|
||||||
condition.ctrl === !!newState.keys[condition.key].ctrl) &&
|
|
||||||
(condition.alt === undefined ||
|
|
||||||
condition.alt === !!newState.keys[condition.key].alt) &&
|
|
||||||
(condition.meta === undefined ||
|
|
||||||
condition.meta === !!newState.keys[condition.key].meta) &&
|
|
||||||
(condition.shift === undefined ||
|
|
||||||
condition.shift === !!newState.keys[condition.key].shift),
|
|
||||||
};
|
|
||||||
case 'user_agent':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.userAgent &&
|
|
||||||
(!condition.user_agent || condition.user_agent === newState.userAgent) &&
|
|
||||||
(condition.casting === undefined ||
|
|
||||||
condition.casting === isBeingCasted(newState.userAgent)) &&
|
|
||||||
(condition.companion === undefined ||
|
|
||||||
condition.companion === isCompanionApp(newState.userAgent)) &&
|
|
||||||
(condition.user_agent_re === undefined ||
|
|
||||||
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
|
||||||
};
|
|
||||||
case 'config': {
|
|
||||||
const newConfig = newState?.config;
|
|
||||||
const oldConfig = oldState?.config;
|
|
||||||
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newConfig &&
|
|
||||||
newConfig !== oldConfig &&
|
|
||||||
(!condition.paths?.length ||
|
|
||||||
condition.paths.some(
|
|
||||||
(key) =>
|
|
||||||
getConfigValue(newConfig, key) !==
|
|
||||||
(oldConfig ? getConfigValue(oldConfig, key) : undefined),
|
|
||||||
)),
|
|
||||||
...(newConfig !== oldConfig && {
|
|
||||||
triggerData: {
|
|
||||||
config: {
|
|
||||||
...((oldState?.config || newState?.config) && {
|
|
||||||
...(oldState?.config && { from: oldState?.config }),
|
|
||||||
...(newState?.config && { to: newState?.config }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'initialized':
|
|
||||||
return { result: !!newState?.initialized };
|
|
||||||
case 'or':
|
|
||||||
for (const subCondition of condition.conditions) {
|
|
||||||
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
|
|
||||||
if (evaluation.result) {
|
|
||||||
return evaluation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { result: false };
|
|
||||||
case 'and': {
|
|
||||||
let triggerData: ConditionsTriggerData = {};
|
|
||||||
for (const subCondition of condition.conditions) {
|
|
||||||
const evaluation = this._evaluateCondition(subCondition, newState, oldState);
|
|
||||||
if (!evaluation.result) {
|
|
||||||
return { result: false };
|
|
||||||
}
|
|
||||||
triggerData = {
|
|
||||||
...triggerData,
|
|
||||||
...evaluation.triggerData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { result: true, triggerData };
|
|
||||||
}
|
|
||||||
case 'not': {
|
|
||||||
// "Not" is an inverted `or` (NOR). There is no trigger data for "not
|
|
||||||
// triggering".
|
|
||||||
return {
|
|
||||||
result: !this._evaluateCondition(
|
|
||||||
{
|
|
||||||
...condition,
|
|
||||||
condition: 'or',
|
|
||||||
},
|
|
||||||
newState,
|
|
||||||
oldState,
|
|
||||||
).result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'template':
|
|
||||||
return {
|
|
||||||
result:
|
|
||||||
!!newState?.hass &&
|
|
||||||
this._templateRenderer.renderRecursively(
|
|
||||||
newState.hass,
|
|
||||||
condition.value_template,
|
|
||||||
{ conditionState: newState },
|
|
||||||
) === true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
ConditionsEvaluationResult,
|
||||||
|
ConditionState,
|
||||||
|
ConditionsTriggerData,
|
||||||
|
} from '../types';
|
||||||
|
import { CompositeConditionEvaluator } from './composite';
|
||||||
|
|
||||||
|
export class AndConditionEvaluator extends CompositeConditionEvaluator {
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
let triggerData: ConditionsTriggerData = {};
|
||||||
|
for (const child of this._children) {
|
||||||
|
const evaluation = child.evaluate(newState, oldState);
|
||||||
|
if (!evaluation.result) {
|
||||||
|
return { result: false };
|
||||||
|
}
|
||||||
|
triggerData = {
|
||||||
|
...triggerData,
|
||||||
|
...evaluation.triggerData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { result: true, triggerData };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class CallConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'call'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'call'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result: (this._condition.call ?? true) === (newState?.call ?? false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class CameraConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'camera'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'camera'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
const oldCamera = oldState?.camera;
|
||||||
|
const newCamera = newState?.camera;
|
||||||
|
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(!!newCamera && !!this._condition.cameras?.includes(newCamera)) ||
|
||||||
|
(newCamera !== oldCamera && !this._condition.cameras?.length),
|
||||||
|
...(newCamera !== oldCamera && {
|
||||||
|
triggerData: {
|
||||||
|
...((oldState?.camera || newState?.camera) && {
|
||||||
|
camera: {
|
||||||
|
...(oldState?.camera && { from: oldState?.camera }),
|
||||||
|
...(newState?.camera && { to: newState?.camera }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionEvaluatorSubscriptionCallback } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for the `or`/`and`/`not` composites: each holds child evaluators
|
||||||
|
* and forwards subscription/teardown to them. Subclasses provide `evaluate`.
|
||||||
|
*/
|
||||||
|
export abstract class CompositeConditionEvaluator implements ConditionEvaluator {
|
||||||
|
protected _children: ConditionEvaluator[];
|
||||||
|
|
||||||
|
constructor(children: ConditionEvaluator[]) {
|
||||||
|
this._children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult;
|
||||||
|
|
||||||
|
public subscribe(onChange: ConditionEvaluatorSubscriptionCallback): void {
|
||||||
|
this._children.forEach((child) => child.subscribe?.(onChange));
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this._children.forEach((child) => child.destroy?.());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { getConfigValue } from '../../config/management';
|
||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class ConfigConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'config'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'config'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
const newConfig = newState?.config;
|
||||||
|
const oldConfig = oldState?.config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newConfig &&
|
||||||
|
newConfig !== oldConfig &&
|
||||||
|
(!this._condition.paths?.length ||
|
||||||
|
this._condition.paths.some(
|
||||||
|
(key) =>
|
||||||
|
getConfigValue(newConfig, key) !==
|
||||||
|
(oldConfig ? getConfigValue(oldConfig, key) : undefined),
|
||||||
|
)),
|
||||||
|
...(newConfig !== oldConfig && {
|
||||||
|
triggerData: {
|
||||||
|
config: {
|
||||||
|
...((oldState?.config || newState?.config) && {
|
||||||
|
...(oldState?.config && { from: oldState?.config }),
|
||||||
|
...(newState?.config && { to: newState?.config }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class DisplayModeConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'display_mode'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'display_mode'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.displayMode && this._condition.display_mode === newState.displayMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class ExpandConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'expand'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'expand'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.expand !== undefined && this._condition.expand === newState.expand,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class FullscreenConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'fullscreen'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'fullscreen'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.fullscreen !== undefined &&
|
||||||
|
this._condition.fullscreen === newState.fullscreen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator } from './types';
|
||||||
|
|
||||||
|
export class InitializedConditionEvaluator implements ConditionEvaluator {
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return { result: !!newState?.initialized };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class InteractionConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'interaction'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'interaction'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.interaction !== undefined &&
|
||||||
|
this._condition.interaction === newState.interaction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class KeyConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'key'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'key'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
const condition = this._condition;
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.keys &&
|
||||||
|
condition.key in newState.keys &&
|
||||||
|
(condition.state ?? 'down') === newState.keys[condition.key].state &&
|
||||||
|
(condition.ctrl === undefined ||
|
||||||
|
condition.ctrl === !!newState.keys[condition.key].ctrl) &&
|
||||||
|
(condition.alt === undefined ||
|
||||||
|
condition.alt === !!newState.keys[condition.key].alt) &&
|
||||||
|
(condition.meta === undefined ||
|
||||||
|
condition.meta === !!newState.keys[condition.key].meta) &&
|
||||||
|
(condition.shift === undefined ||
|
||||||
|
condition.shift === !!newState.keys[condition.key].shift),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class MediaLoadedConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'media_loaded'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'media_loaded'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
newState?.mediaLoadedInfo !== undefined &&
|
||||||
|
this._condition.media_loaded === !!newState.mediaLoadedInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class MicrophoneConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'microphone'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'microphone'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result: newState?.microphone?.muted === this._condition.muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { CompositeConditionEvaluator } from './composite';
|
||||||
|
|
||||||
|
export class NotConditionEvaluator extends CompositeConditionEvaluator {
|
||||||
|
// "Not" is an inverted `or` (NOR). There is no trigger data for "not
|
||||||
|
// triggering".
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result: !this._children.some((child) => child.evaluate(newState, oldState).result),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class NumericStateConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'numeric_state'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'numeric_state'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
const condition = this._condition;
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.hass?.states &&
|
||||||
|
condition.entity in newState.hass?.states &&
|
||||||
|
newState.hass.states[condition.entity].state !== undefined &&
|
||||||
|
(condition.above === undefined ||
|
||||||
|
Number(newState.hass.states[condition.entity].state) > condition.above) &&
|
||||||
|
(condition.below === undefined ||
|
||||||
|
Number(newState.hass.states[condition.entity].state) < condition.below),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { CompositeConditionEvaluator } from './composite';
|
||||||
|
|
||||||
|
export class OrConditionEvaluator extends CompositeConditionEvaluator {
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
for (const child of this._children) {
|
||||||
|
const evaluation = child.evaluate(newState, oldState);
|
||||||
|
if (evaluation.result) {
|
||||||
|
return evaluation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ConditionsEvaluationResult } from '../types';
|
||||||
|
import {
|
||||||
|
ConditionEvaluator,
|
||||||
|
ConditionEvaluatorSubscriptionCallback,
|
||||||
|
ConditionOfType,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export class ScreenConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _mediaQuery: MediaQueryList | null = null;
|
||||||
|
private _onChange: ConditionEvaluatorSubscriptionCallback | null = null;
|
||||||
|
|
||||||
|
private _condition: ConditionOfType<'screen'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'screen'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(): ConditionsEvaluationResult {
|
||||||
|
return { result: window.matchMedia(this._condition.media_query).matches };
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(onChange: ConditionEvaluatorSubscriptionCallback): void {
|
||||||
|
this._onChange = onChange;
|
||||||
|
this._mediaQuery = window.matchMedia(this._condition.media_query);
|
||||||
|
this._mediaQuery.addEventListener('change', this._handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this._mediaQuery?.removeEventListener('change', this._handler);
|
||||||
|
this._mediaQuery = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handler = (): void => this._onChange?.();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class StateConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'state'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'state'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
const condition = this._condition;
|
||||||
|
const fromState = oldState?.hass?.states?.[condition.entity]?.state;
|
||||||
|
const toState = newState?.hass?.states?.[condition.entity]?.state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(!condition.state && !condition.state_not && toState !== fromState) ||
|
||||||
|
((!!condition.state || !!condition.state_not) &&
|
||||||
|
!!toState &&
|
||||||
|
(!condition.state ||
|
||||||
|
(Array.isArray(condition.state)
|
||||||
|
? condition.state.includes(toState)
|
||||||
|
: condition.state === toState)) &&
|
||||||
|
(!condition.state_not ||
|
||||||
|
(Array.isArray(condition.state_not)
|
||||||
|
? !condition.state_not.includes(toState)
|
||||||
|
: condition.state_not !== toState))),
|
||||||
|
...(fromState !== toState && {
|
||||||
|
triggerData: {
|
||||||
|
state: {
|
||||||
|
entity: condition.entity,
|
||||||
|
...(fromState && { from: fromState }),
|
||||||
|
...(toState && { to: toState }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType, EvaluatorContext } from './types';
|
||||||
|
|
||||||
|
export class TemplateConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'template'>;
|
||||||
|
private _context: EvaluatorContext;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'template'>, context: EvaluatorContext) {
|
||||||
|
this._condition = condition;
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.hass &&
|
||||||
|
this._context.templateRenderer.renderRecursively(
|
||||||
|
newState.hass,
|
||||||
|
this._condition.value_template,
|
||||||
|
{ conditionState: newState },
|
||||||
|
) === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class TriggeredConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'triggered'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'triggered'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result: this._condition.triggered.some((triggeredCameraID) =>
|
||||||
|
newState?.triggered?.has(triggeredCameraID),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { TemplateRenderer } from '../../card-controller/templates';
|
||||||
|
import { AdvancedCameraCardCondition } from '../../config/schema/conditions/types';
|
||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
|
||||||
|
export type ConditionEvaluatorSubscriptionCallback = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single condition, constructed once with its configuration and evaluated
|
||||||
|
* repeatedly against incoming state.
|
||||||
|
*/
|
||||||
|
export interface ConditionEvaluator {
|
||||||
|
evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult;
|
||||||
|
|
||||||
|
// Optional hook for conditions with an external change source (e.g.
|
||||||
|
// `screen`). The owner passes a callback to request re-evaluation.
|
||||||
|
subscribe?(onChange: ConditionEvaluatorSubscriptionCallback): void;
|
||||||
|
|
||||||
|
destroy?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvaluatorContext {
|
||||||
|
templateRenderer: TemplateRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The condition union member(s) carrying a given discriminator literal.
|
||||||
|
export type ConditionOfType<T extends string> = Extract<
|
||||||
|
AdvancedCameraCardCondition,
|
||||||
|
{ condition?: T }
|
||||||
|
>;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { isBeingCasted } from '../../utils/casting';
|
||||||
|
import { isCompanionApp } from '../../utils/companion';
|
||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class UserAgentConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'user_agent'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'user_agent'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
const condition = this._condition;
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.userAgent &&
|
||||||
|
(!condition.user_agent || condition.user_agent === newState.userAgent) &&
|
||||||
|
(condition.casting === undefined ||
|
||||||
|
condition.casting === isBeingCasted(newState.userAgent)) &&
|
||||||
|
(condition.companion === undefined ||
|
||||||
|
condition.companion === isCompanionApp(newState.userAgent)) &&
|
||||||
|
(condition.user_agent_re === undefined ||
|
||||||
|
new RegExp(condition.user_agent_re).test(newState.userAgent)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class UserConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'user'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'user'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
!!newState?.hass?.user && this._condition.users.includes(newState.hass.user.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||||
|
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||||
|
|
||||||
|
export class ViewConditionEvaluator implements ConditionEvaluator {
|
||||||
|
private _condition: ConditionOfType<'view'>;
|
||||||
|
|
||||||
|
constructor(condition: ConditionOfType<'view'>) {
|
||||||
|
this._condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evaluate(
|
||||||
|
newState?: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
): ConditionsEvaluationResult {
|
||||||
|
const oldView = oldState?.view;
|
||||||
|
const newView = newState?.view;
|
||||||
|
|
||||||
|
return {
|
||||||
|
result:
|
||||||
|
(!!newView && this._condition.views?.includes(newView)) ||
|
||||||
|
(newView !== oldView && !this._condition.views?.length),
|
||||||
|
...(oldView !== newView && {
|
||||||
|
triggerData: {
|
||||||
|
...((oldState?.view || newState?.view) && {
|
||||||
|
view: {
|
||||||
|
...(oldState?.view && { from: oldState.view }),
|
||||||
|
...(newState?.view && { to: newState.view }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { AdvancedCameraCardCondition } from '../config/schema/conditions/types';
|
||||||
|
import { AndConditionEvaluator } from './conditions/and';
|
||||||
|
import { CallConditionEvaluator } from './conditions/call';
|
||||||
|
import { CameraConditionEvaluator } from './conditions/camera';
|
||||||
|
import { ConfigConditionEvaluator } from './conditions/config';
|
||||||
|
import { DisplayModeConditionEvaluator } from './conditions/display-mode';
|
||||||
|
import { ExpandConditionEvaluator } from './conditions/expand';
|
||||||
|
import { FullscreenConditionEvaluator } from './conditions/fullscreen';
|
||||||
|
import { InitializedConditionEvaluator } from './conditions/initialized';
|
||||||
|
import { InteractionConditionEvaluator } from './conditions/interaction';
|
||||||
|
import { KeyConditionEvaluator } from './conditions/key';
|
||||||
|
import { MediaLoadedConditionEvaluator } from './conditions/media-loaded';
|
||||||
|
import { MicrophoneConditionEvaluator } from './conditions/microphone';
|
||||||
|
import { NotConditionEvaluator } from './conditions/not';
|
||||||
|
import { NumericStateConditionEvaluator } from './conditions/numeric-state';
|
||||||
|
import { OrConditionEvaluator } from './conditions/or';
|
||||||
|
import { ScreenConditionEvaluator } from './conditions/screen';
|
||||||
|
import { StateConditionEvaluator } from './conditions/state';
|
||||||
|
import { TemplateConditionEvaluator } from './conditions/template';
|
||||||
|
import { TriggeredConditionEvaluator } from './conditions/triggered';
|
||||||
|
import { ConditionEvaluator, EvaluatorContext } from './conditions/types';
|
||||||
|
import { UserConditionEvaluator } from './conditions/user';
|
||||||
|
import { UserAgentConditionEvaluator } from './conditions/user-agent';
|
||||||
|
import { ViewConditionEvaluator } from './conditions/view';
|
||||||
|
|
||||||
|
export const createConditionEvaluator = (
|
||||||
|
condition: AdvancedCameraCardCondition,
|
||||||
|
context: EvaluatorContext,
|
||||||
|
): ConditionEvaluator => {
|
||||||
|
switch (condition.condition) {
|
||||||
|
case undefined:
|
||||||
|
case 'state':
|
||||||
|
return new StateConditionEvaluator(condition);
|
||||||
|
case 'view':
|
||||||
|
return new ViewConditionEvaluator(condition);
|
||||||
|
case 'fullscreen':
|
||||||
|
return new FullscreenConditionEvaluator(condition);
|
||||||
|
case 'expand':
|
||||||
|
return new ExpandConditionEvaluator(condition);
|
||||||
|
case 'camera':
|
||||||
|
return new CameraConditionEvaluator(condition);
|
||||||
|
case 'numeric_state':
|
||||||
|
return new NumericStateConditionEvaluator(condition);
|
||||||
|
case 'user':
|
||||||
|
return new UserConditionEvaluator(condition);
|
||||||
|
case 'media_loaded':
|
||||||
|
return new MediaLoadedConditionEvaluator(condition);
|
||||||
|
case 'screen':
|
||||||
|
return new ScreenConditionEvaluator(condition);
|
||||||
|
case 'display_mode':
|
||||||
|
return new DisplayModeConditionEvaluator(condition);
|
||||||
|
case 'triggered':
|
||||||
|
return new TriggeredConditionEvaluator(condition);
|
||||||
|
case 'interaction':
|
||||||
|
return new InteractionConditionEvaluator(condition);
|
||||||
|
case 'microphone':
|
||||||
|
return new MicrophoneConditionEvaluator(condition);
|
||||||
|
case 'call':
|
||||||
|
return new CallConditionEvaluator(condition);
|
||||||
|
case 'key':
|
||||||
|
return new KeyConditionEvaluator(condition);
|
||||||
|
case 'user_agent':
|
||||||
|
return new UserAgentConditionEvaluator(condition);
|
||||||
|
case 'config':
|
||||||
|
return new ConfigConditionEvaluator(condition);
|
||||||
|
case 'initialized':
|
||||||
|
return new InitializedConditionEvaluator();
|
||||||
|
case 'template':
|
||||||
|
return new TemplateConditionEvaluator(condition, context);
|
||||||
|
case 'or':
|
||||||
|
return new OrConditionEvaluator(
|
||||||
|
condition.conditions.map((child) => createConditionEvaluator(child, context)),
|
||||||
|
);
|
||||||
|
case 'and':
|
||||||
|
return new AndConditionEvaluator(
|
||||||
|
condition.conditions.map((child) => createConditionEvaluator(child, context)),
|
||||||
|
);
|
||||||
|
case 'not':
|
||||||
|
return new NotConditionEvaluator(
|
||||||
|
condition.conditions.map((child) => createConditionEvaluator(child, context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('and condition', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate a simple and condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'and' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
{ condition: 'expand' as const, expand: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: true }).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: true, expand: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: false, expand: true }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report combined trigger data for an and condition', () => {
|
||||||
|
// Not a terribly realistic example, but chosen so that trigger data for
|
||||||
|
// both camera and view should be returned.
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'and' as const,
|
||||||
|
conditions: [{ condition: 'camera' as const }, { condition: 'view' as const }],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ camera: 'camera-1' }, {}).result).toBeFalsy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate(
|
||||||
|
{ camera: 'camera-2', view: 'clip' },
|
||||||
|
{ camera: 'camera-1', view: 'live' },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
camera: { from: 'camera-1', to: 'camera-2' },
|
||||||
|
view: { from: 'live', to: 'clip' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should forward subscribe and destroy to its children', () => {
|
||||||
|
const addEventListener = vi.fn();
|
||||||
|
const removeEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
removeEventListener: removeEventListener,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
// The `screen` child supports subscribe/destroy; the `fullscreen` child does
|
||||||
|
// not — forwarding must handle both.
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'and' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
evaluator.subscribe?.(onChange);
|
||||||
|
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
addEventListener.mock.calls[0][1]();
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
|
||||||
|
evaluator.destroy?.();
|
||||||
|
expect(removeEventListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('call condition', () => {
|
||||||
|
it('should default to call true in its bare form', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'call' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ call: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ call: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match when call is true', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'call' as const, call: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ call: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ call: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match when call is false', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'call' as const, call: false },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// With no state.call published, the bare condition matches `false`,
|
||||||
|
// so `call: false` is satisfied.
|
||||||
|
expect(evaluator.evaluate({}).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ call: true }).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ call: false }).result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('camera condition', () => {
|
||||||
|
it('should match a named camera', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'camera' as const, cameras: ['bar'] },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ camera: 'bar' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ camera: 'will-not-match' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report trigger data for any camera change', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'camera' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ camera: 'bar' }, {})).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
camera: {
|
||||||
|
to: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ camera: 'foo' }, { camera: 'bar' })).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
camera: {
|
||||||
|
from: 'bar',
|
||||||
|
to: 'foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createConfig } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('config condition', () => {
|
||||||
|
const config_1 = createConfig({
|
||||||
|
// Default is:
|
||||||
|
//
|
||||||
|
// view: {
|
||||||
|
// default: live,
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
const config_2 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const config_3 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
default_cycle_camera: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const config_4 = createConfig({
|
||||||
|
view: {
|
||||||
|
default: 'clips',
|
||||||
|
default_cycle_camera: true,
|
||||||
|
dim: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report trigger data for any config change', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'config' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ config: config_1 }, {})).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
config: {
|
||||||
|
to: config_1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ config: config_2 }, { config: config_1 })).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
config: {
|
||||||
|
from: config_1,
|
||||||
|
to: config_2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match when the config is unchanged', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'config' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_1 }, { config: config_1 }).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match a specific config change', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'config' as const, paths: ['view.default'] },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ config: config_1 }, {}).result).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_2 }, { config: config_1 }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_3 }, { config: config_2 }).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple specific config changes', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'config' as const,
|
||||||
|
paths: ['view.default', 'view.default_cycle_camera'],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ config: config_1 }, {}).result).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_2 }, { config: config_1 }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_3 }, { config: config_2 }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ config: config_4 }, { config: config_3 }).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('display mode condition', () => {
|
||||||
|
it('should match a display mode condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'display_mode' as const, display_mode: 'grid' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ displayMode: 'grid' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ displayMode: 'single' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('expand condition', () => {
|
||||||
|
it('should match an expand condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'expand' as const, expand: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ expand: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ expand: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('fullscreen condition', () => {
|
||||||
|
it('should match a fullscreen condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('initialized condition', () => {
|
||||||
|
it('should match an initialized condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'initialized' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ initialized: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ initialized: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('interaction condition', () => {
|
||||||
|
it('should match an interaction condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'interaction' as const, interaction: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ interaction: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ interaction: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('key condition', () => {
|
||||||
|
it('should match a simple keypress', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'key' as const, key: 'a' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'up', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match a keypress with modifiers', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'key' as const,
|
||||||
|
key: 'a',
|
||||||
|
state: 'down' as const,
|
||||||
|
ctrl: true,
|
||||||
|
shift: true,
|
||||||
|
alt: true,
|
||||||
|
meta: true,
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: false, shift: false, alt: false, meta: false },
|
||||||
|
},
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: false },
|
||||||
|
},
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
keys: {
|
||||||
|
a: { state: 'down', ctrl: true, shift: true, alt: true, meta: true },
|
||||||
|
},
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createMediaLoadedInfo } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('media loaded condition', () => {
|
||||||
|
it('should match a media loaded condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'media_loaded' as const, media_loaded: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ mediaLoadedInfo: createMediaLoadedInfo() }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ mediaLoadedInfo: null }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { MicrophoneState } from '../../../src/card-controller/types';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('microphone condition', () => {
|
||||||
|
const createMicrophoneState = (state: Partial<MicrophoneState>): MicrophoneState => {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
muted: false,
|
||||||
|
forbidden: false,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should match when muted is true', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'microphone' as const, muted: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ microphone: createMicrophoneState({ muted: true }) }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ microphone: createMicrophoneState({ muted: false }) }).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match when muted is false', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'microphone' as const, muted: false },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ microphone: createMicrophoneState({ muted: true }) }).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ microphone: createMicrophoneState({ muted: false }) }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('not condition', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate a not condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'not' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
{ condition: 'expand' as const, expand: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Neither sub-condition is true, so `not` passes — and reports no trigger
|
||||||
|
// data (nothing is "triggering").
|
||||||
|
expect(evaluator.evaluate({})).toEqual({ result: true });
|
||||||
|
|
||||||
|
// Any sub-condition being true means `not` fails.
|
||||||
|
expect(evaluator.evaluate({ fullscreen: true }).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ expand: true }).result).toBeFalsy();
|
||||||
|
|
||||||
|
// Both sub-conditions false again — `not` passes.
|
||||||
|
expect(evaluator.evaluate({ fullscreen: false, expand: false }).result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should forward subscribe and destroy to its children', () => {
|
||||||
|
const addEventListener = vi.fn();
|
||||||
|
const removeEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
removeEventListener: removeEventListener,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
// The `screen` child supports subscribe/destroy; the `fullscreen` child does
|
||||||
|
// not — forwarding must handle both.
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'not' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
evaluator.subscribe?.(onChange);
|
||||||
|
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
addEventListener.mock.calls[0][1]();
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
|
||||||
|
evaluator.destroy?.();
|
||||||
|
expect(removeEventListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createHASS, createStateEntity } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('numeric state condition', () => {
|
||||||
|
it('should match above a threshold', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'numeric_state' as const, entity: 'sensor.foo', above: 10 },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '11' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: '9' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match below a threshold', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'numeric_state' as const, entity: 'sensor.foo', below: 10 },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '11' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'sensor.foo': createStateEntity({ state: '9' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('or condition', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate a simple or condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'or' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
{ condition: 'expand' as const, expand: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ expand: true }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ fullscreen: false, expand: false }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report trigger data for the first matching sub-condition', () => {
|
||||||
|
// Not a terribly realistic example, but chosen so that trigger data for
|
||||||
|
// both camera and view could be returned.
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'or' as const,
|
||||||
|
conditions: [{ condition: 'camera' as const }, { condition: 'view' as const }],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ camera: 'camera-1' }, {})).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: { camera: { to: 'camera-1' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ view: 'live' }, {})).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: { view: { to: 'live' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// When both change, the camera sub-condition matches first, so only its
|
||||||
|
// trigger data is returned.
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ camera: 'camera-2', view: 'clip' }, { camera: 'camera-1' }),
|
||||||
|
).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: { camera: { from: 'camera-1', to: 'camera-2' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should forward subscribe and destroy to its children', () => {
|
||||||
|
const addEventListener = vi.fn();
|
||||||
|
const removeEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
removeEventListener: removeEventListener,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
// The `screen` child supports subscribe/destroy; the `fullscreen` child does
|
||||||
|
// not — forwarding must handle both.
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'or' as const,
|
||||||
|
conditions: [
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
{ condition: 'fullscreen' as const, fullscreen: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
evaluator.subscribe?.(onChange);
|
||||||
|
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
addEventListener.mock.calls[0][1]();
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
|
||||||
|
evaluator.destroy?.();
|
||||||
|
expect(removeEventListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('screen condition', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate the media query', () => {
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
matches: true,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
expect(evaluator.evaluate().result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register and invoke a media-query listener on subscribe', () => {
|
||||||
|
const addEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
addEventListener: addEventListener,
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
evaluator.subscribe?.(onChange);
|
||||||
|
|
||||||
|
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
|
||||||
|
// Invoke the registered handler and confirm it triggers the callback.
|
||||||
|
addEventListener.mock.calls[0][1]();
|
||||||
|
expect(onChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the media-query listener on destroy', () => {
|
||||||
|
const removeEventListener = vi.fn();
|
||||||
|
vi.spyOn(window, 'matchMedia').mockReturnValue({
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: removeEventListener,
|
||||||
|
} as unknown as MediaQueryList);
|
||||||
|
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'screen' as const, media_query: 'whatever' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
evaluator.subscribe?.(vi.fn());
|
||||||
|
evaluator.destroy?.();
|
||||||
|
expect(removeEventListener).toHaveBeenCalledWith('change', expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createHASS, createStateEntity } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('state condition', () => {
|
||||||
|
it('should report trigger data for any change when neither state nor state_not is set', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'state' as const, entity: 'binary_sensor.foo' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate(
|
||||||
|
{
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
state: {
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
to: 'on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate(
|
||||||
|
{
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'on' }) }),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
state: {
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
from: 'on',
|
||||||
|
to: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match a single positive state', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'state' as const, entity: 'binary_sensor.foo', state: 'on' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple positive states', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state: ['active', 'on'],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({
|
||||||
|
'binary_sensor.foo': createStateEntity({ state: 'active' }),
|
||||||
|
}),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match a single negative state', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'state' as const, entity: 'binary_sensor.foo', state_not: 'on' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple negative states', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'state' as const,
|
||||||
|
entity: 'binary_sensor.foo',
|
||||||
|
state_not: ['active', 'on'],
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({
|
||||||
|
'binary_sensor.foo': createStateEntity({ state: 'active' }),
|
||||||
|
}),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match an implicit state condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ entity: 'binary_sensor.foo', state: 'on' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity() }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'binary_sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createHASS, createStateEntity } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('template condition', () => {
|
||||||
|
it('should evaluate true when template evalutes to true', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'template' as const,
|
||||||
|
value_template: '{{ is_state("sensor.foo", "on") }}',
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'sensor.foo': createStateEntity({ state: 'on' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'sensor.foo': createStateEntity({ state: 'off' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should evaluate false when template evalutes to non-boolean', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'template' as const,
|
||||||
|
// This does not result in a boolean.
|
||||||
|
value_template: '{{ hass.states["light.office"].state }}',
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({
|
||||||
|
hass: createHASS({ 'light.office': createStateEntity({ state: 'on' }) }),
|
||||||
|
}).result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
||||||
|
import { EvaluatorContext } from '../../../src/conditions/conditions/types';
|
||||||
|
|
||||||
|
export const createEvaluatorContext = (): EvaluatorContext => ({
|
||||||
|
templateRenderer: new TemplateRenderer(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('triggered condition', () => {
|
||||||
|
it('should match a triggered condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'triggered' as const, triggered: ['camera_1', 'camera_2'] },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ triggered: new Set(['camera_1']) }).result).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ triggered: new Set(['camera_2', 'camera_1', 'camera_3']) })
|
||||||
|
.result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ triggered: new Set(['camera_3']) }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('user agent condition', () => {
|
||||||
|
const userAgent =
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
|
it('should match exact user agent', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'user_agent' as const, user_agent: userAgent },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: userAgent }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'Something else' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match user agent regex', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'user_agent' as const, user_agent_re: 'Chrome/' },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: userAgent }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'Something else' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match casting', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'user_agent' as const, casting: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'CrKey/1.0' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: userAgent }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match companion app', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'user_agent' as const, companion: true },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'Home Assistant/' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: userAgent }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match multiple parameters', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{
|
||||||
|
condition: 'user_agent' as const,
|
||||||
|
companion: true,
|
||||||
|
user_agent: 'Home Assistant/',
|
||||||
|
user_agent_re: 'Home.Assistant',
|
||||||
|
},
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'Home Assistant/' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ userAgent: 'Something else' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createHASS, createUser } from '../../test-utils';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('user condition', () => {
|
||||||
|
it('should match a user condition', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'user' as const, users: ['user_1', 'user_2'] },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ hass: createHASS({}, createUser({ id: 'user_1' })) }).result,
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
evaluator.evaluate({ hass: createHASS({}, createUser({ id: 'user_WRONG' })) })
|
||||||
|
.result,
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createConditionEvaluator } from '../../../src/conditions/factory';
|
||||||
|
import { createEvaluatorContext } from './test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('view condition', () => {
|
||||||
|
it('should match a named view', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'view' as const, views: ['live'] },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({}).result).toBeFalsy();
|
||||||
|
expect(evaluator.evaluate({ view: 'live' }).result).toBeTruthy();
|
||||||
|
expect(evaluator.evaluate({ view: 'clips' }).result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report trigger data for any view change', () => {
|
||||||
|
const evaluator = createConditionEvaluator(
|
||||||
|
{ condition: 'view' as const },
|
||||||
|
createEvaluatorContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ view: 'clips' }, {})).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
view: {
|
||||||
|
to: 'clips',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(evaluator.evaluate({ view: 'timeline' }, { view: 'clips' })).toEqual({
|
||||||
|
result: true,
|
||||||
|
triggerData: {
|
||||||
|
view: {
|
||||||
|
from: 'clips',
|
||||||
|
to: 'timeline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user