refactor: Restructure condition evaluation into per-type evaluator classes (#2518)
This commit is contained in:
committed by
dermotduffy
parent
3480a55a87
commit
12ae3b216c
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