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 817f3d7df..031cf2a8c 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 @@ -112,6 +112,26 @@ describe('testConditions()', () => { const result = testConditions([{ field: 'eventNow.title', operator: 'equals', value: '' }], 'all', mockStore); expect(result).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); + }); + + 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); + }); }); describe('not_equals operator', () => { diff --git a/apps/server/src/api-data/automation/automation.service.ts b/apps/server/src/api-data/automation/automation.service.ts index 1aa6ab349..c5fbffec2 100644 --- a/apps/server/src/api-data/automation/automation.service.ts +++ b/apps/server/src/api-data/automation/automation.service.ts @@ -14,7 +14,7 @@ import { isOntimeCloud } from '../../externals.js'; import { emitOSC } from './clients/osc.client.js'; import { emitHTTP } from './clients/http.client.js'; import { getAutomationsEnabled, getAutomations, getAutomationTriggers } from './automation.dao.js'; -import { isGreaterThan, isLessThan } from './automation.utils.js'; +import { isBooleanEquals, isGreaterThan, isLessThan } from './automation.utils.js'; /** * Exposes a method for triggering actions based on a TimerLifeCycle event @@ -74,12 +74,18 @@ export function testConditions( function evaluateCondition(filter: AutomationFilter): boolean { const { field, operator, value } = filter; + const lowerCasedValue = value.toLowerCase(); const fieldValue = getPropertyFromPath(field, state); // if value is empty string, the user could be meaning to check if the value does not exist // we use loose equality to be able to check for converted values (eg '10' == 10) switch (operator) { case 'equals': + // handle the case where we are comparing boolean strings + + if (typeof fieldValue === 'boolean') { + return isBooleanEquals(fieldValue, lowerCasedValue); + } // overload the edge case where we use empty string to check if a value does not exist if (value === '' && fieldValue === undefined) { return true; @@ -88,14 +94,8 @@ export function testConditions( case 'not_equals': return fieldValue != value; case 'greater_than': - if (typeof fieldValue !== 'number') { - return false; - } return isGreaterThan(fieldValue, value); case 'less_than': - if (typeof fieldValue !== 'number') { - return false; - } return isLessThan(fieldValue, value); case 'contains': return typeof fieldValue === 'string' && fieldValue.includes(value); diff --git a/apps/server/src/api-data/automation/automation.utils.ts b/apps/server/src/api-data/automation/automation.utils.ts index 72eb0539d..b3929d584 100644 --- a/apps/server/src/api-data/automation/automation.utils.ts +++ b/apps/server/src/api-data/automation/automation.utils.ts @@ -79,7 +79,8 @@ export function parseTemplateNested(template: string, state: object, humanReadab value = undefined; } } else { - value = getPropertyFromPath(variableName, state); + // we cast to string since this will be used in a string context + value = getPropertyFromPath(variableName, state) as string; } if (value !== undefined) { parsedTemplate = parsedTemplate.replace(match[0], value); @@ -140,17 +141,18 @@ const quickAliases: AliasesDefinition = { * @example isGreaterThan('5', '10') // false * @example isGreaterThan('Ontime', 'Cool') // false */ -export function isGreaterThan(a: string, b: string): boolean { - const aValue = Number(a); - const bValue = Number(b); - - // we check if the values encore numbers and compare them - if (!isNaN(aValue) && !isNaN(bValue)) { - return aValue > bValue; +export function isGreaterThan(a: unknown, b: string): boolean { + // If either value is not a number, there is no logical comparison to be made + if (typeof a !== 'number') { + return false; } - // If either value is not a number, there is no logical comparison to be made - return false; + const bValue = Number(b); + if (isNaN(bValue)) { + return false; + } + + return a > bValue; } /** @@ -159,15 +161,33 @@ export function isGreaterThan(a: string, b: string): boolean { * @example isLessThan('5', '10') // true * @example isLessThan('Ontime', 'Cool') // false */ -export function isLessThan(a: string, b: string): boolean { - const aValue = Number(a); - const bValue = Number(b); - - // we check if the values encore numbers and compare them - if (!isNaN(aValue) && !isNaN(bValue)) { - return aValue < bValue; +export function isLessThan(a: unknown, b: string): boolean { + // If either value is not a number, there is no logical comparison to be made + if (typeof a !== 'number') { + return false; } - // If either value is not a number, there is no logical comparison to be made + const bValue = Number(b); + if (isNaN(bValue)) { + return false; + } + + return a < bValue; +} + +/** + * Utility encapsulates logic for comparing two strings which may encode booleans + * @example isBooleanEquals(true, 'true') // true + * @example isBooleanEquals(true, 'false') // false + * @example isBooleanEquals(true, 'something') // false + */ +export function isBooleanEquals(a: boolean, b: string): boolean { + if (b === 'true') { + return a === true; + } + + if (b === 'false') { + return a === false; + } return false; }