import { PlayableEvent, TimerLifeCycle } from 'ontime-types'; import { makeRuntimeStateData } from '../../../stores/__mocks__/runtimeState.mocks.js'; import { makeOntimeEvent } from '../../../services/rundown-service/__mocks__/rundown.mocks.js'; import { deleteAllTriggers, addTrigger, addAutomation } from '../automation.dao.js'; import { testConditions, triggerAutomations } from '../automation.service.js'; import * as oscClient from '../clients/osc.client.js'; import * as httpClient from '../clients/http.client.js'; import { makeOSCAction, makeHTTPAction } from './testUtils.js'; beforeAll(() => { vi.mock('../../../classes/data-provider/DataProvider.js', () => { // Create a small mock store let automations = { enabledAutomations: true, enabledOscIn: true, oscPortIn: 8888, triggers: [], automations: {}, }; return { getDataProvider: vi.fn().mockImplementation(() => { return { getAutomation: vi.fn().mockImplementation(() => automations), setAutomation: vi.fn().mockImplementation((newData) => (automations = newData)), }; }), }; }); }); afterAll(() => { vi.clearAllMocks(); }); describe('triggerAction()', () => { let oscSpy = vi.spyOn(oscClient, 'emitOSC'); let httpSpy = vi.spyOn(httpClient, 'emitHTTP'); beforeEach(async () => { oscSpy = vi.spyOn(oscClient, 'emitOSC').mockImplementation(() => {}); httpSpy = vi.spyOn(httpClient, 'emitHTTP').mockImplementation(() => {}); await deleteAllTriggers(); const oscAutomation = await addAutomation({ title: 'test-osc', filterRule: 'all', filters: [], outputs: [makeOSCAction()], }); const httpAutomation = await addAutomation({ title: 'test-http', filterRule: 'any', filters: [], outputs: [makeHTTPAction()], }); await addTrigger({ title: 'test-osc', trigger: TimerLifeCycle.onLoad, automationId: oscAutomation.id, }); await addTrigger({ title: 'test-http', trigger: TimerLifeCycle.onFinish, automationId: httpAutomation.id, }); }); it('should trigger automations for a given action', () => { const state = makeRuntimeStateData(); triggerAutomations(TimerLifeCycle.onLoad, state); expect(oscSpy).toHaveBeenCalledTimes(1); expect(httpSpy).not.toBeCalled(); oscSpy.mockReset(); httpSpy.mockReset(); triggerAutomations(TimerLifeCycle.onStart, state); expect(oscClient.emitOSC).not.toBeCalled(); expect(httpSpy).not.toBeCalled(); oscSpy.mockReset(); httpSpy.mockReset(); triggerAutomations(TimerLifeCycle.onFinish, state); expect(oscSpy).not.toBeCalled(); expect(httpSpy).toHaveBeenCalledTimes(1); oscSpy.mockReset(); httpSpy.mockReset(); triggerAutomations(TimerLifeCycle.onStop, state); expect(oscSpy).not.toBeCalled(); expect(httpSpy).not.toBeCalled(); }); }); describe('testConditions()', () => { it('should return true when no filters are provided', () => { const result = testConditions([], 'all', {}); expect(result).toBe(true); }); 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 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); }); 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', () => { 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); }); }); 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); }); 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); }); }); 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); }); 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); }); }); 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); const result2 = testConditions( [{ field: 'eventNow.title', operator: 'contains', value: 'sound' }], 'all', makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), ); expect(result2).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); const result2 = testConditions( [{ field: 'eventNow.title', operator: 'not_contains', value: 'sound' }], 'all', makeRuntimeStateData({ eventNow: makeOntimeEvent({ title: 'test-lighting' }) as PlayableEvent }), ); expect(result2).toBe(true); }); }); describe('for all filter rule', () => { it('should return true when all filters are true', () => { const mockStore = makeRuntimeStateData({ clock: 10, eventNow: makeOntimeEvent({ title: 'test', custom: { av: 'av-value' }, }) as PlayableEvent, }); const result = testConditions( [ { field: 'clock', operator: 'equals', value: '10' }, { field: 'eventNow.title', operator: 'equals', value: 'test' }, { field: 'eventNow.custom.av', operator: 'equals', value: 'av-value' }, ], 'all', mockStore, ); expect(result).toBe(true); }); it('should return false if any filters are false', () => { const mockStore = makeRuntimeStateData({ clock: 10, eventNow: makeOntimeEvent({ title: 'test', custom: { av: 'not-av-value' }, }) as PlayableEvent, }); const result = testConditions( [ { field: 'clock', operator: 'equals', value: '11' }, { field: 'eventNow.title', operator: 'equals', value: 'test' }, { field: 'eventNow.custom.av', operator: 'equals', value: 'av-value' }, ], 'all', mockStore, ); expect(result).toBe(false); }); }); describe('for any filter rule', () => { it('should return true when all filters are true', () => { const mockStore = makeRuntimeStateData({ clock: 10, eventNow: makeOntimeEvent({ title: 'test', custom: { av: 'av-value' }, }) as PlayableEvent, }); const result = testConditions( [ { field: 'clock', operator: 'equals', value: '10' }, { field: 'eventNow.title', operator: 'equals', value: 'test' }, { field: 'eventNow.custom.av', operator: 'equals', value: 'av-value' }, ], 'any', mockStore, ); expect(result).toBe(true); }); it('should return true if any filters are true', () => { const mockStore = makeRuntimeStateData({ clock: 10, eventNow: makeOntimeEvent({ title: 'not-test', custom: { av: 'av-value' }, }) as PlayableEvent, }); const result = testConditions( [ { field: 'clock', operator: 'equals', value: '11' }, { field: 'eventNow.title', operator: 'equals', value: 'not-test' }, { field: 'eventNow.custom.av', operator: 'equals', value: 'av-value' }, ], 'any', mockStore, ); expect(result).toBe(true); }); it('should return false if all filters are false', () => { const mockStore = makeRuntimeStateData({ clock: 10, eventNow: makeOntimeEvent({ title: 'test' }) as PlayableEvent, }); const result = testConditions( [ { field: 'clock', operator: 'equals', value: '11' }, { field: 'eventNow.title', operator: 'equals', value: 'not-test' }, ], 'any', mockStore, ); expect(result).toBe(false); }); }); });