mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
1049 lines
32 KiB
TypeScript
1049 lines
32 KiB
TypeScript
import {
|
|
CustomFields,
|
|
EndAction,
|
|
EventCustomFields,
|
|
OntimeBlock,
|
|
OntimeDelay,
|
|
OntimeEvent,
|
|
OntimeRundown,
|
|
SupportedEvent,
|
|
TimeStrategy,
|
|
TimerType,
|
|
} from 'ontime-types';
|
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, dayInMs } from 'ontime-utils';
|
|
|
|
import { calculateRuntimeDelays, getDelayAt, calculateRuntimeDelaysFrom } from '../delayUtils.js';
|
|
import {
|
|
add,
|
|
batchEdit,
|
|
edit,
|
|
generate,
|
|
remove,
|
|
reorder,
|
|
swap,
|
|
createCustomField,
|
|
editCustomField,
|
|
removeCustomField,
|
|
customFieldChangelog,
|
|
} from '../rundownCache.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()', () => {
|
|
it('creates normalised versions of a given rundown', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
|
|
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(3);
|
|
expect(initResult.order).toStrictEqual(['1', '2', '3']);
|
|
expect(initResult.rundown['1'].type).toBe(SupportedEvent.Event);
|
|
expect(initResult.rundown['2'].type).toBe(SupportedEvent.Block);
|
|
expect(initResult.rundown['3'].type).toBe(SupportedEvent.Delay);
|
|
});
|
|
|
|
it('calculates delays versions of a given rundown', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Delay, id: '1', duration: 100 } as OntimeDelay,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 1, timeEnd: 100 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(2);
|
|
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(100);
|
|
expect(initResult.totalDelay).toBe(100);
|
|
});
|
|
|
|
it('accounts for gaps in rundown when calculating delays', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Delay, id: 'delay', duration: 200 } as OntimeDelay,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
|
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(7);
|
|
expect((initResult.rundown['1'] as OntimeEvent).delay).toBe(0);
|
|
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(200);
|
|
expect((initResult.rundown['3'] as OntimeEvent).delay).toBe(100);
|
|
expect((initResult.rundown['4'] as OntimeEvent).delay).toBe(0);
|
|
expect(initResult.totalDelay).toBe(0);
|
|
expect(initResult.totalDuration).toBe(700 - 100);
|
|
});
|
|
|
|
it('accounts for overlaps in rundown', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.totalDuration).toBe(10500 - 9000); // last end - first start
|
|
});
|
|
|
|
it('accounts for overlaps in rundown (with added gap)', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 9000, timeEnd: 10000, duration: 1000 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 9250, timeEnd: 9500, duration: 250 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 9500, timeEnd: 10500, duration: 1000 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '4', timeStart: 15000, timeEnd: 20000, duration: 5000 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.totalDuration).toBe(20000 - 9000); // last end - first start
|
|
});
|
|
|
|
it('accounts for overlaps in rundown (with multiple days)', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '1',
|
|
timeStart: 9 * MILLIS_PER_HOUR,
|
|
timeEnd: 10 * MILLIS_PER_HOUR,
|
|
duration: MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
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,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '3',
|
|
timeStart: 9 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
|
timeEnd: 10 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE,
|
|
duration: MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '4',
|
|
timeStart: 9 * MILLIS_PER_HOUR,
|
|
timeEnd: 10 * MILLIS_PER_HOUR,
|
|
duration: MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.totalDuration).toBe(dayInMs + MILLIS_PER_HOUR); // day + last end - first start
|
|
});
|
|
|
|
it('handles negative delays', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Delay, id: 'delay', duration: -200 } as OntimeDelay,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
|
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(7);
|
|
expect((initResult.rundown['1'] as OntimeEvent).delay).toBe(0);
|
|
expect((initResult.rundown['2'] as OntimeEvent).delay).toBe(-200);
|
|
expect((initResult.rundown['3'] as OntimeEvent).delay).toBe(-200);
|
|
expect((initResult.rundown['4'] as OntimeEvent).delay).toBe(-200);
|
|
expect(initResult.totalDelay).toBe(-200);
|
|
expect(initResult.totalDuration).toBe(700 - 100);
|
|
});
|
|
|
|
it('links times across events', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '1',
|
|
timeStart: 1,
|
|
duration: 1,
|
|
timeEnd: 2,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '2',
|
|
timeStart: 11,
|
|
duration: 1,
|
|
timeEnd: 12,
|
|
linkStart: '1',
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
} as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: 'block' } as OntimeBlock,
|
|
{ type: SupportedEvent.Delay, id: 'delay' } as OntimeDelay,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '3',
|
|
timeStart: 21,
|
|
duration: 1,
|
|
timeEnd: 22,
|
|
linkStart: '2',
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
} as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(5);
|
|
expect((initResult.rundown['2'] as OntimeEvent).timeStart).toBe(2);
|
|
expect((initResult.rundown['2'] as OntimeEvent).timeEnd).toBe(12);
|
|
expect((initResult.rundown['2'] as OntimeEvent).duration).toBe(10);
|
|
|
|
expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(12);
|
|
expect((initResult.rundown['3'] as OntimeEvent).timeEnd).toBe(22);
|
|
expect((initResult.rundown['3'] as OntimeEvent).duration).toBe(10);
|
|
|
|
expect(initResult.links['1']).toBe('2');
|
|
expect(initResult.links['2']).toBe('3');
|
|
});
|
|
|
|
it('links times across events, reordered', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 1, timeEnd: 2 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 21, timeEnd: 22, linkStart: '2' } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 11, timeEnd: 12, linkStart: '1' } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(3);
|
|
expect((initResult.rundown['3'] as OntimeEvent).timeStart).toBe(2);
|
|
expect(initResult.links['1']).toBe('3');
|
|
expect(initResult.links['3']).toBe('2');
|
|
});
|
|
|
|
it('calculates total duration', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: 'skipped',
|
|
skip: true,
|
|
timeStart: 300,
|
|
timeEnd: 400,
|
|
duration: 100,
|
|
} as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
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 testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300, duration: 200 } as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: 'skipped',
|
|
skip: true,
|
|
timeStart: 300,
|
|
timeEnd: 400,
|
|
duration: 0,
|
|
} as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(4);
|
|
expect(initResult.totalDuration).toBe(500 - 100);
|
|
});
|
|
|
|
it('calculates total duration across days with gap', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '1',
|
|
timeStart: 9 * MILLIS_PER_HOUR,
|
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '2',
|
|
timeStart: 9 * MILLIS_PER_HOUR,
|
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '3',
|
|
timeStart: 9 * MILLIS_PER_HOUR,
|
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
|
});
|
|
|
|
it('calculates total duration across days', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '1',
|
|
timeStart: 12 * MILLIS_PER_HOUR,
|
|
timeEnd: 22 * MILLIS_PER_HOUR,
|
|
duration: 10 * MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '2',
|
|
timeStart: 22 * MILLIS_PER_HOUR,
|
|
timeEnd: 8 * MILLIS_PER_HOUR,
|
|
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
|
|
} as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
|
expect(initResult.totalDuration).toBe(expectedDuration);
|
|
});
|
|
|
|
it('handles updating event sequence', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '97cc3e',
|
|
timeStart: 0,
|
|
timeEnd: 600000,
|
|
duration: 600000,
|
|
timeStrategy: TimeStrategy.LockDuration,
|
|
linkStart: null,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: 'e01948',
|
|
timeStart: 600000,
|
|
timeEnd: 601000,
|
|
duration: 85801000, // <------------- value out of sync
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: '97cc3e',
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '25c1af',
|
|
timeStart: 100, // <------------- value out of sync
|
|
timeEnd: 602000,
|
|
duration: 0,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: 'e01948',
|
|
} as OntimeEvent,
|
|
];
|
|
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.rundown).toMatchObject({
|
|
'97cc3e': {
|
|
timeStart: 0,
|
|
timeEnd: 600000,
|
|
duration: 600000,
|
|
timeStrategy: 'lock-duration',
|
|
linkStart: null,
|
|
},
|
|
e01948: {
|
|
timeStart: 600000,
|
|
timeEnd: 601000,
|
|
duration: 1000,
|
|
timeStrategy: 'lock-end',
|
|
linkStart: '97cc3e',
|
|
},
|
|
'25c1af': {
|
|
timeStart: 601000,
|
|
timeEnd: 602000,
|
|
duration: 1000,
|
|
timeStrategy: 'lock-end',
|
|
linkStart: 'e01948',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('deletes links if invalid', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1', timeStart: 1, linkStart: '10' } as OntimeEvent,
|
|
];
|
|
const initResult = generate(testRundown);
|
|
expect(initResult.order.length).toBe(1);
|
|
expect((initResult.rundown['1'] as OntimeEvent).timeStart).toBe(1);
|
|
expect(Object.keys(initResult.links).length).toBe(0);
|
|
});
|
|
|
|
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 testRundown: OntimeRundown = [
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '1',
|
|
custom: {
|
|
lighting: 'event 1 lx',
|
|
} as EventCustomFields,
|
|
} as OntimeEvent,
|
|
{
|
|
type: SupportedEvent.Event,
|
|
id: '2',
|
|
custom: {
|
|
lighting: 'event 2 lx',
|
|
sound: 'event 2 sound',
|
|
} as EventCustomFields,
|
|
} as OntimeEvent,
|
|
];
|
|
const initResult = generate(testRundown, customProperties);
|
|
expect(initResult.order.length).toBe(2);
|
|
expect(initResult.assignedCustomFields).toMatchObject({
|
|
lighting: ['1', '2'],
|
|
sound: ['2'],
|
|
});
|
|
expect((initResult.rundown['1'] as OntimeEvent).custom).toMatchObject({ lighting: 'event 1 lx' });
|
|
expect((initResult.rundown['2'] as OntimeEvent).custom).toMatchObject({
|
|
lighting: 'event 2 lx',
|
|
sound: 'event 2 sound',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('add() mutation', () => {
|
|
test('adds an event to the rundown', () => {
|
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
|
const testRundown: OntimeRundown = [];
|
|
const { newRundown } = add({ atIndex: 0, event: mockEvent, persistedRundown: testRundown });
|
|
expect(newRundown.length).toBe(1);
|
|
expect(newRundown[0]).toMatchObject(mockEvent);
|
|
});
|
|
});
|
|
|
|
describe('remove() mutation', () => {
|
|
test('deletes an event from the rundown', () => {
|
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
|
const testRundown: OntimeRundown = [mockEvent];
|
|
const { newRundown } = remove({ eventIds: [mockEvent.id], persistedRundown: testRundown });
|
|
expect(newRundown.length).toBe(0);
|
|
});
|
|
test('deletes multiple events from the rundown', () => {
|
|
const testRundown: OntimeRundown = [
|
|
{ type: SupportedEvent.Event, id: '1' } as OntimeEvent,
|
|
{ type: SupportedEvent.Block, id: '2' } as OntimeBlock,
|
|
{ type: SupportedEvent.Delay, id: '3' } as OntimeDelay,
|
|
{ type: SupportedEvent.Event, id: '4' } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '5' } as OntimeEvent,
|
|
{ type: SupportedEvent.Event, id: '6' } as OntimeEvent,
|
|
];
|
|
const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown });
|
|
expect(newRundown.length).toBe(3);
|
|
expect(newRundown.at(0)?.id).toBe('4');
|
|
});
|
|
});
|
|
|
|
describe('edit() mutation', () => {
|
|
test('edits an event in the rundown', () => {
|
|
const mockEvent = { id: 'mock', cue: 'mock', type: SupportedEvent.Event } as OntimeEvent;
|
|
const mockEventPatch = { cue: 'patched' } as OntimeEvent;
|
|
const testRundown: OntimeRundown = [mockEvent];
|
|
const { newRundown, newEvent } = edit({
|
|
eventId: mockEvent.id,
|
|
patch: mockEventPatch,
|
|
persistedRundown: testRundown,
|
|
});
|
|
expect(newRundown.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 persistedRundown: OntimeRundown = [
|
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1' } as OntimeEvent,
|
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2' } as OntimeEvent,
|
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3' } as OntimeEvent,
|
|
];
|
|
const eventIds = ['1', '3'];
|
|
const patch = { cue: 'newData' };
|
|
|
|
const { newRundown } = batchEdit({ persistedRundown, eventIds, patch });
|
|
|
|
expect(newRundown).toMatchObject([
|
|
{ id: '1', type: SupportedEvent.Event, cue: 'newData' },
|
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2' },
|
|
{ id: '3', type: SupportedEvent.Event, cue: 'newData' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('reorder() mutation', () => {
|
|
it('should correctly reorder two events', () => {
|
|
const persistedRundown: OntimeRundown = [
|
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 0 } as OntimeEvent,
|
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 0 } as OntimeEvent,
|
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 0 } as OntimeEvent,
|
|
];
|
|
const { newRundown } = reorder({
|
|
persistedRundown,
|
|
eventId: persistedRundown[0].id,
|
|
from: 0,
|
|
to: persistedRundown.length - 1,
|
|
});
|
|
|
|
expect(newRundown).toMatchObject([
|
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', revision: 1 },
|
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', revision: 1 },
|
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', revision: 1 },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('swap() mutation', () => {
|
|
it('should correctly swap data between events', () => {
|
|
const persistedRundown: OntimeRundown = [
|
|
{ id: '1', type: SupportedEvent.Event, cue: 'data1', timeStart: 1, revision: 0 } as OntimeEvent,
|
|
{ id: '2', type: SupportedEvent.Event, cue: 'data2', timeStart: 2, revision: 0 } as OntimeEvent,
|
|
{ id: '3', type: SupportedEvent.Event, cue: 'data3', timeStart: 3, revision: 0 } as OntimeEvent,
|
|
];
|
|
const { newRundown } = swap({
|
|
persistedRundown,
|
|
fromId: persistedRundown[0].id,
|
|
toId: persistedRundown[1].id,
|
|
});
|
|
|
|
expect((newRundown[0] as OntimeEvent).id).toBe('1');
|
|
expect((newRundown[0] as OntimeEvent).cue).toBe('data2');
|
|
expect((newRundown[0] as OntimeEvent).timeStart).toBe(1);
|
|
expect((newRundown[0] as OntimeEvent).revision).toBe(1);
|
|
|
|
expect((newRundown[1] as OntimeEvent).id).toBe('2');
|
|
expect((newRundown[1] as OntimeEvent).cue).toBe('data1');
|
|
expect((newRundown[1] as OntimeEvent).timeStart).toBe(2);
|
|
expect((newRundown[1] as OntimeEvent).revision).toBe(1);
|
|
|
|
expect((newRundown[2] as OntimeEvent).id).toBe('3');
|
|
expect((newRundown[2] as OntimeEvent).cue).toBe('data3');
|
|
expect((newRundown[2] as OntimeEvent).timeStart).toBe(3);
|
|
expect((newRundown[2] as OntimeEvent).revision).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('calculateRuntimeDelays', () => {
|
|
it('calculates all delays in a given rundown', () => {
|
|
const rundown: OntimeRundown = [
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '659e1',
|
|
cue: '1',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 600000,
|
|
type: SupportedEvent.Delay,
|
|
id: '07986',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1200000,
|
|
duration: 0,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '1c48f',
|
|
cue: '2',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 1200000,
|
|
type: SupportedEvent.Delay,
|
|
id: '7db42',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: 'd48c2',
|
|
cue: '3',
|
|
custom: {},
|
|
},
|
|
{
|
|
title: '',
|
|
type: SupportedEvent.Block,
|
|
id: '9870d',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1800000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '2f185',
|
|
cue: '4',
|
|
custom: {},
|
|
},
|
|
];
|
|
|
|
const updatedRundown = calculateRuntimeDelays(rundown);
|
|
|
|
expect(rundown.length).toBe(updatedRundown.length);
|
|
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
|
expect((updatedRundown[2] as OntimeEvent).delay).toBe(600000);
|
|
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
|
expect((updatedRundown[6] as OntimeEvent).delay).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getDelayAt()', () => {
|
|
const delayedRundown: OntimeRundown = [
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '659e1',
|
|
delay: 0,
|
|
cue: '1',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 600000,
|
|
type: SupportedEvent.Delay,
|
|
id: '07986',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1200000,
|
|
duration: 0,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '1c48f',
|
|
delay: 600000,
|
|
cue: '2',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 1200000,
|
|
type: SupportedEvent.Delay,
|
|
id: '7db42',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: 'd48c2',
|
|
delay: 1800000,
|
|
cue: '3',
|
|
custom: {},
|
|
},
|
|
{
|
|
title: '',
|
|
type: SupportedEvent.Block,
|
|
id: '9870d',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1800000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '2f185',
|
|
delay: 0,
|
|
cue: '4',
|
|
custom: {},
|
|
},
|
|
];
|
|
|
|
it('calculates delay in a rundown', () => {
|
|
const delayAtStart = getDelayAt(0, delayedRundown);
|
|
const delayOnFirstEvent = getDelayAt(2, delayedRundown);
|
|
const delayOnSecondEvent = getDelayAt(4, delayedRundown);
|
|
const delayOnBlockedEvent = getDelayAt(0, delayedRundown);
|
|
|
|
expect(delayAtStart).toBe(0);
|
|
expect(delayOnFirstEvent).toBe(600000);
|
|
expect(delayOnSecondEvent).toBe(600000 + 1200000);
|
|
expect(delayOnBlockedEvent).toBe(0);
|
|
});
|
|
it('finds delay before a delay block', () => {
|
|
const valueOnFirstDelayBlock = getDelayAt(1, delayedRundown);
|
|
const valueOnSecondDelayBlock = getDelayAt(3, delayedRundown);
|
|
const valueAfterSecondDelayBlock = getDelayAt(4, delayedRundown);
|
|
|
|
expect(valueOnFirstDelayBlock).toBe(0);
|
|
expect(valueOnSecondDelayBlock).toBe(600000);
|
|
expect(valueAfterSecondDelayBlock).toBe(600000 + 1200000);
|
|
});
|
|
it('returns 0 after blocks', () => {
|
|
const valueOnBlock = getDelayAt(6, delayedRundown);
|
|
expect(valueOnBlock).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('calculateRuntimeDelaysFrom()', () => {
|
|
it('updates delays from given id', () => {
|
|
const delayedRundown: OntimeRundown = [
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '659e1',
|
|
delay: 0,
|
|
cue: '1',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 600000,
|
|
type: SupportedEvent.Delay,
|
|
id: '07986',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1200000,
|
|
duration: 0,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '1c48f',
|
|
delay: 0,
|
|
cue: '2',
|
|
custom: {},
|
|
},
|
|
{
|
|
duration: 1200000,
|
|
type: SupportedEvent.Delay,
|
|
id: '7db42',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 600000,
|
|
timeEnd: 1200000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: 'd48c2',
|
|
delay: 1800000,
|
|
cue: '3',
|
|
custom: {},
|
|
},
|
|
{
|
|
title: '',
|
|
type: SupportedEvent.Block,
|
|
id: '9870d',
|
|
},
|
|
{
|
|
title: '',
|
|
note: '',
|
|
endAction: EndAction.None,
|
|
timerType: TimerType.CountDown,
|
|
isTimeToEnd: false,
|
|
timeStrategy: TimeStrategy.LockEnd,
|
|
linkStart: null,
|
|
timeStart: 1200000,
|
|
timeEnd: 1800000,
|
|
duration: 600000,
|
|
isPublic: true,
|
|
skip: false,
|
|
colour: '',
|
|
type: SupportedEvent.Event,
|
|
revision: 0,
|
|
timeWarning: 120000,
|
|
timeDanger: 60000,
|
|
id: '2f185',
|
|
delay: 0,
|
|
cue: '4',
|
|
custom: {},
|
|
},
|
|
];
|
|
|
|
const updatedRundown = calculateRuntimeDelaysFrom('07986', delayedRundown);
|
|
|
|
// we only update from the 4th on
|
|
expect((updatedRundown[0] as OntimeEvent).delay).toBe(0);
|
|
// 1 + 3
|
|
expect((updatedRundown[4] as OntimeEvent).delay).toBe(600000 + 1200000);
|
|
});
|
|
});
|
|
|
|
describe('custom fields', () => {
|
|
describe('createCustomField()', () => {
|
|
it('creates a field from given parameters', async () => {
|
|
const expected = {
|
|
Lighting: {
|
|
label: 'Lighting',
|
|
type: 'string',
|
|
colour: 'blue',
|
|
},
|
|
};
|
|
|
|
const customField = await createCustomField({ label: 'Lighting', type: 'string', colour: 'blue' });
|
|
expect(customField).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('editCustomField()', () => {
|
|
it('edits a field with a given label', async () => {
|
|
await 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 = await editCustomField('Sound', { label: 'Sound', type: 'string', colour: 'green' });
|
|
expect(customFieldChangelog).toStrictEqual(new Map());
|
|
|
|
expect(customField).toStrictEqual(expected);
|
|
});
|
|
|
|
it('renames a field to a new label', async () => {
|
|
const created = await 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 = await editCustomField('Video', { label: 'AV', type: 'string', colour: 'red' });
|
|
expect(customField).toStrictEqual(expectedAfter);
|
|
expect(customFieldChangelog).toStrictEqual(new Map([['Video', 'AV']]));
|
|
await editCustomField('AV', { label: 'Video' });
|
|
vi.runAllTimers();
|
|
expect(customFieldChangelog).toStrictEqual(new Map());
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe('removeCustomField()', () => {
|
|
it('deletes a field with a given label', async () => {
|
|
const expected = {
|
|
Lighting: {
|
|
label: 'Lighting',
|
|
type: 'string',
|
|
colour: 'blue',
|
|
},
|
|
Video: {
|
|
label: 'Video',
|
|
type: 'string',
|
|
colour: 'red',
|
|
},
|
|
};
|
|
|
|
const customField = await removeCustomField('Sound');
|
|
|
|
expect(customField).toStrictEqual(expected);
|
|
});
|
|
});
|
|
});
|