From 206135e6ad9448bac8ab1a77bdcdec45a7498f50 Mon Sep 17 00:00:00 2001 From: Philippe Allard-Rousse Date: Fri, 2 Jan 2026 17:20:27 -0500 Subject: [PATCH 1/5] Fix(automation): Handles undefined values in conditions Addresses an issue where conditions with the 'not_equals' operator incorrectly evaluated undefined values. This change ensures that empty string comparisons correctly identify missing values in automation rules. --- .../automation/__tests__/automation.service.test.ts | 6 ++++++ apps/server/src/api-data/automation/automation.service.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts index e0ea647094..2c7f90054f 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts @@ -150,6 +150,12 @@ describe('testConditions()', () => { const result = testConditions([{ field: 'clock', operator: 'not_equals', value: '11' }], 'all', mockStore); expect(result).toBe(true); }); + it('should check if a value does exist', () => { + const mockStore = makeRuntimeStateData({ eventNow: null }); + expect(testConditions([{ field: 'eventNow.title', operator: 'not_equals', value: '' }], 'all', mockStore)).toBe( + false, + ); + }); }); describe('greater_than operator', () => { diff --git a/apps/server/src/api-data/automation/automation.service.ts b/apps/server/src/api-data/automation/automation.service.ts index 87b0aec6dd..abc51a9477 100644 --- a/apps/server/src/api-data/automation/automation.service.ts +++ b/apps/server/src/api-data/automation/automation.service.ts @@ -106,6 +106,11 @@ export function testConditions( } return fieldValue == value; case 'not_equals': + // overload the edge case where we use empty string to check if a value does not exist + if (value === '' && fieldValue === undefined) { + return false; + } + return fieldValue != value; case 'greater_than': return isGreaterThan(fieldValue, value); From 6100ac97af0fc78746de6c2b07e0e069a4363ad2 Mon Sep 17 00:00:00 2001 From: Philippe Allard-Rousse Date: Sat, 3 Jan 2026 13:12:13 -0500 Subject: [PATCH 2/5] Fixes automation "not equals" logic Simplifies the 'not_equals' condition evaluation in automations by reusing the 'equals' condition, improving code readability and consistency. Fixes #1932 --- apps/server/src/api-data/automation/automation.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/server/src/api-data/automation/automation.service.ts b/apps/server/src/api-data/automation/automation.service.ts index abc51a9477..07ab781c7e 100644 --- a/apps/server/src/api-data/automation/automation.service.ts +++ b/apps/server/src/api-data/automation/automation.service.ts @@ -106,12 +106,8 @@ export function testConditions( } return fieldValue == value; case 'not_equals': - // overload the edge case where we use empty string to check if a value does not exist - if (value === '' && fieldValue === undefined) { - return false; - } + return !evaluateCondition({ field, operator: 'equals', value }); - return fieldValue != value; case 'greater_than': return isGreaterThan(fieldValue, value); case 'less_than': From 917f5deaa09b4d1d0aa45f7e6831d1a8ef7be4f3 Mon Sep 17 00:00:00 2001 From: Philippe Allard-Rousse Date: Sat, 3 Jan 2026 20:41:02 -0500 Subject: [PATCH 3/5] Test(automation): Expanding filter condition testing Expanding test cases to cover various data types and edge case for each operators. Unexpected behavior have been mark with a TO_DO. --- .../__tests__/automation.service.test.ts | 240 ++++++++++++------ .../automation/__tests__/filterTestUtils.ts | 60 +++++ 2 files changed, 223 insertions(+), 77 deletions(-) create mode 100644 apps/server/src/api-data/automation/__tests__/filterTestUtils.ts diff --git a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts index 2c7f90054f..0c9b645741 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts @@ -9,6 +9,7 @@ import * as oscClient from '../clients/osc.client.js'; import * as httpClient from '../clients/http.client.js'; import { makeOSCAction, makeHTTPAction } from './testUtils.js'; +import { runTestCondition } from './filterTestUtils.js'; beforeAll(() => { vi.mock('../../../classes/data-provider/DataProvider.js', () => { @@ -101,122 +102,207 @@ describe('testConditions()', () => { }); describe('equals operator', () => { - it('should compare two equal values', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'equals', value: '10' }], 'all', mockStore); - expect(result).toBe(true); - }); - - it('string comparisons should be case insensitive', () => { - const mockStore = makeRuntimeStateData({ eventNow: { title: 'Title' } as PlayableEvent }); - const result = testConditions( - [{ field: 'eventNow.title', operator: 'equals', value: 'title' }], - 'all', - mockStore, - ); - expect(result).toBe(true); + it('should be true when comparing two equal number/string', () => { + expect(runTestCondition('number', 10, 'equals', '10')).toBe(true); + expect(runTestCondition('string', 'Title', 'equals', 'Title')).toBe(true); }); it('should check if a value does not exist', () => { - const mockStore = makeRuntimeStateData({ eventNow: null }); - const result = testConditions([{ field: 'eventNow.title', operator: 'equals', value: '' }], 'all', mockStore); - expect(result).toBe(true); + expect(runTestCondition('null/undefined', null, 'equals', '')).toBe(true); }); it('should handle trueness boolean comparisons', () => { - const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }); - expect( - testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore), - ).toBe(true); - expect( - testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore), - ).toBe(false); + expect(runTestCondition('boolean', true, 'equals', 'true')).toBe(true); + expect(runTestCondition('boolean', true, 'equals', 'false')).toBe(false); }); it('should handle falseness boolean comparisons', () => { - const mockStore = makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }); - expect( - testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'false' }], 'all', mockStore), - ).toBe(true); - expect( - testConditions([{ field: 'eventNow.countToEnd', operator: 'equals', value: 'true' }], 'all', mockStore), - ).toBe(false); + expect(runTestCondition('boolean', false, 'equals', 'false')).toBe(true); + expect(runTestCondition('boolean', false, 'equals', 'true')).toBe(false); + }); + + it('should be case insensitive', () => { + expect(runTestCondition('string', 'Title', 'equals', 'title')).toBe(true); + expect(runTestCondition('boolean', true, 'equals', 'TRUE')).toBe(true); + }); + + it('should be false in other cases', () => { + // Mismatched types + expect(runTestCondition('number', 10, 'equals', 'lighting')).toBe(false); + // Greater than / less than number + expect(runTestCondition('number', 10, 'equals', '100')).toBe(false); + expect(runTestCondition('number', 10, 'equals', '11')).toBe(false); + expect(runTestCondition('number', 10, 'equals', '5')).toBe(false); + expect(runTestCondition('number', 10, 'equals', '1')).toBe(false); + // String / Substring + expect(runTestCondition('string', 'testing-lighting-10', 'equals', 'lighting')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'equals', 'sound')).toBe(false); + // Null / Empty / No value + expect(runTestCondition('null/undefined', null, 'equals', 'not-empty')).toBe(false); + expect(runTestCondition('boolean', false, 'equals', '')).toBe(false); + + expect(runTestCondition('number', 0, 'equals', '')).toBe(true); // TO_DO: is this the desired behavior? }); }); describe('not_equals operator', () => { - it('should check if two values are different', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'not_equals', value: '11' }], 'all', mockStore); - expect(result).toBe(true); + it('should be false when comparing two equal number/string', () => { + expect(runTestCondition('number', 10, 'not_equals', '10')).toBe(false); + expect(runTestCondition('string', 'Title', 'not_equals', 'Title')).toBe(false); }); - it('should check if a value does exist', () => { - const mockStore = makeRuntimeStateData({ eventNow: null }); - expect(testConditions([{ field: 'eventNow.title', operator: 'not_equals', value: '' }], 'all', mockStore)).toBe( - false, - ); + + it('should check if a value does not exist', () => { + expect(runTestCondition('null/undefined', null, 'not_equals', '')).toBe(false); + }); + + it('should handle trueness boolean comparisons', () => { + expect(runTestCondition('boolean', true, 'not_equals', 'true')).toBe(false); + expect(runTestCondition('boolean', true, 'not_equals', 'false')).toBe(true); + }); + + it('should handle falseness boolean comparisons', () => { + expect(runTestCondition('boolean', false, 'not_equals', 'false')).toBe(false); + expect(runTestCondition('boolean', false, 'not_equals', 'true')).toBe(true); + }); + + it('should be case insensitive', () => { + expect(runTestCondition('string', 'Title', 'not_equals', 'title')).toBe(false); + expect(runTestCondition('boolean', true, 'not_equals', 'TRUE')).toBe(false); + }); + + it('should be true in other cases', () => { + // Mismatched types + expect(runTestCondition('number', 10, 'not_equals', 'lighting')).toBe(true); + // Greater than / less than number + expect(runTestCondition('number', 10, 'not_equals', '100')).toBe(true); + expect(runTestCondition('number', 10, 'not_equals', '11')).toBe(true); + expect(runTestCondition('number', 10, 'not_equals', '5')).toBe(true); + expect(runTestCondition('number', 10, 'not_equals', '1')).toBe(true); + // String / Substring + expect(runTestCondition('string', 'testing-lighting-10', 'not_equals', 'lighting')).toBe(true); + expect(runTestCondition('string', 'testing-lighting-10', 'not_equals', 'sound')).toBe(true); + // Null / Empty / No value + expect(runTestCondition('null/undefined', null, 'not_equals', 'not-empty')).toBe(true); + expect(runTestCondition('boolean', false, 'not_equals', '')).toBe(true); + + expect(runTestCondition('number', 0, 'not_equals', '')).toBe(false); // TO_DO: is this the desired behavior? }); }); describe('greater_than operator', () => { it('should check if the given value is smaller', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'greater_than', value: '9' }], 'all', mockStore); - expect(result).toBe(true); + expect(runTestCondition('number', 10, 'greater_than', '10')).toBe(false); + expect(runTestCondition('number', 10, 'greater_than', '100')).toBe(false); + expect(runTestCondition('number', 10, 'greater_than', '11')).toBe(false); + expect(runTestCondition('number', 10, 'greater_than', '5')).toBe(true); + expect(runTestCondition('number', 10, 'greater_than', '1')).toBe(true); }); it('should handle values which are not numbers', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'greater_than', value: 'testing' }], 'all', mockStore); - expect(result).toBe(false); + // Mismatched types / Empty / No value + expect(runTestCondition('number', 10, 'greater_than', 'lighting')).toBe(false); + expect(runTestCondition('number', 0, 'greater_than', '')).toBe(false); + // Other types + expect(runTestCondition('string', 'Title', 'greater_than', 'Title')).toBe(false); + expect(runTestCondition('null/undefined', null, 'greater_than', '')).toBe(false); + expect(runTestCondition('boolean', true, 'greater_than', 'true')).toBe(false); + expect(runTestCondition('boolean', true, 'greater_than', 'false')).toBe(false); + expect(runTestCondition('boolean', false, 'greater_than', 'false')).toBe(false); + expect(runTestCondition('boolean', false, 'greater_than', 'true')).toBe(false); + expect(runTestCondition('string', 'Title', 'greater_than', 'title')).toBe(false); + expect(runTestCondition('boolean', true, 'greater_than', 'TRUE')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'greater_than', 'lighting')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'greater_than', 'sound')).toBe(false); + expect(runTestCondition('null/undefined', null, 'greater_than', 'not-empty')).toBe(false); + expect(runTestCondition('boolean', false, 'greater_than', '')).toBe(false); }); }); describe('less_than operator', () => { it('should check if the given value is larger', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'less_than', value: '11' }], 'all', mockStore); - expect(result).toBe(true); + expect(runTestCondition('number', 10, 'less_than', '100')).toBe(true); + expect(runTestCondition('number', 10, 'less_than', '11')).toBe(true); + expect(runTestCondition('number', 10, 'less_than', '10')).toBe(false); + expect(runTestCondition('number', 10, 'less_than', '5')).toBe(false); + expect(runTestCondition('number', 10, 'less_than', '1')).toBe(false); }); it('should handle values which are not numbers', () => { - const mockStore = makeRuntimeStateData({ clock: 10 }); - const result = testConditions([{ field: 'clock', operator: 'less_than', value: 'testing' }], 'all', mockStore); - expect(result).toBe(false); + // Mismatched types / Empty / No value + expect(runTestCondition('number', 10, 'less_than', 'lighting')).toBe(false); + expect(runTestCondition('number', 0, 'less_than', '')).toBe(false); + // Other types + expect(runTestCondition('string', 'Title', 'less_than', 'Title')).toBe(false); + expect(runTestCondition('null/undefined', null, 'less_than', '')).toBe(false); + expect(runTestCondition('boolean', true, 'less_than', 'true')).toBe(false); + expect(runTestCondition('boolean', true, 'less_than', 'false')).toBe(false); + expect(runTestCondition('boolean', false, 'less_than', 'false')).toBe(false); + expect(runTestCondition('boolean', false, 'less_than', 'true')).toBe(false); + expect(runTestCondition('string', 'Title', 'less_than', 'title')).toBe(false); + expect(runTestCondition('boolean', true, 'less_than', 'TRUE')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'less_than', 'lighting')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'less_than', 'sound')).toBe(false); + expect(runTestCondition('null/undefined', null, 'less_than', 'not-empty')).toBe(false); + expect(runTestCondition('boolean', false, 'less_than', '')).toBe(false); }); }); describe('contains operator', () => { it('should check if value contains given string', () => { - const result = testConditions( - [{ field: 'eventNow.title', operator: 'contains', value: 'lighting' }], - 'all', - makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), - ); - expect(result).toBe(true); + expect(runTestCondition('string', 'testing-lighting-10', 'contains', 'lighting')).toBe(true); + expect(runTestCondition('string', 'testing-lighting-10', 'contains', '10')).toBe(true); + expect(runTestCondition('string', 'testing-lighting-10', 'contains', '1')).toBe(true); + expect(runTestCondition('string', 'testing-lighting-10', 'contains', 'sound')).toBe(false); + }); - const result2 = testConditions( - [{ field: 'eventNow.title', operator: 'contains', value: 'sound' }], - 'all', - makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), - ); - expect(result2).toBe(false); + it('should match with equals string', () => { + expect(runTestCondition('string', '', 'contains', '')).toBe(true); + expect(runTestCondition('string', 'Title', 'contains', 'Title')).toBe(true); + }); + + it('should handle case sensitivity', () => { + // TO_DO: is this the desired behavior? + expect(runTestCondition('string', 'Title', 'contains', 'title')).toBe(false); + }); + + it('should handle non-string equals values', () => { + //TO_DO: is this the desired behavior? All the fields contain the value when converted to string. + expect(runTestCondition('number', 10, 'contains', '10')).toBe(false); + expect(runTestCondition('boolean', true, 'contains', 'true')).toBe(false); + expect(runTestCondition('boolean', false, 'contains', 'false')).toBe(false); + }); + + it('should handle number values contained in field number', () => { + expect(runTestCondition('number', 12345, 'contains', '234')).toBe(false); // TO_DO: is this the desired behavior? + expect(runTestCondition('number', 12345, 'contains', '456')).toBe(false); }); }); describe('not_contains operator', () => { - it('should check if value does not contain given string', () => { - const result = testConditions( - [{ field: 'eventNow.title', operator: 'not_contains', value: 'lighting' }], - 'all', - makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), - ); - expect(result).toBe(false); + it("should check if value doesn't contains given string", () => { + expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', 'lighting')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', '10')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', '1')).toBe(false); + expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', 'sound')).toBe(true); + }); - const result2 = testConditions( - [{ field: 'eventNow.title', operator: 'not_contains', value: 'sound' }], - 'all', - makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), - ); - expect(result2).toBe(true); + it('should not match with equals string', () => { + expect(runTestCondition('string', '', 'not_contains', '')).toBe(false); + expect(runTestCondition('string', 'Title', 'not_contains', 'Title')).toBe(false); + }); + + it('should handle case sensitivity', () => { + // TO_DO: is this the desired behavior? + expect(runTestCondition('string', 'Title', 'not_contains', 'title')).toBe(true); + }); + + it('should handle non-string equals values', () => { + expect(runTestCondition('number', 10, 'not_contains', '10')).toBe(false); + expect(runTestCondition('boolean', true, 'not_contains', 'true')).toBe(false); + expect(runTestCondition('boolean', false, 'not_contains', 'false')).toBe(false); + }); + + it('should handle number values contained in field number', () => { + expect(runTestCondition('number', 12345, 'not_contains', '234')).toBe(false); + expect(runTestCondition('number', 12345, 'not_contains', '456')).toBe(false); // TO_DO: is this the desired behavior? }); }); diff --git a/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts b/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts new file mode 100644 index 0000000000..ad20252f69 --- /dev/null +++ b/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts @@ -0,0 +1,60 @@ +import { AutomationFilter, PlayableEvent } from 'ontime-types'; +import { RuntimeState } from '../../../stores/runtimeState.js'; +import { makeRuntimeStateData } from '../../../stores/__mocks__/runtimeState.mocks.js'; +import { testConditions } from '../automation.service.js'; + +type FieldCategories = 'number' | 'string' | 'boolean' | 'null/undefined'; +type FieldCategoriesTypeMap = { + number: number; + string: string; + boolean: boolean; + 'null/undefined': null; +}; + +export function runTestCondition( + stateType: T, + stateValue: FieldCategoriesTypeMap[T], + operator: AutomationFilter['operator'], + value: string, +) { + const { state, field } = getFilterState(stateType, stateValue); + const mockStore = makeRuntimeStateData(state); + const filter: AutomationFilter = { + field, + operator, + value, + }; + return testConditions([filter], 'all', mockStore); +} + +export function getFilterState( + stateType: T, + stateValue: FieldCategoriesTypeMap[T], +): { state: Partial; field: string } { + switch (stateType) { + case 'number': + return { + state: { clock: stateValue as number }, + field: 'clock', + }; + case 'string': + return { + state: { + eventNow: { + title: stateValue as string, + } as PlayableEvent, + }, + field: 'eventNow.title', + }; + case 'boolean': + return { + state: { eventNow: { countToEnd: stateValue as boolean } as PlayableEvent }, + field: 'eventNow.countToEnd', + }; + case 'null/undefined': + return { + state: { eventNow: null }, + field: 'eventNow.title', + }; + } +} From 11314a0daf8fde6960f40ab94d24fbcbb810c0fe Mon Sep 17 00:00:00 2001 From: Philippe Allard-Rousse Date: Sat, 3 Jan 2026 21:09:18 -0500 Subject: [PATCH 4/5] Fix(automation): Handles default filter case - Ensures that their is a default scenario. - Fixing Deepsource issue "No default cases in switch statements JS-0047" --- apps/server/src/api-data/automation/__tests__/filterTestUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts b/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts index ad20252f69..20f8b98796 100644 --- a/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts +++ b/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts @@ -52,6 +52,7 @@ export function getFilterState( field: 'eventNow.countToEnd', }; case 'null/undefined': + default: return { state: { eventNow: null }, field: 'eventNow.title', From 47629c2fca71b3bfe8b14d362b380cd34fa3f808 Mon Sep 17 00:00:00 2001 From: Philippe Allard-Rousse Date: Tue, 6 Jan 2026 09:22:40 -0500 Subject: [PATCH 5/5] Test(automation): Remove abstraction in test --- .../__tests__/automation.service.test.ts | 1700 ++++++++++++++++- .../automation/__tests__/filterTestUtils.ts | 61 - 2 files changed, 1599 insertions(+), 162 deletions(-) delete mode 100644 apps/server/src/api-data/automation/__tests__/filterTestUtils.ts diff --git a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts index 0c9b645741..bdb23cec43 100644 --- a/apps/server/src/api-data/automation/__tests__/automation.service.test.ts +++ b/apps/server/src/api-data/automation/__tests__/automation.service.test.ts @@ -9,7 +9,6 @@ import * as oscClient from '../clients/osc.client.js'; import * as httpClient from '../clients/http.client.js'; import { makeOSCAction, makeHTTPAction } from './testUtils.js'; -import { runTestCondition } from './filterTestUtils.js'; beforeAll(() => { vi.mock('../../../classes/data-provider/DataProvider.js', () => { @@ -103,206 +102,1705 @@ describe('testConditions()', () => { describe('equals operator', () => { it('should be true when comparing two equal number/string', () => { - expect(runTestCondition('number', 10, 'equals', '10')).toBe(true); - expect(runTestCondition('string', 'Title', 'equals', 'Title')).toBe(true); + // Test Conditions (State type: number) [10 'equals' '10'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['Title' 'equals' 'Title'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(true); }); it('should check if a value does not exist', () => { - expect(runTestCondition('null/undefined', null, 'equals', '')).toBe(true); + // Test Conditions (State type: 'null/undefined') [null 'equals' ''] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(true); }); it('should handle trueness boolean comparisons', () => { - expect(runTestCondition('boolean', true, 'equals', 'true')).toBe(true); - expect(runTestCondition('boolean', true, 'equals', 'false')).toBe(false); + // Test Conditions (State type: boolean) [true 'equals' 'true'] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(true); + + // Test Conditions (State type: boolean) [true 'equals' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); }); it('should handle falseness boolean comparisons', () => { - expect(runTestCondition('boolean', false, 'equals', 'false')).toBe(true); - expect(runTestCondition('boolean', false, 'equals', 'true')).toBe(false); + // Test Conditions (State type: boolean) [false 'equals' 'false'] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(true); + + // Test Conditions (State type: boolean) [false 'equals' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); }); it('should be case insensitive', () => { - expect(runTestCondition('string', 'Title', 'equals', 'title')).toBe(true); - expect(runTestCondition('boolean', true, 'equals', 'TRUE')).toBe(true); + // Test Conditions (State type: string) ['Title' 'equals' 'title'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: boolean) [true 'equals' 'TRUE'] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: 'TRUE', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(true); }); it('should be false in other cases', () => { // Mismatched types - expect(runTestCondition('number', 10, 'equals', 'lighting')).toBe(false); + + // Test Conditions (State type: number) [10 'equals' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); // Greater than / less than number - expect(runTestCondition('number', 10, 'equals', '100')).toBe(false); - expect(runTestCondition('number', 10, 'equals', '11')).toBe(false); - expect(runTestCondition('number', 10, 'equals', '5')).toBe(false); - expect(runTestCondition('number', 10, 'equals', '1')).toBe(false); + + // Test Conditions (State type: number) [10 'equals' '100'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '100', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'equals' '11'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '11', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'equals' '5'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '5', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'equals' '1'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); // String / Substring - expect(runTestCondition('string', 'testing-lighting-10', 'equals', 'lighting')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'equals', 'sound')).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'equals' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'equals' 'sound'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); // Null / Empty / No value - expect(runTestCondition('null/undefined', null, 'equals', 'not-empty')).toBe(false); - expect(runTestCondition('boolean', false, 'equals', '')).toBe(false); - expect(runTestCondition('number', 0, 'equals', '')).toBe(true); // TO_DO: is this the desired behavior? + // Test Conditions (State type: 'null/undefined') [null 'equals' 'not-empty'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'equals', + value: 'not-empty', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'equals' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [0 'equals' ''] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ clock: 0 }), + ), + ).toBe(true); // TO_DO: is this the desired behavior? }); }); describe('not_equals operator', () => { it('should be false when comparing two equal number/string', () => { - expect(runTestCondition('number', 10, 'not_equals', '10')).toBe(false); - expect(runTestCondition('string', 'Title', 'not_equals', 'Title')).toBe(false); + // Test Conditions (State type: number) [10 'not_equals' '10'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['Title' 'not_equals' 'Title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); }); it('should check if a value does not exist', () => { - expect(runTestCondition('null/undefined', null, 'not_equals', '')).toBe(false); + // Test Conditions (State type: 'null/undefined') [null 'not_equals' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); }); it('should handle trueness boolean comparisons', () => { - expect(runTestCondition('boolean', true, 'not_equals', 'true')).toBe(false); - expect(runTestCondition('boolean', true, 'not_equals', 'false')).toBe(true); + // Test Conditions (State type: boolean) [true 'not_equals' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'not_equals' 'false'] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(true); }); it('should handle falseness boolean comparisons', () => { - expect(runTestCondition('boolean', false, 'not_equals', 'false')).toBe(false); - expect(runTestCondition('boolean', false, 'not_equals', 'true')).toBe(true); + // Test Conditions (State type: boolean) [false 'not_equals' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'not_equals' 'true'] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(true); }); it('should be case insensitive', () => { - expect(runTestCondition('string', 'Title', 'not_equals', 'title')).toBe(false); - expect(runTestCondition('boolean', true, 'not_equals', 'TRUE')).toBe(false); + // Test Conditions (State type: string) ['Title' 'not_equals' 'title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'not_equals' 'TRUE'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: 'TRUE', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); }); it('should be true in other cases', () => { // Mismatched types - expect(runTestCondition('number', 10, 'not_equals', 'lighting')).toBe(true); + + // Test Conditions (State type: number) [10 'not_equals' 'lighting'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); // Greater than / less than number - expect(runTestCondition('number', 10, 'not_equals', '100')).toBe(true); - expect(runTestCondition('number', 10, 'not_equals', '11')).toBe(true); - expect(runTestCondition('number', 10, 'not_equals', '5')).toBe(true); - expect(runTestCondition('number', 10, 'not_equals', '1')).toBe(true); + + // Test Conditions (State type: number) [10 'not_equals' '100'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '100', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'not_equals' '11'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '11', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'not_equals' '5'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '5', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'not_equals' '1'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); // String / Substring - expect(runTestCondition('string', 'testing-lighting-10', 'not_equals', 'lighting')).toBe(true); - expect(runTestCondition('string', 'testing-lighting-10', 'not_equals', 'sound')).toBe(true); + + // Test Conditions (State type: string) ['testing-lighting-10' 'not_equals' 'lighting'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['testing-lighting-10' 'not_equals' 'sound'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); // Null / Empty / No value - expect(runTestCondition('null/undefined', null, 'not_equals', 'not-empty')).toBe(true); - expect(runTestCondition('boolean', false, 'not_equals', '')).toBe(true); - expect(runTestCondition('number', 0, 'not_equals', '')).toBe(false); // TO_DO: is this the desired behavior? + // Test Conditions (State type: 'null/undefined') [null 'not_equals' 'not-empty'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_equals', + value: 'not-empty', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(true); + + // Test Conditions (State type: boolean) [false 'not_equals' ''] = true + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [0 'not_equals' ''] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_equals', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ clock: 0 }), + ), + ).toBe(false); // TO_DO: is this the desired behavior? }); }); describe('greater_than operator', () => { it('should check if the given value is smaller', () => { - expect(runTestCondition('number', 10, 'greater_than', '10')).toBe(false); - expect(runTestCondition('number', 10, 'greater_than', '100')).toBe(false); - expect(runTestCondition('number', 10, 'greater_than', '11')).toBe(false); - expect(runTestCondition('number', 10, 'greater_than', '5')).toBe(true); - expect(runTestCondition('number', 10, 'greater_than', '1')).toBe(true); + // Test Conditions (State type: number) [10 'greater_than' '10'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'greater_than' '100'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '100', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'greater_than' '11'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '11', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'greater_than' '5'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '5', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'greater_than' '1'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); }); it('should handle values which are not numbers', () => { // Mismatched types / Empty / No value - expect(runTestCondition('number', 10, 'greater_than', 'lighting')).toBe(false); - expect(runTestCondition('number', 0, 'greater_than', '')).toBe(false); + + // Test Conditions (State type: number) [10 'greater_than' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [0 'greater_than' ''] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'greater_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ clock: 0 }), + ), + ).toBe(false); // Other types - expect(runTestCondition('string', 'Title', 'greater_than', 'Title')).toBe(false); - expect(runTestCondition('null/undefined', null, 'greater_than', '')).toBe(false); - expect(runTestCondition('boolean', true, 'greater_than', 'true')).toBe(false); - expect(runTestCondition('boolean', true, 'greater_than', 'false')).toBe(false); - expect(runTestCondition('boolean', false, 'greater_than', 'false')).toBe(false); - expect(runTestCondition('boolean', false, 'greater_than', 'true')).toBe(false); - expect(runTestCondition('string', 'Title', 'greater_than', 'title')).toBe(false); - expect(runTestCondition('boolean', true, 'greater_than', 'TRUE')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'greater_than', 'lighting')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'greater_than', 'sound')).toBe(false); - expect(runTestCondition('null/undefined', null, 'greater_than', 'not-empty')).toBe(false); - expect(runTestCondition('boolean', false, 'greater_than', '')).toBe(false); + + // Test Conditions (State type: string) ['Title' 'greater_than' 'Title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: 'null/undefined') [null 'greater_than' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'greater_than' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'greater_than' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'greater_than' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'greater_than' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['Title' 'greater_than' 'title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'greater_than' 'TRUE'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: 'TRUE', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'greater_than' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'greater_than' 'sound'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: 'null/undefined') [null 'greater_than' 'not-empty'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'greater_than', + value: 'not-empty', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'greater_than' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'greater_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); }); }); describe('less_than operator', () => { it('should check if the given value is larger', () => { - expect(runTestCondition('number', 10, 'less_than', '100')).toBe(true); - expect(runTestCondition('number', 10, 'less_than', '11')).toBe(true); - expect(runTestCondition('number', 10, 'less_than', '10')).toBe(false); - expect(runTestCondition('number', 10, 'less_than', '5')).toBe(false); - expect(runTestCondition('number', 10, 'less_than', '1')).toBe(false); + // Test Conditions (State type: number) [10 'less_than' '100'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '100', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'less_than' '11'] = true + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '11', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(true); + + // Test Conditions (State type: number) [10 'less_than' '10'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'less_than' '5'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '5', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [10 'less_than' '1'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); }); it('should handle values which are not numbers', () => { // Mismatched types / Empty / No value - expect(runTestCondition('number', 10, 'less_than', 'lighting')).toBe(false); - expect(runTestCondition('number', 0, 'less_than', '')).toBe(false); + + // Test Conditions (State type: number) [10 'less_than' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [0 'less_than' ''] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'less_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ clock: 0 }), + ), + ).toBe(false); // Other types - expect(runTestCondition('string', 'Title', 'less_than', 'Title')).toBe(false); - expect(runTestCondition('null/undefined', null, 'less_than', '')).toBe(false); - expect(runTestCondition('boolean', true, 'less_than', 'true')).toBe(false); - expect(runTestCondition('boolean', true, 'less_than', 'false')).toBe(false); - expect(runTestCondition('boolean', false, 'less_than', 'false')).toBe(false); - expect(runTestCondition('boolean', false, 'less_than', 'true')).toBe(false); - expect(runTestCondition('string', 'Title', 'less_than', 'title')).toBe(false); - expect(runTestCondition('boolean', true, 'less_than', 'TRUE')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'less_than', 'lighting')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'less_than', 'sound')).toBe(false); - expect(runTestCondition('null/undefined', null, 'less_than', 'not-empty')).toBe(false); - expect(runTestCondition('boolean', false, 'less_than', '')).toBe(false); + + // Test Conditions (State type: string) ['Title' 'less_than' 'Title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: 'null/undefined') [null 'less_than' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'less_than' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'less_than' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'less_than' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'less_than' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['Title' 'less_than' 'title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'less_than' 'TRUE'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: 'TRUE', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'less_than' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'less_than' 'sound'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: 'null/undefined') [null 'less_than' 'not-empty'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'less_than', + value: 'not-empty', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: null }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'less_than' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'less_than', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); }); }); describe('contains operator', () => { it('should check if value contains given string', () => { - expect(runTestCondition('string', 'testing-lighting-10', 'contains', 'lighting')).toBe(true); - expect(runTestCondition('string', 'testing-lighting-10', 'contains', '10')).toBe(true); - expect(runTestCondition('string', 'testing-lighting-10', 'contains', '1')).toBe(true); - expect(runTestCondition('string', 'testing-lighting-10', 'contains', 'sound')).toBe(false); + // Test Conditions (State type: string) ['testing-lighting-10' 'contains' 'lighting'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['testing-lighting-10' 'contains' '10'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['testing-lighting-10' 'contains' '1'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['testing-lighting-10' 'contains' 'sound'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); }); it('should match with equals string', () => { - expect(runTestCondition('string', '', 'contains', '')).toBe(true); - expect(runTestCondition('string', 'Title', 'contains', 'Title')).toBe(true); + // Test Conditions (State type: string) ['' 'contains' ''] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: '', + } as PlayableEvent, + }), + ), + ).toBe(true); + + // Test Conditions (State type: string) ['Title' 'contains' 'Title'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(true); }); it('should handle case sensitivity', () => { // TO_DO: is this the desired behavior? - expect(runTestCondition('string', 'Title', 'contains', 'title')).toBe(false); + + // Test Conditions (State type: string) ['Title' 'contains' 'title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'contains', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); }); it('should handle non-string equals values', () => { //TO_DO: is this the desired behavior? All the fields contain the value when converted to string. - expect(runTestCondition('number', 10, 'contains', '10')).toBe(false); - expect(runTestCondition('boolean', true, 'contains', 'true')).toBe(false); - expect(runTestCondition('boolean', false, 'contains', 'false')).toBe(false); + + // Test Conditions (State type: number) [10 'contains' '10'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'contains', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'contains' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'contains', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'contains' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'contains', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); }); it('should handle number values contained in field number', () => { - expect(runTestCondition('number', 12345, 'contains', '234')).toBe(false); // TO_DO: is this the desired behavior? - expect(runTestCondition('number', 12345, 'contains', '456')).toBe(false); + // Test Conditions (State type: number) [12345 'contains' '234'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'contains', + value: '234', + }, + ], + 'all', + makeRuntimeStateData({ clock: 12345 }), + ), + ).toBe(false); // TO_DO: is this the desired behavior? + + // Test Conditions (State type: number) [12345 'contains' '456'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'contains', + value: '456', + }, + ], + 'all', + makeRuntimeStateData({ clock: 12345 }), + ), + ).toBe(false); }); }); describe('not_contains operator', () => { it("should check if value doesn't contains given string", () => { - expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', 'lighting')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', '10')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', '1')).toBe(false); - expect(runTestCondition('string', 'testing-lighting-10', 'not_contains', 'sound')).toBe(true); + // Test Conditions (State type: string) ['testing-lighting-10' 'not_contains' 'lighting'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: 'lighting', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'not_contains' '10'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'not_contains' '1'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: '1', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['testing-lighting-10' 'not_contains' 'sound'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: 'sound', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'testing-lighting-10', + } as PlayableEvent, + }), + ), + ).toBe(true); }); it('should not match with equals string', () => { - expect(runTestCondition('string', '', 'not_contains', '')).toBe(false); - expect(runTestCondition('string', 'Title', 'not_contains', 'Title')).toBe(false); + // Test Conditions (State type: string) ['' 'not_contains' ''] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: '', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: '', + } as PlayableEvent, + }), + ), + ).toBe(false); + + // Test Conditions (State type: string) ['Title' 'not_contains' 'Title'] = false + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: 'Title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(false); }); it('should handle case sensitivity', () => { // TO_DO: is this the desired behavior? - expect(runTestCondition('string', 'Title', 'not_contains', 'title')).toBe(true); + + // Test Conditions (State type: string) ['Title' 'not_contains' 'title'] = true + expect( + testConditions( + [ + { + field: 'eventNow.title', + operator: 'not_contains', + value: 'title', + }, + ], + 'all', + makeRuntimeStateData({ + eventNow: { + title: 'Title', + } as PlayableEvent, + }), + ), + ).toBe(true); }); it('should handle non-string equals values', () => { - expect(runTestCondition('number', 10, 'not_contains', '10')).toBe(false); - expect(runTestCondition('boolean', true, 'not_contains', 'true')).toBe(false); - expect(runTestCondition('boolean', false, 'not_contains', 'false')).toBe(false); + // Test Conditions (State type: number) [10 'not_contains' '10'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_contains', + value: '10', + }, + ], + 'all', + makeRuntimeStateData({ clock: 10 }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [true 'not_contains' 'true'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_contains', + value: 'true', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: true } as PlayableEvent }), + ), + ).toBe(false); + + // Test Conditions (State type: boolean) [false 'not_contains' 'false'] = false + expect( + testConditions( + [ + { + field: 'eventNow.countToEnd', + operator: 'not_contains', + value: 'false', + }, + ], + 'all', + makeRuntimeStateData({ eventNow: { countToEnd: false } as PlayableEvent }), + ), + ).toBe(false); }); it('should handle number values contained in field number', () => { - expect(runTestCondition('number', 12345, 'not_contains', '234')).toBe(false); - expect(runTestCondition('number', 12345, 'not_contains', '456')).toBe(false); // TO_DO: is this the desired behavior? + // Test Conditions (State type: number) [12345 'not_contains' '234'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_contains', + value: '234', + }, + ], + 'all', + makeRuntimeStateData({ clock: 12345 }), + ), + ).toBe(false); + + // Test Conditions (State type: number) [12345 'not_contains' '456'] = false + expect( + testConditions( + [ + { + field: 'clock', + operator: 'not_contains', + value: '456', + }, + ], + 'all', + makeRuntimeStateData({ clock: 12345 }), + ), + ).toBe(false); // TO_DO: is this the desired behavior? }); }); diff --git a/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts b/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts deleted file mode 100644 index 20f8b98796..0000000000 --- a/apps/server/src/api-data/automation/__tests__/filterTestUtils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { AutomationFilter, PlayableEvent } from 'ontime-types'; -import { RuntimeState } from '../../../stores/runtimeState.js'; -import { makeRuntimeStateData } from '../../../stores/__mocks__/runtimeState.mocks.js'; -import { testConditions } from '../automation.service.js'; - -type FieldCategories = 'number' | 'string' | 'boolean' | 'null/undefined'; -type FieldCategoriesTypeMap = { - number: number; - string: string; - boolean: boolean; - 'null/undefined': null; -}; - -export function runTestCondition( - stateType: T, - stateValue: FieldCategoriesTypeMap[T], - operator: AutomationFilter['operator'], - value: string, -) { - const { state, field } = getFilterState(stateType, stateValue); - const mockStore = makeRuntimeStateData(state); - const filter: AutomationFilter = { - field, - operator, - value, - }; - return testConditions([filter], 'all', mockStore); -} - -export function getFilterState( - stateType: T, - stateValue: FieldCategoriesTypeMap[T], -): { state: Partial; field: string } { - switch (stateType) { - case 'number': - return { - state: { clock: stateValue as number }, - field: 'clock', - }; - case 'string': - return { - state: { - eventNow: { - title: stateValue as string, - } as PlayableEvent, - }, - field: 'eventNow.title', - }; - case 'boolean': - return { - state: { eventNow: { countToEnd: stateValue as boolean } as PlayableEvent }, - field: 'eventNow.countToEnd', - }; - case 'null/undefined': - default: - return { - state: { eventNow: null }, - field: 'eventNow.title', - }; - } -}