Files
ontime/apps/server/src/api-data/excel/__tests__/excel.parser.test.ts
T
Carlos Valente 1849b4d39f Deps migration (#1988)
* chore: migrate eslint to oxlint

* chore: migrate prettier to oxfmt

* chore: migrate typescript

* chore: toThrow should have a expected value

* chore: cast test value as Day

* chore: small title fix

* chore: mocks should be hoisted

* chore: incorrect async useage

* chore: test should be inside description

* chore: test sohuld include an expeced

* chore: oxfmt

---------

Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
2026-03-08 16:22:12 +01:00

521 lines
15 KiB
TypeScript

import { CustomFields, OntimeEvent, OntimeGroup, SupportedEntry, TimerType } from 'ontime-types';
import { ImportMap, MILLIS_PER_MINUTE, millisToString } from 'ontime-utils';
import { simpleTemplate } from '../__mocks__/simpleTemplate.js';
import { parseExcel } from '../excel.parser.js';
import { dataFromExcelTemplate } from './mockData.js';
describe('parseExcel()', () => {
it('parses the simple template file', () => {
const result = parseExcel(simpleTemplate, {});
expect(result.rundown.flatOrder.length).toBe(14);
expect(result.rundown.order.length).toBe(4);
const firstEventId = result.rundown.flatOrder[2];
const firstEvent = result.rundown.entries[firstEventId];
expect(firstEvent).toMatchObject({
id: firstEventId,
type: SupportedEntry.Event,
title: 'Pre-show Countdown',
});
expect(millisToString((firstEvent as OntimeEvent).timeStart)).toBe('10:00:00');
const lastEventId = result.rundown.flatOrder[12];
const lastEvent = result.rundown.entries[lastEventId];
expect(lastEvent).toMatchObject({
id: lastEventId,
type: SupportedEntry.Event,
title: 'Wrap up',
});
// the excel step does not relate elements yet
expect(millisToString((lastEvent as OntimeEvent).timeEnd)).toBe('00:00:00');
});
it('parses an import map with only custom fields', () => {
// partial import map with only custom fields
const importMap = {
custom: {
user0: 't0',
user1: 'Test1',
user2: 'test2',
user3: 'test3',
},
};
const existingCustomFields: CustomFields = {
user0: { type: 'text', colour: 'red', label: 'user0' },
user1: { type: 'text', colour: 'green', label: 'user1' },
user2: { type: 'text', colour: 'blue', label: 'user2' },
};
const parsedData = parseExcel(dataFromExcelTemplate, existingCustomFields, 'testSheet', importMap);
expect(parsedData.customFields).toStrictEqual({
user0: {
type: 'text',
colour: 'red',
label: 'user0',
},
user1: {
type: 'text',
colour: 'green',
label: 'user1',
},
user2: {
type: 'text',
colour: 'blue',
label: 'user2',
},
user3: {
type: 'text',
colour: '',
label: 'user3',
},
});
expect(parsedData.rundown.order.length).toBe(2);
// TODO: why dont we parse the date in UTC?
expect(parsedData.rundown.entries).toMatchObject({
'event-a': {
id: 'event-a',
//timeStart: 28800000,
//timeEnd: 32410000,
title: 'Guest Welcome',
timerType: 'count-down',
endAction: 'none',
skip: false,
note: 'Ballyhoo',
custom: {
user0: 'a0',
user1: 'a1',
user2: 'a2',
user3: 'a3',
},
colour: 'red',
type: 'event',
cue: '101',
},
'event-b': {
id: 'event-b',
//timeStart: 32400000,
//timeEnd: 34200000,
title: 'A song from the hearth',
timerType: 'clock',
endAction: 'load-next',
skip: true,
note: 'Rainbow chase',
custom: {},
colour: '#F00',
type: 'event',
cue: '102',
},
});
});
it('parses a file without custom fields', () => {
// partial import map with only custom fields
const importMap = {
custom: {
niu1: 'niu1',
niu2: 'niu2',
},
};
const parsedData = parseExcel(dataFromExcelTemplate, {}, 'testSheet', importMap);
expect(parsedData.customFields).toStrictEqual({
niu1: {
type: 'text',
colour: '',
label: 'niu1',
},
niu2: {
type: 'text',
colour: '',
label: 'niu2',
},
});
expect(parsedData.rundown.order).toMatchObject(['event-a', 'event-b']);
expect(parsedData.rundown.entries['event-a']).toMatchObject({
//timeStart: 28800000,
//timeEnd: 32410000,
id: 'event-a',
title: 'Guest Welcome',
timerType: 'count-down',
endAction: 'none',
skip: false,
note: 'Ballyhoo',
custom: {},
colour: 'red',
type: 'event',
cue: '101',
});
expect(parsedData.rundown.entries['event-b']).toMatchObject({
//timeStart: 32400000,
//timeEnd: 34200000,
id: 'event-b',
title: 'A song from the hearth',
timerType: 'clock',
endAction: 'load-next',
skip: true,
note: 'Rainbow chase',
custom: {},
colour: '#F00',
type: 'event',
cue: '102',
});
});
it('ignores unknown event types', () => {
const testdata = [
['Title', 'Timer type'],
['Guest Welcome', 'x'],
['A song from the hearth', 'clock'],
];
const importMap = {
title: 'title',
timerType: 'timer type',
};
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstEvent = result.rundown.entries[result.rundown.order[0]];
expect(result.rundown.order.length).toBe(1);
expect((firstEvent as OntimeEvent).title).toBe('A song from the hearth');
});
it('imports group', () => {
const testdata = [
['Title', 'Timer type', 'duration'],
['a group', 'group', '10m'],
['an event', 'clock', '1m'],
];
const importMap = {
title: 'title',
timerType: 'timer type',
duration: 'duration',
} as ImportMap;
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstGroup = result.rundown.entries[result.rundown.order[0]];
expect(result.rundown.order.length).toBe(1);
expect(result.rundown.flatOrder.length).toBe(2);
expect((firstGroup as OntimeGroup).type).toBe(SupportedEntry.Group);
expect((firstGroup as OntimeGroup).targetDuration).toBe(10 * MILLIS_PER_MINUTE);
});
it('places event between groups inside the group', () => {
const testdata = [
['Title', 'Timer type'],
['a group', 'group'],
['an event', 'clock'],
['an event', 'clock'],
['an event', 'clock'],
['a second group ', 'group'],
['an event', 'clock'],
['an event', 'clock'],
];
const importMap = {
title: 'title',
timerType: 'timer type',
};
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstGroup = result.rundown.entries[result.rundown.order[0]] as OntimeGroup;
const secondGroup = result.rundown.entries[result.rundown.order[1]] as OntimeGroup;
expect(result.rundown.order.length).toBe(2);
expect(result.rundown.flatOrder.length).toBe(7);
expect(firstGroup.type).toBe(SupportedEntry.Group);
expect(firstGroup.entries.length).toBe(3);
expect(secondGroup.type).toBe(SupportedEntry.Group);
expect(secondGroup.entries.length).toBe(2);
});
it('imports as events if there is no timer type column', () => {
const testdata = [['Title'], ['no timer type'], ['also no timer type']];
const importMap = {
title: 'title',
};
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstEvent = result.rundown.entries[result.rundown.order[0]];
const secondEvent = result.rundown.entries[result.rundown.order[1]];
expect(result.rundown.order.length).toBe(2);
expect(firstEvent).toMatchObject({
type: SupportedEntry.Event,
timerType: TimerType.CountDown,
});
expect(secondEvent).toMatchObject({
type: SupportedEntry.Event,
timerType: TimerType.CountDown,
});
});
it('imports as events if timer type is empty or has whitespace', () => {
const testdata = [
['Title', 'Timer type'],
['first', ' '],
['second', undefined],
['third', ' count-up '],
];
const importMap = {
title: 'title',
timerType: 'timer type',
};
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstEvent = result.rundown.entries[result.rundown.order[0]];
const secondEvent = result.rundown.entries[result.rundown.order[1]];
const thirdEvent = result.rundown.entries[result.rundown.order[2]];
expect(result.rundown.order.length).toBe(3);
expect(firstEvent).toMatchObject({ title: 'first', type: SupportedEntry.Event, timerType: TimerType.CountDown });
expect(secondEvent).toMatchObject({ title: 'second', type: SupportedEntry.Event, timerType: TimerType.CountDown });
expect(thirdEvent).toMatchObject({ title: 'third', type: SupportedEntry.Event, timerType: TimerType.CountUp });
});
it('am/pm conversion to 24h', () => {
const testData = [
['Time Start', 'Time End', 'ID'],
['4:30:00', '4:36:00', 'event-1'],
['9:45:00', '10:56:00', 'event-2'],
['16:30:00', '16:36:00', 'event-3'],
['21:45:00', '22:56:00', 'event-4'],
['4:30:00AM', '4:36:00AM', 'event-5'],
['9:45:00AM', '10:56:00AM', 'event-6'],
['4:30:00PM', '4:36:00PM', 'event-7'],
['9:45:00PM', '10:56:00PM', 'event-8'],
];
const importMap = {
timeStart: 'time start',
timeEnd: 'time end',
id: 'id',
};
const result = parseExcel(testData, {}, 'testSheet', importMap);
expect(result.rundown.order.length).toBe(8);
expect(result.rundown.entries['event-1']).toMatchObject({
timeStart: 16200000,
timeEnd: 16560000,
});
expect(result.rundown.entries['event-2']).toMatchObject({
timeStart: 35100000,
timeEnd: 39360000,
});
expect(result.rundown.entries['event-3']).toMatchObject({
timeStart: 59400000,
timeEnd: 59760000,
});
expect(result.rundown.entries['event-4']).toMatchObject({
timeStart: 78300000,
timeEnd: 82560000,
});
expect(result.rundown.entries['event-5']).toMatchObject({
timeStart: 16200000,
timeEnd: 16560000,
});
expect(result.rundown.entries['event-6']).toMatchObject({
timeStart: 35100000,
timeEnd: 39360000,
});
expect(result.rundown.entries['event-7']).toMatchObject({
timeStart: 59400000,
timeEnd: 59760000,
});
expect(result.rundown.entries['event-8']).toMatchObject({
timeStart: 78300000,
timeEnd: 82560000,
});
});
it('handle leading and trailing whitespace', () => {
const testData = [
[' ID', ' title ', 'Colour '], // <--- leading and trailing white space
['event-a', 'title', '#F00'],
];
const importMap = {
id: 'id',
title: ' title', // <--- leading white space
colour: 'colour ', // <--- trailing white space
};
const result = parseExcel(testData, {}, 'testSheet', importMap);
expect(result.rundown.order.length).toBe(1);
expect(result.rundown.entries['event-a']).toMatchObject({
colour: '#F00',
id: 'event-a',
title: 'title',
});
});
it('parses link start', () => {
const testData = [
['Time Start', 'Time End', 'ID', 'Link Start', 'Timer type'],
['4:30:00', '9:45:00', 'A', '', 'count-down'],
['9:45:00', '10:56:00', 'B', 'x', 'count-down'],
['10:00:00', '16:36:00', 'C', 'x', 'count-down'],
['21:45:00', '22:56:00', 'D', '', 'count-down'],
['', '', 'GROUP', 'x', 'group'], // <-- group with link
['00:0:00', '23:56:00', 'E', 'x', 'count-down'], // <-- must link past previous group
];
const importMap = {
timeStart: 'time start',
timeEnd: 'time end',
linkStart: 'link start',
id: 'id',
timerType: 'timer type',
};
const result = parseExcel(testData, {}, 'testSheet', importMap);
expect(result.rundown.order.length).toBe(5);
expect(result.rundown.order).toMatchObject(['A', 'B', 'C', 'D', 'GROUP']);
expect(result.rundown.flatOrder.length).toBe(6);
expect(result.rundown.flatOrder).toMatchObject(['A', 'B', 'C', 'D', 'GROUP', 'E']);
expect(result.rundown.entries).toMatchObject({
A: {
linkStart: false,
},
B: {
linkStart: true,
},
C: {
linkStart: true,
},
D: {
linkStart: false,
},
GROUP: {
type: SupportedEntry.Group,
},
E: {
linkStart: true,
},
});
});
it('#971 BUG: parses time fields and booleans', () => {
const testData = [
[
'ID',
'Time Start',
'Time End',
'Duration',
'Link Start',
'Timer Type',
'End Action',
'Warning time',
'Danger time',
],
[
'SETUP',
'1899-12-30T07:15:00.000Z',
'1899-12-30T08:30:00.000Z',
'',
'false',
'count-down',
'none',
'15',
'00:05:00',
],
[
'MEET1',
'1899-12-30T08:30:00.000Z',
'1899-12-30T10:00:00.000Z',
'',
'false',
'count-down',
'none',
15,
'00:05:00',
],
['MEET2', '1899-12-30T10:00:00.000Z', '', '60', 'false', 'count-down', 'none', '13', '5'],
['lunch', '', '1899-12-30T11:30:00.000Z', '', 'true', 'count-down', 'none', 13, 5],
['MEET3', '1899-12-30T11:30:00.000Z', '', 90, false, 'count-up', 'none', '11', 5],
['MEET4', '', '', 30, true, 'count-up', 'none', 11, '00:05:00'],
];
const parsedData = parseExcel(testData, {}, 'bug-report');
// '15' as a string is parsed by smart time entry as minutes
expect(parsedData.rundown.entries['SETUP']).toMatchObject({
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// elements in bug report
// 15 is a number, in which case we parse it as a minutes value
expect(parsedData.rundown.entries['MEET1']).toMatchObject({
timeWarning: 15 * MILLIS_PER_MINUTE,
});
// in the case where a string is passed, we need to check whether it is an ISO 8601 date
expect(parsedData.rundown.entries['MEET2']).toMatchObject({
duration: 60 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect(parsedData.rundown.entries['lunch']).toMatchObject({
timeWarning: 13 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect(parsedData.rundown.entries['MEET3']).toMatchObject({
duration: 90 * MILLIS_PER_MINUTE,
linkStart: false,
timeWarning: 11 * MILLIS_PER_MINUTE,
timeDanger: 5 * MILLIS_PER_MINUTE,
});
expect(parsedData.rundown.entries['MEET4']).toMatchObject({
duration: 30 * MILLIS_PER_MINUTE,
timeWarning: 11 * MILLIS_PER_MINUTE,
linkStart: true,
});
});
it('handles milestones', () => {
const testdata = [
['Title', 'type', 'notes'],
['event...', 'count-down', ''],
['also event...', 'count-down', ''],
['this i a milestone', 'milestone', 'milestone note'],
];
const importMap = {
title: 'title',
timerType: 'type',
note: 'notes',
} as ImportMap;
const result = parseExcel(testdata, {}, 'testSheet', importMap);
const firstEvent = result.rundown.entries[result.rundown.order[0]];
const secondEvent = result.rundown.entries[result.rundown.order[1]];
const milestone = result.rundown.entries[result.rundown.order[2]];
expect(result.rundown.order.length).toBe(3);
expect(firstEvent).toMatchObject({
type: SupportedEntry.Event,
timerType: TimerType.CountDown,
});
expect(secondEvent).toMatchObject({
type: SupportedEntry.Event,
timerType: TimerType.CountDown,
});
expect(milestone).toMatchObject({
type: SupportedEntry.Milestone,
title: 'this i a milestone',
note: 'milestone note',
});
});
});