Files
ontime/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts
T
2025-06-19 18:53:28 +02:00

900 lines
30 KiB
TypeScript

import { CustomFields, OntimeEvent, SupportedEvent, TimeStrategy } from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
import { demoDb } from '../../../models/demoProject.js';
import {
add,
batchEdit,
edit,
generate,
remove,
reorder,
swap,
createCustomField,
editCustomField,
removeCustomField,
customFieldChangelog,
} from '../rundownCache.js';
import { makeOntimeBlock, makeOntimeDelay, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
import { ProcessedRundownMetadata } from '../rundownCache.utils.js';
beforeAll(() => {
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
return {
getDataProvider: vi.fn().mockImplementation(() => {
return {
setCustomFields: vi.fn().mockImplementation((newData) => newData),
setRundown: vi.fn().mockImplementation((newData) => newData),
};
}),
};
});
});
describe('generate()', () => {
test('benchmark function execution time', () => {
const t1 = performance.now();
let result: ProcessedRundownMetadata | null = null;
for (let i = 0; i < 100; i++) {
result = generate(demoDb.rundowns.default, demoDb.customFields);
}
const t2 = performance.now();
console.warn(
'Rundown generation took',
t2 - t1,
'milliseconds for 100x',
Object.keys(result?.entries ?? {}).length,
'events',
);
});
it('generates metadata from given rundown', () => {
const initResult = generate(demoDb.rundowns.default, demoDb.customFields);
expect(initResult.order.length).toBe(12);
expect(initResult.flatEntryOrder.length).toBe(17);
expect(initResult.timedEventOrder.length).toBe(14);
expect(initResult.playableEventOrder.length).toBe(14);
expect(initResult.firstStart).toBe(36000000);
expect(initResult.lastEnd).toBe(61800000);
expect(initResult.totalDays).toBe(0);
expect(initResult.totalDelay).toBe(0);
});
it('creates normalised versions of a given rundown', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1' }),
'2': makeOntimeBlock({ id: '2' }),
'3': makeOntimeDelay({ id: '3' }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(3);
expect(initResult.order).toStrictEqual(['1', '2', '3']);
expect(initResult.entries['1'].type).toBe(SupportedEvent.Event);
expect(initResult.entries['2'].type).toBe(SupportedEvent.Block);
expect(initResult.entries['3'].type).toBe(SupportedEvent.Delay);
});
it('calculates delays versions of a given rundown', () => {
const rundown = makeRundown({
order: ['1', '2'],
entries: {
'1': makeOntimeDelay({ id: '1', duration: 100 }),
'2': makeOntimeEvent({ id: '2', timeStart: 1, timeEnd: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(2);
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(100);
expect(initResult.totalDelay).toBe(100);
});
it('accounts for gaps in rundown when calculating delays', () => {
const rundown = makeRundown({
order: ['1', 'delay', '2', 'block', '3', 'another-block', '4'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
delay: makeOntimeDelay({ id: 'delay', duration: 200 }),
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
block: makeOntimeBlock({ id: 'block', title: 'break' }),
'3': makeOntimeEvent({ id: '3', timeStart: 400, timeEnd: 500, duration: 100 }),
'another-block': makeOntimeBlock({ id: 'another-block', title: 'another-break' }),
'4': makeOntimeEvent({ id: '4', timeStart: 600, timeEnd: 700, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(7);
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(200);
expect((initResult.entries['3'] as OntimeEvent).delay).toBe(100);
expect((initResult.entries['4'] as OntimeEvent).delay).toBe(0);
expect(initResult.totalDelay).toBe(0);
expect(initResult.totalDuration).toBe(700 - 100);
});
it('accounts for overlaps in rundown', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 }),
'2': makeOntimeEvent({ id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 }),
'3': makeOntimeEvent({ id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start
});
it('accounts for overlaps in rundown (with added gap)', () => {
const rundown = makeRundown({
order: ['1', '2', '3', '4'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 }),
'2': makeOntimeEvent({ id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 }),
'3': makeOntimeEvent({ id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 }),
'4': makeOntimeEvent({ id: '4', timeStart: 15000, timeEnd: 20000, duration: 5000 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start
});
it('accounts for overlaps in rundown (with multiple days)', () => {
const rundown = makeRundown({
order: ['1', '2', '3', '4'],
entries: {
'1': makeOntimeEvent({
id: '1',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 10 * MILLIS_PER_HOUR,
duration: MILLIS_PER_HOUR,
}),
'2': makeOntimeEvent({
id: '2',
timeStart: 9 * MILLIS_PER_HOUR + 15 * MILLIS_PER_MINUTE,
timeEnd: 9 * MILLIS_PER_HOUR + 45 * MILLIS_PER_MINUTE,
duration: 30 * MILLIS_PER_MINUTE,
}),
'3': makeOntimeEvent({
id: '3',
timeStart: 9 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
timeEnd: 10 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
duration: MILLIS_PER_HOUR,
}),
'4': makeOntimeEvent({
id: '4',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 10 * MILLIS_PER_HOUR,
duration: MILLIS_PER_HOUR,
}),
},
});
const initResult = generate(rundown, {});
expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start
});
it('maintains delays over gaps', () => {
const rundown = makeRundown({
order: ['1', 'delay', '2'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
delay: makeOntimeDelay({ id: 'delay', duration: 500 }),
'2': makeOntimeEvent({ id: '2', timeStart: 500, timeEnd: 600, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.entries['2']).toMatchObject({
gap: 400,
delay: 500,
});
expect(initResult.totalDelay).toBe(500);
expect(initResult.totalDuration).toBe(600);
});
it('maintains delays over gaps when there are cumulative delays', () => {
const rundown = makeRundown({
order: ['delay1', '1', 'delay2', '2'],
entries: {
delay1: makeOntimeDelay({ id: 'delay', duration: 100 }),
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
delay2: makeOntimeDelay({ id: 'delay', duration: 300 }),
'2': makeOntimeEvent({ id: '2', timeStart: 300, timeEnd: 400, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.entries['1']).toMatchObject({
gap: 0,
delay: 100,
});
expect(initResult.entries['2']).toMatchObject({
gap: 200,
delay: 300, // first delay was fully absorbed
});
expect(initResult.totalDelay).toBe(300);
expect(initResult.totalDuration).toBe(400);
});
it('handles negative delays on gaps', () => {
const rundown = makeRundown({
order: ['delay1', '1', 'delay2', '2'],
entries: {
delay1: makeOntimeDelay({ id: 'delay', duration: -100 }),
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
delay2: makeOntimeDelay({ id: 'delay', duration: -300 }),
'2': makeOntimeEvent({ id: '2', timeStart: 300, timeEnd: 400, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.entries['1']).toMatchObject({
gap: 0,
delay: -100,
});
expect(initResult.entries['2']).toMatchObject({
gap: 200,
delay: -400,
});
expect(initResult.totalDelay).toBe(-400);
expect(initResult.totalDuration).toBe(400);
});
it('handles negative delays', () => {
const rundown = makeRundown({
order: ['1', 'delay', '2', 'block', '3', 'another-block', '4'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
delay: makeOntimeDelay({ id: 'delay', duration: -200 }),
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
block: makeOntimeBlock({ id: 'block', title: 'break' }),
'3': makeOntimeEvent({ id: '3', timeStart: 400, timeEnd: 500, duration: 100 }),
'another-block': makeOntimeBlock({ id: 'another-block', title: 'another-break' }),
'4': makeOntimeEvent({ id: '4', timeStart: 600, timeEnd: 700, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(7);
expect((initResult.entries['1'] as OntimeEvent).delay).toBe(0);
expect((initResult.entries['2'] as OntimeEvent).delay).toBe(-200);
expect((initResult.entries['3'] as OntimeEvent).delay).toBe(-200);
expect((initResult.entries['4'] as OntimeEvent).delay).toBe(-200);
expect(initResult.totalDelay).toBe(-200);
expect(initResult.totalDuration).toBe(700 - 100);
});
it('links times across events', () => {
const rundown = makeRundown({
order: ['1', '2', 'block', 'delay', '3'],
entries: {
'1': makeOntimeEvent({
id: '1',
timeStart: 1,
duration: 1,
timeEnd: 2,
timeStrategy: TimeStrategy.LockEnd,
}),
'2': makeOntimeEvent({
id: '2',
timeStart: 11,
duration: 1,
timeEnd: 12,
linkStart: true,
timeStrategy: TimeStrategy.LockEnd,
}),
block: makeOntimeBlock({ id: 'block' }),
delay: makeOntimeDelay({ id: 'delay' }),
'3': makeOntimeEvent({
id: '3',
timeStart: 21,
duration: 1,
timeEnd: 22,
linkStart: true,
timeStrategy: TimeStrategy.LockEnd,
}),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(5);
expect((initResult.entries['2'] as OntimeEvent).timeStart).toBe(2);
expect((initResult.entries['2'] as OntimeEvent).timeEnd).toBe(12);
expect((initResult.entries['2'] as OntimeEvent).duration).toBe(10);
expect((initResult.entries['3'] as OntimeEvent).timeStart).toBe(12);
expect((initResult.entries['3'] as OntimeEvent).timeEnd).toBe(22);
expect((initResult.entries['3'] as OntimeEvent).duration).toBe(10);
});
it('links times across events, reordered', () => {
const rundown = makeRundown({
order: ['1', '3', '2'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 1, timeEnd: 2 }),
'3': makeOntimeEvent({ id: '3', timeStart: 21, timeEnd: 22, linkStart: true }),
'2': makeOntimeEvent({ id: '2', timeStart: 11, timeEnd: 12, linkStart: true }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(3);
expect((initResult.entries['3'] as OntimeEvent).timeStart).toBe(2);
});
it('calculates total duration', () => {
const rundown = makeRundown({
order: ['1', '2', 'skipped', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 200, duration: 100 }),
'2': makeOntimeEvent({ id: '2', timeStart: 200, timeEnd: 300, duration: 100 }),
skipped: makeOntimeEvent({ id: 'skipped', skip: true, timeStart: 300, timeEnd: 400, duration: 100 }),
'3': makeOntimeEvent({ id: '2', timeStart: 400, timeEnd: 500, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(4);
expect(initResult.totalDuration).toBe(500 - 100);
});
it('calculates total duration with 0 duration events without causing a next day', () => {
const rundown = makeRundown({
order: ['1', '2', 'skipped', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 100, timeEnd: 100, duration: 0 }),
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 300, duration: 200 }),
skipped: makeOntimeEvent({ id: 'skipped', skip: true, timeStart: 300, timeEnd: 400, duration: 0 }),
'3': makeOntimeEvent({ id: '2', timeStart: 400, timeEnd: 500, duration: 100 }),
},
});
const initResult = generate(rundown, {});
expect(initResult.order.length).toBe(4);
expect(initResult.totalDuration).toBe(500 - 100);
});
it('calculates total duration across days with gap', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({
id: '1',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
}),
'2': makeOntimeEvent({
id: '2',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
}),
'3': makeOntimeEvent({
id: '2',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
}),
},
});
const initResult = generate(rundown, {});
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
});
it('calculates total duration across days', () => {
const rundown = makeRundown({
order: ['1', '2'],
entries: {
'1': makeOntimeEvent({
id: '1',
timeStart: 12 * MILLIS_PER_HOUR,
timeEnd: 22 * MILLIS_PER_HOUR,
duration: 10 * MILLIS_PER_HOUR,
}),
'2': makeOntimeEvent({
id: '2',
timeStart: 22 * MILLIS_PER_HOUR,
timeEnd: 8 * MILLIS_PER_HOUR,
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
}),
},
});
const initResult = generate(rundown, {});
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
expect(initResult.totalDuration).toBe(expectedDuration);
});
it('handles updating event sequence', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({
id: '1',
timeStart: 0,
timeEnd: 600000,
duration: 600000,
timeStrategy: TimeStrategy.LockDuration,
linkStart: false,
}),
'2': makeOntimeEvent({
id: '2',
timeStart: 600000,
timeEnd: 601000,
duration: 85801000, // <------------- value out of sync
timeStrategy: TimeStrategy.LockEnd,
linkStart: true,
}),
'3': makeOntimeEvent({
id: '3',
timeStart: 100, // <------------- value out of sync
timeEnd: 602000,
duration: 0,
timeStrategy: TimeStrategy.LockEnd,
linkStart: true,
}),
},
});
const initResult = generate(rundown, {});
expect(initResult.entries).toMatchObject({
'1': {
timeStart: 0,
timeEnd: 600000,
duration: 600000,
timeStrategy: 'lock-duration',
linkStart: false,
},
'2': {
timeStart: 600000,
timeEnd: 601000,
duration: 1000,
timeStrategy: 'lock-end',
linkStart: true,
},
'3': {
timeStart: 601000,
timeEnd: 602000,
duration: 1000,
timeStrategy: 'lock-end',
linkStart: true,
},
});
});
describe('custom properties feature', () => {
it('creates a map of custom properties', () => {
const customProperties: CustomFields = {
lighting: {
label: 'lighting',
type: 'string',
colour: 'red',
},
sound: {
label: 'sound',
type: 'string',
colour: 'red',
},
};
const rundown = makeRundown({
order: ['1', '2'],
entries: {
'1': makeOntimeEvent({
id: '1',
custom: {
lighting: 'event 1 lx',
},
}),
'2': makeOntimeEvent({
id: '2',
custom: {
lighting: 'event 2 lx',
sound: 'event 2 sound',
},
}),
},
});
const initResult = generate(rundown, customProperties);
expect(initResult.order.length).toBe(2);
expect(initResult.assignedCustomFields).toMatchObject({
lighting: ['1', '2'],
sound: ['2'],
});
expect((initResult.entries['1'] as OntimeEvent).custom).toMatchObject({ lighting: 'event 1 lx' });
expect((initResult.entries['2'] as OntimeEvent).custom).toMatchObject({
lighting: 'event 2 lx',
sound: 'event 2 sound',
});
});
});
});
describe('generate() v4', () => {
describe('handle of event groups', () => {
it('correctly parses group metadata', () => {
const rundown = makeRundown({
order: ['1'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['100', '200', '300'] }),
'100': makeOntimeEvent({ id: '100', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }),
'200': makeOntimeEvent({ id: '200', timeStart: 200, timeEnd: 300, duration: 100 }),
'300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }),
},
});
const generatedRundown = generate(rundown, {});
expect(generatedRundown.order).toMatchObject(['1']);
expect(generatedRundown.totalDuration).toBe(300);
expect(generatedRundown.totalDelay).toBe(0);
expect(generatedRundown.entries).toMatchObject({
'1': {
type: SupportedEvent.Block,
events: ['100', '200', '300'],
startTime: 100,
endTime: 400,
duration: 300,
isFirstLinked: false,
numEvents: 3,
},
'100': { type: SupportedEvent.Event, parent: '1' },
'200': { type: SupportedEvent.Event, parent: '1' },
'300': { type: SupportedEvent.Event, parent: '1' },
});
});
it('treats groups as invisible for gap calculations', () => {
const rundown = makeRundown({
order: ['0', '1', '2', '3'],
entries: {
'0': makeOntimeEvent({ id: '0', timeStart: 0, timeEnd: 10, duration: 10, linkStart: false }),
'1': makeOntimeBlock({ id: '1', events: ['101', '102', '103'] }),
'101': makeOntimeEvent({ id: '101', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }),
'102': makeOntimeEvent({ id: '102', timeStart: 200, timeEnd: 300, duration: 100, linkStart: true }),
'103': makeOntimeEvent({ id: '103', timeStart: 300, timeEnd: 400, duration: 100, linkStart: true }),
'2': makeOntimeBlock({ id: '2', events: ['201', '202', '203'] }),
'201': makeOntimeEvent({ id: '201', timeStart: 500, timeEnd: 600, duration: 100, linkStart: false }),
'202': makeOntimeEvent({ id: '202', timeStart: 600, timeEnd: 700, duration: 100, linkStart: true }),
'203': makeOntimeEvent({ id: '203', timeStart: 700, timeEnd: 800, duration: 100, linkStart: true }),
'3': makeOntimeBlock({ id: '3', events: ['301', '302', '303'] }),
'301': makeOntimeEvent({ id: '301', timeStart: 900, timeEnd: 1000, duration: 100, linkStart: false }),
'302': makeOntimeEvent({ id: '302', timeStart: 1000, timeEnd: 1100, duration: 100, linkStart: true }),
'303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }),
},
});
const generatedRundown = generate(rundown, {});
expect(generatedRundown.order).toMatchObject(['0', '1', '2', '3']);
expect(generatedRundown.totalDuration).toBe(1200);
expect(generatedRundown.totalDelay).toBe(0);
expect(generatedRundown.entries).toMatchObject({
'0': { type: SupportedEvent.Event, parent: null },
'1': {
type: SupportedEvent.Block,
events: ['101', '102', '103'],
startTime: 100,
endTime: 400,
duration: 300,
isFirstLinked: false,
numEvents: 3,
},
'101': { parent: '1', gap: 90, linkStart: false },
'102': { parent: '1' },
'103': { parent: '1' },
'2': {
type: SupportedEvent.Block,
events: ['201', '202', '203'],
startTime: 500,
endTime: 800,
duration: 300,
isFirstLinked: false,
numEvents: 3,
},
'201': { id: '201', timeStart: 500, timeEnd: 600, duration: 100, gap: 100, linkStart: false },
'202': { id: '202', timeStart: 600, timeEnd: 700, duration: 100 },
'203': { id: '203', timeStart: 700, timeEnd: 800, duration: 100 },
'3': {
type: SupportedEvent.Block,
events: ['301', '302', '303'],
startTime: 900,
endTime: 1200,
duration: 300,
isFirstLinked: false,
numEvents: 3,
},
'301': { id: '301', timeStart: 900, timeEnd: 1000, duration: 100, gap: 100, linkStart: false },
'302': { id: '302', timeStart: 1000, timeEnd: 1100, duration: 100 },
'303': { id: '303', timeStart: 1100, timeEnd: 1200, duration: 100 },
});
});
});
});
describe('add() mutation', () => {
test('adds an event to the rundown', () => {
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
const rundown = makeRundown({});
const { newRundown } = add({ atIndex: 0, entry: mockEvent, parent: null, rundown });
expect(newRundown.order.length).toBe(1);
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
});
});
describe('remove() mutation', () => {
test('deletes an event from the rundown', () => {
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
const rundown = makeRundown({
order: ['mock'],
entries: {
mock: mockEvent,
},
});
const { newRundown } = remove({ eventIds: [mockEvent.id], rundown });
expect(newRundown.order.length).toBe(0);
});
test('deletes multiple events from the rundown', () => {
const rundown = makeRundown({
order: ['1', '2', '3', '4', '5', '6'],
entries: {
'1': makeOntimeEvent({ id: '1' }),
'2': makeOntimeBlock({ id: '2' }),
'3': makeOntimeDelay({ id: '3' }),
'4': makeOntimeEvent({ id: '4' }),
'5': makeOntimeEvent({ id: '5' }),
'6': makeOntimeEvent({ id: '6' }),
},
});
const { newRundown } = remove({ eventIds: ['1', '2', '3'], rundown });
expect(newRundown.order.length).toBe(3);
expect(newRundown.entries[newRundown.order[0]].id).toBe('4');
});
});
describe('edit() mutation', () => {
test('edits an event in the rundown', () => {
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
const mockEventPatch = makeOntimeEvent({ cue: 'patched' });
const rundown = makeRundown({
order: ['mock'],
entries: {
mock: mockEvent,
},
});
const { newRundown, newEvent } = edit({
eventId: mockEvent.id,
patch: mockEventPatch,
rundown,
});
expect(newRundown.order.length).toBe(1);
expect(newEvent).toMatchObject({
id: 'mock',
cue: 'patched',
type: SupportedEvent.Event,
});
});
});
describe('batchEdit() mutation', () => {
it('should correctly apply the patch to the events with the given IDs', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', cue: 'data1' }),
'2': makeOntimeEvent({ id: '2', cue: 'data2' }),
'3': makeOntimeEvent({ id: '3', cue: 'data3' }),
},
});
const eventIds = ['1', '3'];
const patch = { cue: 'newData' };
const { newRundown } = batchEdit({ rundown, eventIds, patch });
expect(newRundown.entries).toMatchObject({
'1': { id: '1', type: SupportedEvent.Event, cue: 'newData' },
'2': { id: '2', type: SupportedEvent.Event, cue: 'data2' },
'3': { id: '3', type: SupportedEvent.Event, cue: 'newData' },
});
});
});
describe('reorder() mutation', () => {
it('should correctly reorder two events', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', cue: 'data1', revision: 0 }),
'2': makeOntimeEvent({ id: '2', cue: 'data2', revision: 0 }),
'3': makeOntimeEvent({ id: '3', cue: 'data3', revision: 0 }),
},
});
// move first event to the end
const { newRundown } = reorder({
rundown: rundown,
eventId: rundown.order[0],
from: 0,
to: rundown.order.length - 1,
});
expect(newRundown.order).toStrictEqual(['2', '3', '1']);
expect(newRundown.entries).toMatchObject({
'2': { id: '2', cue: 'data2', revision: 1 },
'3': { id: '3', cue: 'data3', revision: 1 },
'1': { id: '1', cue: 'data1', revision: 1 },
});
});
});
describe('swap() mutation', () => {
it('should correctly swap data between events', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', cue: 'data1', timeStart: 1, revision: 4 }),
'2': makeOntimeEvent({ id: '2', cue: 'data2', timeStart: 2, revision: 8 }),
'3': makeOntimeEvent({ id: '3', cue: 'data3', timeStart: 3, revision: 12 }),
},
});
// swap first and second event
const { newRundown } = swap({
rundown: rundown,
fromId: rundown.order[0],
toId: rundown.order[1],
});
expect(newRundown.order).toStrictEqual(['1', '2', '3']);
expect(newRundown.entries['1']).toMatchObject({
id: '1',
cue: 'data2',
timeStart: 1,
revision: 5,
});
expect(newRundown.entries['2']).toMatchObject({
id: '2',
cue: 'data1',
timeStart: 2,
revision: 9,
});
expect(newRundown.entries['3']).toMatchObject({
id: '3',
cue: 'data3',
timeStart: 3,
revision: 12,
});
});
});
describe('custom fields flow', () => {
describe('createCustomField()', () => {
it('creates a field from given parameters', () => {
const expected = {
Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
};
const customField = createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' });
expect(customField).toStrictEqual(expected);
});
});
describe('editCustomField()', () => {
it('edits a field with a given label', () => {
createCustomField({ label: 'Sound', type: 'string', colour: 'blue' });
const expected = {
Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
};
const customField = editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' });
expect(customFieldChangelog).toStrictEqual({});
expect(customField).toStrictEqual(expected);
});
it('renames a field to a new label', () => {
const created = createCustomField({ label: 'Video', type: 'string', colour: 'red' });
const expected = {
Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
Video: {
label: 'Video',
type: 'string',
colour: 'red',
},
};
expect(created).toStrictEqual(expected);
const expectedAfter = {
Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
Sound: {
label: 'Sound',
type: 'string',
colour: 'green',
},
AV: {
label: 'AV',
type: 'string',
colour: 'red',
},
};
// We need to flush all scheduled tasks for the generate function to settle
vi.useFakeTimers();
const customField = editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' });
expect(customField).toStrictEqual(expectedAfter);
expect(customFieldChangelog).toStrictEqual({ Video: 'AV' });
editCustomField('AV', { label: 'Video' });
vi.runAllTimers();
expect(customFieldChangelog).toStrictEqual({});
vi.useRealTimers();
});
});
describe('removeCustomField()', () => {
it('deletes a field with a given label', () => {
const expected = {
Lighting: {
label: 'Lighting',
type: 'string',
colour: 'blue',
},
Video: {
label: 'Video',
type: 'string',
colour: 'red',
},
};
const customField = removeCustomField('Sound');
expect(customField).toStrictEqual(expected);
});
});
});