mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 20:33:47 +00:00
62c8319d70
refactor: extract functions to api domain refactor: strict custom field parsing refactor: remove rundown cache utilities refactor: directory restructure
241 lines
7.6 KiB
TypeScript
241 lines
7.6 KiB
TypeScript
import { TimeStrategy, EndAction, TimerType, OntimeEvent } from 'ontime-types';
|
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
|
|
|
import { assertType } from 'vitest';
|
|
|
|
import { calculateDayOffset, createEvent, deleteById, doesInvalidateMetadata, getInsertAfterId, hasChanges } from '../rundown.utils.js';
|
|
import { makeRundown } from '../__mocks__/rundown.mocks.js';
|
|
|
|
describe('test event validator', () => {
|
|
it('validates a good object', () => {
|
|
const event = {
|
|
title: 'test',
|
|
};
|
|
const validated = createEvent(event, 1);
|
|
|
|
expect(validated).toEqual(
|
|
expect.objectContaining({
|
|
title: expect.any(String),
|
|
note: expect.any(String),
|
|
timeStart: expect.any(Number),
|
|
timeEnd: expect.any(Number),
|
|
countToEnd: expect.any(Boolean),
|
|
isPublic: expect.any(Boolean),
|
|
skip: expect.any(Boolean),
|
|
revision: expect.any(Number),
|
|
type: expect.any(String),
|
|
id: expect.any(String),
|
|
cue: '2',
|
|
colour: expect.any(String),
|
|
custom: expect.any(Object),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('fails an empty object', () => {
|
|
const event = {};
|
|
const validated = createEvent(event, 1);
|
|
expect(validated).toEqual(null);
|
|
});
|
|
|
|
it('makes objects strings', () => {
|
|
const event = {
|
|
title: 2,
|
|
note: '1899-12-30T08:00:10.000Z',
|
|
};
|
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
|
const validated = createEvent(event, 1);
|
|
if (validated === null) {
|
|
throw new Error('unexpected value');
|
|
}
|
|
expect(typeof validated.title).toEqual('string');
|
|
expect(typeof validated.note).toEqual('string');
|
|
});
|
|
|
|
it('enforces numbers on times', () => {
|
|
const event = {
|
|
timeStart: false,
|
|
timeEnd: '2',
|
|
};
|
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
|
const validated = createEvent(event);
|
|
if (validated === null) {
|
|
throw new Error('unexpected value');
|
|
}
|
|
assertType<number>(validated.timeStart);
|
|
assertType<number>(validated.timeEnd);
|
|
assertType<number>(validated.duration);
|
|
expect(validated.timeStart).toEqual(0);
|
|
expect(validated.timeEnd).toEqual(2);
|
|
expect(validated.duration).toEqual(2);
|
|
});
|
|
|
|
it('handles bad objects', () => {
|
|
const event = {
|
|
title: {},
|
|
};
|
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
|
const validated = createEvent(event);
|
|
if (validated === null) {
|
|
throw new Error('unexpected value');
|
|
}
|
|
expect(typeof validated.title).toEqual('string');
|
|
});
|
|
});
|
|
|
|
describe('doesInvalidateMetadata()', () => {
|
|
it('is stale if data contains timers', () => {
|
|
const needsRecompute = [
|
|
{ timeStart: 10 },
|
|
{ timeEnd: 10 },
|
|
{ duration: 10 },
|
|
{ linkStart: true },
|
|
{ timerStrategy: TimeStrategy.LockDuration },
|
|
];
|
|
|
|
for (const testCase of needsRecompute) {
|
|
expect(doesInvalidateMetadata(testCase)).toBe(true);
|
|
}
|
|
expect.assertions(needsRecompute.length);
|
|
});
|
|
|
|
it('is not stale if data contains auxiliary dataset', () => {
|
|
expect(
|
|
doesInvalidateMetadata({
|
|
cue: 'cue',
|
|
title: 'title',
|
|
note: 'note',
|
|
endAction: EndAction.LoadNext,
|
|
timerType: TimerType.Clock,
|
|
isPublic: false,
|
|
colour: 'colour',
|
|
timeWarning: 1,
|
|
timeDanger: 2,
|
|
custom: {
|
|
lighting: '3',
|
|
},
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('hasChanges()', () => {
|
|
it('identifies objects with new values', () => {
|
|
const newEvent = { id: '1', title: 'new-title' } as OntimeEvent;
|
|
const existing = { id: '1', cue: 'cue', title: 'title' } as OntimeEvent;
|
|
expect(hasChanges(existing, newEvent)).toBe(true);
|
|
});
|
|
it('identifies objects with all same values', () => {
|
|
const newEvent = { id: '1', title: 'title' } as OntimeEvent;
|
|
const existing = { id: '1', cue: 'cue', title: 'title' } as OntimeEvent;
|
|
expect(hasChanges(existing, newEvent)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deleteById()', () => {
|
|
it('should delete the first instance of the specified ID from the array', () => {
|
|
const array = ['id1', 'id2', 'id3', 'id4'];
|
|
const result = deleteById(array, 'id2');
|
|
expect(result).toStrictEqual(['id1', 'id3', 'id4']);
|
|
expect(result).not.toBe(array); // Ensure a new array is returned
|
|
});
|
|
|
|
it('should not modify the array if the specified ID does not exist', () => {
|
|
const array = ['id1', 'id2', 'id3', 'id4'];
|
|
const result = deleteById(array, 'id5');
|
|
expect(result).toStrictEqual(['id1', 'id2', 'id3', 'id4']);
|
|
});
|
|
|
|
it('should return the same array if it is empty', () => {
|
|
const array: string[] = [];
|
|
const result = deleteById(array, 'id1');
|
|
expect(result).toStrictEqual([]);
|
|
});
|
|
|
|
it('should handle scenarios where the delete id is not found', () => {
|
|
const array = ['id1', 'id2', 'id3'];
|
|
const result = deleteById(array, 'id4');
|
|
expect(result).toStrictEqual(['id1', 'id2', 'id3']);
|
|
});
|
|
});
|
|
|
|
describe('calculateDayOffset()', () => {
|
|
it('returns 0 if there is no previous event', () => {
|
|
expect(calculateDayOffset({ timeStart: 0 }, null)).toBe(0);
|
|
});
|
|
|
|
it('returns 0 if the previous event duration is 0', () => {
|
|
expect(calculateDayOffset({ timeStart: 0 }, { timeStart: 0, duration: 0 })).toBe(0);
|
|
});
|
|
|
|
it('returns 0 if event starts after previous', () => {
|
|
expect(calculateDayOffset({ timeStart: 11 }, { timeStart: 10, duration: 2 })).toBe(0);
|
|
});
|
|
|
|
it('returns 1 if event starts before previous', () => {
|
|
expect(calculateDayOffset({ timeStart: 9 }, { timeStart: 10, duration: 2 })).toBe(1);
|
|
});
|
|
|
|
it('returns 1 if event starts at the same time as one before', () => {
|
|
expect(calculateDayOffset({ timeStart: 10 }, { timeStart: 10, duration: 2 })).toBe(1);
|
|
});
|
|
|
|
it('should account for an event that crossed midnight and there is a overlap', () => {
|
|
expect(
|
|
calculateDayOffset(
|
|
{ timeStart: MILLIS_PER_HOUR }, // starts at 01:00:00
|
|
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
|
),
|
|
).toBe(1);
|
|
});
|
|
|
|
it('should account for an event that crossed midnight and there is a gap', () => {
|
|
expect(
|
|
calculateDayOffset(
|
|
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 2 * MILLIS_PER_HOUR }, // ends at 01:00:00
|
|
),
|
|
).toBe(1);
|
|
});
|
|
|
|
it('should account for an event that crossed midnight with no overlaps or gaps', () => {
|
|
expect(
|
|
calculateDayOffset(
|
|
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
|
),
|
|
).toBe(1);
|
|
});
|
|
|
|
it('should account for an event that finishes exactly at midnight', () => {
|
|
expect(
|
|
calculateDayOffset(
|
|
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
|
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 24:00:00
|
|
),
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('getInsertAfterId()', () => {
|
|
const rundown = makeRundown({
|
|
flatOrder: ['a', 'b', 'c', 'd'],
|
|
});
|
|
|
|
it('returns afterId if provided', () => {
|
|
expect(getInsertAfterId(rundown, 'b')).toBe('b');
|
|
});
|
|
|
|
it('returns the previous id before beforeId if provided', () => {
|
|
expect(getInsertAfterId(rundown, undefined, 'c')).toBe('b');
|
|
});
|
|
|
|
it('returns undefined if neither afterId nor beforeId is provided', () => {
|
|
expect(getInsertAfterId(rundown)).toBeNull();
|
|
});
|
|
|
|
it('returns undefined if beforeId is not found', () => {
|
|
expect(getInsertAfterId(rundown, undefined, 'z')).toBeNull();
|
|
});
|
|
});
|