diff --git a/apps/server/src/models/__tests__/demoProject.test.ts b/apps/server/src/models/__tests__/demoProject.test.ts new file mode 100644 index 000000000..0bf219b58 --- /dev/null +++ b/apps/server/src/models/__tests__/demoProject.test.ts @@ -0,0 +1,73 @@ +import { isHTTPOutput, isOSCOutput, isOntimeEvent, timerLifecycleValues } from 'ontime-types'; + +import { parseAutomation } from '../../api-data/automation/automation.validation.js'; +import { demoDb } from '../demoProject.js'; + +/** + * The demo ships with automations so the feature is visible on first run. + * They are hand written literals: nothing in the type system checks that a trigger + * resolves to an automation, or that the demo cannot reach out to the network. + */ +describe('demo project automations', () => { + const { automations, triggers } = demoDb.automation; + + it('is enabled, otherwise the automations are invisible', () => { + expect(demoDb.automation.enabledAutomations).toBe(true); + }); + + it('does not open a listening socket', () => { + expect(demoDb.automation.enabledOscIn).toBe(false); + }); + + it('keys every automation by its own id', () => { + for (const [key, automation] of Object.entries(automations)) { + expect(automation.id).toBe(key); + } + }); + + it('passes the same validation as a user created automation', () => { + for (const automation of Object.values(automations)) { + expect(() => parseAutomation(automation)).not.toThrow(); + } + }); + + it('resolves every global trigger to an automation', () => { + for (const trigger of triggers) { + expect(timerLifecycleValues).toContain(trigger.trigger); + expect(Object.hasOwn(automations, trigger.automationId)).toBe(true); + } + }); + + it('resolves every event level trigger to an automation', () => { + for (const rundown of Object.values(demoDb.rundowns)) { + for (const entry of Object.values(rundown.entries)) { + if (!isOntimeEvent(entry)) { + continue; + } + for (const trigger of entry.triggers) { + expect(timerLifecycleValues).toContain(trigger.trigger); + expect(Object.hasOwn(automations, trigger.automationId)).toBe(true); + } + } + } + }); + + it('never sends outside this machine, whatever the user presses', () => { + // an automation only fires if something triggers it + const boundIds = new Set(triggers.map((trigger) => trigger.automationId)); + for (const rundown of Object.values(demoDb.rundowns)) { + for (const entry of Object.values(rundown.entries)) { + if (isOntimeEvent(entry)) { + entry.triggers.forEach((trigger) => boundIds.add(trigger.automationId)); + } + } + } + + for (const id of boundIds) { + for (const output of automations[id].outputs) { + expect(isOSCOutput(output)).toBe(false); + expect(isHTTPOutput(output)).toBe(false); + } + } + }); +}); diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index db21d6075..7e68fc583 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -1,4 +1,4 @@ -import { DatabaseModel, OntimeView } from 'ontime-types'; +import { DatabaseModel, OntimeView, TimerLifeCycle } from 'ontime-types'; import { backstageRundown, broadcastRundown, stageRundown } from './demoRundowns.js'; @@ -76,11 +76,56 @@ export const demoDb: DatabaseModel = { label: 'PowerPoint Slide', }, }, + /** + * The demo ships with working automations so the engine is visible the first time + * someone presses Play, rather than hidden behind an empty settings panel. + * + * Everything that actually fires is an Ontime action: the demo must not put traffic + * on whatever network it happens to be opened on. The OSC entry is there to be read + * and edited, and is deliberately left without a trigger. + * + * The ids are hand written and must match the map keys. Ids are only generated for + * automations created through the DAO, so literals are safe here. + */ automation: { - enabledAutomations: false, + enabledAutomations: true, + // never open a listening socket without the user asking for it enabledOscIn: false, oscPortIn: 8888, - triggers: [], - automations: {}, + triggers: [ + { + id: 'demo-trigger-aux', + title: 'Demo: aux timer on start', + trigger: TimerLifeCycle.onStart, + automationId: 'demo-aux-timer', + }, + ], + automations: { + 'demo-aux-timer': { + id: 'demo-aux-timer', + title: 'Demo: run Aux Timer 1 with the event', + filterRule: 'all', + filters: [], + outputs: [ + { type: 'ontime', action: 'aux1-set', time: '00:05:00' }, + { type: 'ontime', action: 'aux1-start' }, + ], + }, + 'demo-danger-message': { + id: 'demo-danger-message', + title: 'Demo: warn the stage at danger', + filterRule: 'all', + filters: [], + // self labelled, so nobody mistakes it for something Ontime does on its own + outputs: [{ type: 'ontime', action: 'message-set', text: 'Demo automation: please wrap up', visible: true }], + }, + 'demo-osc-example': { + id: 'demo-osc-example', + title: 'Demo: OSC to a lighting console (example, not wired up)', + filterRule: 'all', + filters: [], + outputs: [{ type: 'osc', targetIP: '127.0.0.1', targetPort: 8000, address: '/ontime/go', args: '1' }], + }, + }, }, }; diff --git a/apps/server/src/models/demoRundowns.ts b/apps/server/src/models/demoRundowns.ts index 3f1a77744..aecfdf524 100644 --- a/apps/server/src/models/demoRundowns.ts +++ b/apps/server/src/models/demoRundowns.ts @@ -1,4 +1,4 @@ -import { SupportedEntry, TimeStrategy, EndAction, TimerType, Day, Rundown } from 'ontime-types'; +import { SupportedEntry, TimeStrategy, EndAction, TimerType, Day, Rundown, TimerLifeCycle } from 'ontime-types'; export const stageRundown: Rundown = { id: 'default', @@ -120,7 +120,16 @@ export const stageRundown: Rundown = { Audio_Notes: '1x Wireless Hand Held', PowerPoint_Name: 'HoldingSlide.pptx', }, - triggers: [], + // demonstrates that an automation can be attached to a single event, which is + // otherwise only discoverable by reading the docs. See demoProject.ts + triggers: [ + { + id: 'demo-event-trigger', + title: 'Wrap up warning', + trigger: TimerLifeCycle.onDanger, + automationId: 'demo-danger-message', + }, + ], }, fa593e: { id: 'fa593e',