Files
ontime/packages/utils/src/rundown-utils/rundownUtils.test.ts
T
2026-02-14 08:56:16 +01:00

392 lines
14 KiB
TypeScript

import type { OntimeDelay, OntimeEntry, OntimeEvent, OntimeGroup, Rundown } from 'ontime-types';
import { SupportedEntry } from 'ontime-types';
import {
addToRundown,
getFirstGroupNormal,
getInsertAfterId,
getLastEvent,
getLastGroupNormal,
getLastNormal,
getNextEvent,
getNextGroupNormal,
getNextNormal,
getPreviousGroupNormal,
getPreviousNormal,
resolveInsertParent,
swapEventData,
} from './rundownUtils';
import { demoDb } from './rundownUtils.mock';
describe('getNextEvent()', () => {
it('returns the next event of type event', () => {
const testRundown = [
{ id: '1', type: SupportedEntry.Event } as OntimeEvent,
{ id: '2', type: SupportedEntry.Event } as OntimeEvent,
{ id: '3', type: SupportedEntry.Event } as OntimeEvent,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent?.id).toBe('2');
expect(nextIndex).toBe(1);
});
it('ignores other event types', () => {
const testRundown = [
{ id: '1', type: SupportedEntry.Event } as OntimeEvent,
{ id: '2', type: SupportedEntry.Delay } as OntimeDelay,
{ id: '3', type: SupportedEntry.Group } as OntimeGroup,
{ id: '4', type: SupportedEntry.Event } as OntimeEvent,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent?.id).toBe('4');
expect(nextIndex).toBe(3);
});
it('returns null if none found', () => {
const testRundown = [
{ id: '1', type: SupportedEntry.Event } as OntimeEvent,
{ id: '2', type: SupportedEntry.Delay } as OntimeDelay,
{ id: '3', type: SupportedEntry.Group } as OntimeGroup,
];
const { nextEvent, nextIndex } = getNextEvent(testRundown, '1');
expect(nextEvent).toBe(null);
expect(nextIndex).toBe(null);
});
});
describe('swapEventData', () => {
it('swaps some data between two events', () => {
const eventA = {
id: '1',
cue: 'A',
timeStart: 1,
timeEnd: 1,
duration: 1,
delay: 1,
revision: 3,
parent: null,
} as OntimeEvent;
const eventB = {
id: '2',
cue: 'B',
timeStart: 2,
timeEnd: 2,
duration: 2,
delay: 2,
revision: 7,
parent: 'testing',
} as OntimeEvent;
const [newA, newB] = swapEventData(eventA, eventB);
expect(newA).toMatchObject({
id: '1',
cue: 'B',
timeStart: 1,
timeEnd: 1,
duration: 1,
delay: 1,
revision: 3,
parent: null,
});
expect(newB).toMatchObject({
id: '2',
cue: 'A',
timeStart: 2,
timeEnd: 2,
duration: 2,
delay: 2,
revision: 7,
parent: 'testing',
});
});
});
describe('getNextNormal / getPreviousNormal (flat order)', () => {
const demoRundown = demoDb.rundowns.default;
it('steps forward using flat order', () => {
const { entry, index } = getNextNormal(demoRundown.entries, demoRundown.flatOrder, '7eaf99');
expect(entry?.id).toBe('9bf60f');
expect(index).toBe(2);
});
it('steps backward using flat order', () => {
const { entry, index } = getPreviousNormal(demoRundown.entries, demoRundown.flatOrder, '9bf60f');
expect(entry?.id).toBe('7eaf99');
expect(index).toBe(1);
});
it('uses start/end boundaries for null cursor', () => {
const next = getNextNormal(demoRundown.entries, demoRundown.flatOrder, null);
const previous = getPreviousNormal(demoRundown.entries, demoRundown.flatOrder, null);
expect(next.entry?.id).toBe('e2163f');
expect(next.index).toBe(0);
expect(previous.entry?.id).toBe('07df89');
expect(previous.index).toBe(demoRundown.flatOrder.length - 1);
});
});
describe('getNextGroupNormal / getPreviousGroupNormal (flat order)', () => {
const demoRundown = demoDb.rundowns.default;
it('finds the next group from inside a group', () => {
const { entry, index } = getNextGroupNormal(demoRundown.entries, demoRundown.flatOrder, '9bf60f');
expect(entry?.id).toBe('f60403');
expect(index).toBe(7);
});
it('finds the previous group from inside a group', () => {
const { entry, index } = getPreviousGroupNormal(demoRundown.entries, demoRundown.flatOrder, '9bf60f');
expect(entry?.id).toBe('7eaf99');
expect(index).toBe(1);
});
it('uses start/end boundaries for null cursor', () => {
const next = getNextGroupNormal(demoRundown.entries, demoRundown.flatOrder, null);
const previous = getPreviousGroupNormal(demoRundown.entries, demoRundown.flatOrder, null);
expect(next.entry?.id).toBe('7eaf99');
expect(next.index).toBe(1);
expect(previous.entry?.id).toBe('6b0edb');
expect(previous.index).toBe(9);
});
});
describe('getFirstGroupNormal / getLastGroupNormal (flat order)', () => {
const demoRundown = demoDb.rundowns.default;
it('finds the first group in flat order', () => {
const { entry, index } = getFirstGroupNormal(demoRundown.entries, demoRundown.flatOrder);
expect(entry?.id).toBe('7eaf99');
expect(index).toBe(1);
});
it('finds the last group in flat order', () => {
const { entry, index } = getLastGroupNormal(demoRundown.entries, demoRundown.flatOrder);
expect(entry?.id).toBe('6b0edb');
expect(index).toBe(9);
});
});
describe('getLastEvent', () => {
it('returns the last event of type event', () => {
const testRundown: OntimeEntry[] = [
{ id: '1', type: SupportedEntry.Event } as OntimeEvent,
{ id: '2', type: SupportedEntry.Delay } as OntimeDelay,
{ id: '3', type: SupportedEntry.Event } as OntimeEvent,
{ id: '4', type: SupportedEntry.Group } as OntimeGroup,
];
const { lastEvent } = getLastEvent(testRundown);
expect(lastEvent?.id).toBe('3');
});
it('handles rundowns with a single event', () => {
const testRundown: OntimeEntry[] = [{ id: '1', type: SupportedEntry.Event } as OntimeEvent];
const { lastEvent } = getLastEvent(testRundown);
expect(lastEvent?.id).toBe('1');
});
describe('getLastNormal', () => {
it('returns the last entry', () => {
const entries = {
4: { id: '4', type: SupportedEntry.Group } as OntimeGroup,
1: { id: '1', type: SupportedEntry.Event } as OntimeEvent,
3: { id: '3', type: SupportedEntry.Event } as OntimeEvent,
2: { id: '2', type: SupportedEntry.Delay } as OntimeDelay,
};
const order = ['1', '2', '3', '4'];
const lastEntry = getLastNormal(entries, order);
expect(lastEntry?.id).toBe('4');
});
it('handles rundowns with a single event', () => {
const entries = {
1: { id: '1', type: SupportedEntry.Event } as OntimeEvent,
};
const lastEvent = getLastNormal(entries, ['1']);
expect(lastEvent?.id).toBe('1');
});
it('handles empty order', () => {
const testRundown = {
1: { id: '1', type: SupportedEntry.Event } as OntimeEvent,
2: { id: '2', type: SupportedEntry.Event } as OntimeEvent,
};
const lastEntry = getLastNormal(testRundown, []);
expect(lastEntry).toBe(null);
});
it('handles empty rundown', () => {
const lastEntry = getLastNormal({}, ['1', '2', '3', '4']);
expect(lastEntry).toBe(null);
});
});
});
describe('getInsertAfterId()', () => {
const rundown = {
id: 'test',
title: 'test',
entries: {
'1': { id: '1', type: SupportedEntry.Event, parent: null } as OntimeEvent,
'2': { id: '2', type: SupportedEntry.Event, parent: null } as OntimeEvent,
group: { id: 'group', type: SupportedEntry.Group, entries: ['31', '32'] } as unknown as OntimeGroup,
'31': { id: '31', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
'32': { id: '32', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
'4': { id: '4', type: SupportedEntry.Event, parent: null } as OntimeEvent,
},
order: ['1', '2', 'group', '4'],
flatOrder: ['1', '2', 'group', '31', '32', '4'],
revision: 1,
} as Rundown;
it('returns afterId if provided', () => {
expect(getInsertAfterId(rundown, null, 'b')).toBe('b');
});
it('returns null if neither afterId nor beforeId is provided', () => {
expect(getInsertAfterId(rundown, null)).toBeNull();
});
it('returns null if beforeId is not found', () => {
expect(getInsertAfterId(rundown, null, undefined, 'z')).toBeNull();
expect(getInsertAfterId(rundown, null, undefined, '1')).toBeNull();
});
it('returns the previous id of an entry in the rundown', () => {
expect(getInsertAfterId(rundown, null, undefined, '2')).toBe('1');
expect(getInsertAfterId(rundown, null, undefined, '4')).toBe('group');
expect(getInsertAfterId(rundown, null, undefined, 'group')).toBe('2');
});
it('returns the previous id of an event in a group', () => {
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '31')).toBeNull();
expect(getInsertAfterId(rundown, rundown.entries.group as OntimeGroup, undefined, '32')).toBe('31');
});
});
describe('resolveInsertParent()', () => {
const rundown = {
id: 'test',
title: 'test',
entries: {
top: { id: 'top', type: SupportedEntry.Event, parent: null } as OntimeEvent,
group: { id: 'group', type: SupportedEntry.Group, entries: ['31', '32'] } as unknown as OntimeGroup,
'31': { id: '31', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
'32': { id: '32', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
},
order: ['top', 'group'],
flatOrder: ['top', 'group', '31', '32'],
revision: 1,
} as Rundown;
it('returns explicit parent id', () => {
expect(resolveInsertParent(rundown, { parent: 'group' })).toBe('group');
});
it('returns non-existent explicit parent id as-is', () => {
expect(resolveInsertParent(rundown, { parent: 'missing' })).toBe('missing');
});
it('infers parent id from sibling when parent is omitted', () => {
expect(resolveInsertParent(rundown, { after: '31' })).toBe('group');
expect(resolveInsertParent(rundown, { before: '32' })).toBe('group');
});
it('returns null when no grouped sibling reference exists', () => {
expect(resolveInsertParent(rundown, { after: 'top' })).toBeNull();
});
it('returns null when no references are provided', () => {
expect(resolveInsertParent(rundown, {})).toBeNull();
});
});
describe('addToRundown()', () => {
const makeTestRundown = (): Rundown =>
({
id: 'test',
title: 'test',
entries: {
'1': { id: '1', type: SupportedEntry.Event, parent: null } as OntimeEvent,
'2': { id: '2', type: SupportedEntry.Event, parent: null } as OntimeEvent,
group: { id: 'group', type: SupportedEntry.Group, entries: ['31', '32'] } as unknown as OntimeGroup,
'31': { id: '31', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
'32': { id: '32', type: SupportedEntry.Event, parent: 'group' } as OntimeEvent,
},
order: ['1', '2', 'group'],
flatOrder: ['1', '2', 'group', '31', '32'],
revision: 1,
}) as Rundown;
it('adds an entry to an empty rundown', () => {
const rundown = { id: 'test', title: '', entries: {}, order: [], flatOrder: [], revision: 0 } as Rundown;
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, null, null);
expect(rundown.order).toEqual(['new']);
expect(rundown.flatOrder).toEqual(['new']);
expect(rundown.entries['new']).toBe(newEntry);
});
// case 2b: insert at the beginning of the rundown
it('adds at the beginning of order and flatOrder when afterId is null', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, null, null);
expect(rundown.order).toEqual(['new', '1', '2', 'group']);
expect(rundown.flatOrder).toEqual(['new', '1', '2', 'group', '31', '32']);
});
// case 2a: insert after a given entry at top level
it('inserts after the referenced entry in both order and flatOrder', () => {
const rundown = makeTestRundown();
const newEntry = { id: 'new', type: SupportedEntry.Event } as OntimeEvent;
addToRundown(rundown, newEntry, '1', null);
expect(rundown.order).toEqual(['1', 'new', '2', 'group']);
expect(rundown.flatOrder).toEqual(['1', 'new', '2', 'group', '31', '32']);
});
// case 1b: insert at the beginning of a group
it('inserts right after the group header in flatOrder and sets parent', () => {
const rundown = makeTestRundown();
const parent = rundown.entries['group'] as OntimeGroup;
const newEntry = { id: 'new', type: SupportedEntry.Event, parent: null } as OntimeEvent;
addToRundown(rundown, newEntry, null, parent);
expect(parent.entries).toEqual(['new', '31', '32']);
expect(newEntry.parent).toBe('group');
expect(rundown.flatOrder).toEqual(['1', '2', 'group', 'new', '31', '32']);
// top-level order must not change when inserting into a group
expect(rundown.order).toEqual(['1', '2', 'group']);
});
// case 1a: insert after a given entry within a group
it('inserts after the referenced entry within the group and in flatOrder', () => {
const rundown = makeTestRundown();
const parent = rundown.entries['group'] as OntimeGroup;
const newEntry = { id: 'new', type: SupportedEntry.Event, parent: null } as OntimeEvent;
addToRundown(rundown, newEntry, '31', parent);
expect(parent.entries).toEqual(['31', 'new', '32']);
expect(newEntry.parent).toBe('group');
expect(rundown.flatOrder).toEqual(['1', '2', 'group', '31', 'new', '32']);
expect(rundown.order).toEqual(['1', '2', 'group']);
});
});