Files
ontime/apps/server/src/utils/__tests__/parser.test.ts
T
2023-09-02 14:53:58 +02:00

852 lines
20 KiB
TypeScript

import { vi } from 'vitest';
import { EndAction, TimerType } from 'ontime-types';
import { dbModel } from '../../models/dataModel.js';
import { parseExcel, parseJson, validateEvent } from '../parser.js';
import { makeString } from '../parserUtils.js';
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
describe('test json parser with valid def', () => {
const testData = {
rundown: [
{
title: 'Guest Welcoming',
subtitle: '',
presenter: '',
note: '',
timeStart: 31500000,
timeEnd: 32400000,
timeType: 'start-end',
duration: 32400000 - 31500000,
isPublic: false,
endAction: 'play-next',
timerType: 'clock',
colour: '',
type: 'event',
revision: 0,
id: '4b31',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
{
title: 'Good Morning',
subtitle: 'Days schedule',
presenter: 'Carlos Valente',
note: '',
timeStart: 32400000,
timeEnd: 36000000,
timeType: 'start-end',
duration: 36000000 - 32400000,
isPublic: true,
endAction: 'play-next',
timerType: 'count-up',
skip: true,
colour: 'red',
type: 'event',
revision: 0,
id: 'f24d',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
{
title: 'Stage 2 setup',
subtitle: '',
presenter: '',
note: '',
timeStart: 32400000,
timeEnd: 37200000,
timeType: 'start-end',
duration: 37200000 - 32400000,
isPublic: false,
endAction: 'wrong action',
timerType: 'clock',
colour: '',
type: 'event',
revision: 0,
id: 'bbc5',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
{
title: 'Working Procedures',
subtitle: '',
presenter: 'Filip Johansen',
note: '',
timeStart: 37200000,
timeEnd: 39000000,
timeType: 'start-end',
duration: 39000000 - 37200000,
isPublic: true,
endAction: 'none',
timerType: 'clock',
skip: false,
colour: '',
type: 'event',
revision: 0,
id: '5b3e',
},
{
title: 'Lunch',
subtitle: '',
presenter: '',
note: '',
timeStart: 39600000,
timeEnd: 45000000,
timeType: 'start-end',
duration: 37200000 - 32400000,
isPublic: false,
endAction: 'none',
timerType: 'clock',
colour: '',
type: 'event',
revision: 0,
id: '8e2c',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
{
title: 'A day being carlos',
subtitle: 'My life in a song',
presenter: 'Carlos Valente',
note: '',
timeStart: 46800000,
timeEnd: 50400000,
timeType: 'start-end',
duration: 37200000 - 32400000,
isPublic: true,
endAction: 'none',
timerType: 'clock',
colour: '',
type: 'event',
revision: 0,
id: '08e9',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
{
title: 'Hamburgers and Cheese',
subtitle: '... and other life questions',
presenter: 'Filip Johansen',
note: '',
timeStart: 54000000,
timeEnd: 57600000,
timeType: 'start-end',
duration: 37200000 - 32400000,
isPublic: true,
endAction: 'none',
timerType: 'clock',
colour: '',
type: 'event',
revision: 0,
id: 'e25a',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
},
],
project: {
title: 'This is a test definition',
url: 'www.carlosvalente.com',
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
},
settings: {
app: 'ontime',
version: 2,
timeFormat: '24',
},
viewSettings: {},
};
let parseResponse;
beforeEach(async () => {
parseResponse = await parseJson(testData);
});
it('has 7 events', () => {
const length = parseResponse?.rundown.length;
expect(length).toBe(7);
});
it('first event is as a match', () => {
const first = parseResponse?.rundown[0];
const expected = {
title: 'Guest Welcoming',
type: 'event',
id: '4b31',
};
expect(first).toMatchObject(expected);
});
it('second event is as a match', () => {
const second = parseResponse?.rundown[1];
const expected = {
title: 'Good Morning',
type: 'event',
id: 'f24d',
};
expect(second).toMatchObject(expected);
});
it('third event end action is set as the default value', () => {
const third = parseResponse?.rundown[2];
expect(third.endAction).toStrictEqual(EndAction.None);
});
it('fourth event timer type is set as the default value', () => {
const fourth = parseResponse?.rundown[3];
expect(fourth.timerType).toStrictEqual(TimerType.Clock);
});
it('loaded event settings', () => {
const eventTitle = parseResponse?.project?.title;
expect(eventTitle).toBe('This is a test definition');
});
it('endMessage to exist but be empty', () => {
const endMessage = parseResponse?.viewSettings?.endMessage;
expect(endMessage).toBeDefined();
expect(endMessage).toBe('');
});
it('settings are for right app and version', () => {
const settings = parseResponse?.settings;
expect(settings.app).toBe('ontime');
expect(settings.version).toBe(2);
});
it('missing settings', () => {
const settings = parseResponse?.settings;
expect(settings.osc_port).toBeUndefined();
});
});
describe('test parser edge cases', () => {
it('generates missing ids', async () => {
const testData = {
rundown: [
{
title: 'Test Event',
type: 'event',
},
],
};
const parseResponse = await parseJson(testData);
expect(parseResponse.rundown[0].id).toBeDefined();
});
it('detects duplicate Ids', async () => {
console.log = vi.fn();
const testData = {
rundown: [
{
title: 'Test Event 1',
type: 'event',
id: '1',
},
{
title: 'Test Event 2',
type: 'event',
id: '1',
},
],
};
const parseResponse = await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: ID collision on import, skipping');
expect(parseResponse?.rundown.length).toBe(1);
});
it('handles incomplete datasets', async () => {
console.log = vi.fn();
const testData = {
rundown: [
{
title: 'Test Event 1',
id: '1',
},
{
title: 'Test Event 2',
id: '1',
},
],
};
const parseResponse = await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: undefined event type, skipping');
expect(parseResponse?.rundown.length).toBe(0);
});
it('skips unknown app and version settings', async () => {
console.log = vi.fn();
const testData = {
settings: {
osc_port: 8888,
},
};
await parseJson(testData);
expect(console.log).toHaveBeenCalledWith('ERROR: unknown app version, skipping');
});
});
describe('test corrupt data', () => {
it('handles some empty events', async () => {
const emptyEvents = {
rundown: [
{},
{},
{},
{
title: 'Test Event 1',
type: 'event',
id: '1',
},
{
title: 'Test Event 2',
type: 'event',
id: '2',
},
{},
{},
{},
],
event: {
title: 'All about Carlos demo event',
url: 'www.carlosvalente.com',
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
endMessage: '',
},
settings: {
app: 'ontime',
version: 2,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
const parsedDef = await parseJson(emptyEvents);
expect(parsedDef.rundown.length).toBe(2);
});
it('handles all empty events', async () => {
const emptyEvents = {
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
event: {
title: 'All about Carlos demo event',
url: 'www.carlosvalente.com',
publicInfo: 'WiFi: demoproject \nPassword: ontimeproject',
backstageInfo: 'WiFi: demobackstage\nPassword: ontimeproject',
endMessage: '',
},
settings: {
app: 'ontime',
version: 2,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
const parsedDef = await parseJson(emptyEvents);
expect(parsedDef.rundown.length).toBe(0);
});
it('handles missing project data', async () => {
const emptyProjectData = {
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
project: {},
settings: {
app: 'ontime',
version: 2,
serverPort: 4001,
lock: null,
timeFormat: '24',
},
};
const parsedDef = await parseJson(emptyProjectData);
expect(parsedDef.project).toStrictEqual(dbModel.project);
});
it('handles missing settings', async () => {
const missingSettings = {
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
event: {},
settings: {
app: 'ontime',
version: 2,
},
};
const parsedDef = await parseJson(missingSettings);
expect(parsedDef.settings).toStrictEqual(dbModel.settings);
});
it('fails with invalid JSON', async () => {
const invalidJSON = 'some random dataset';
const parsedDef = await parseJson(invalidJSON);
expect(parsedDef).toBeNull();
});
});
describe('test event validator', () => {
it('validates a good object', () => {
const event = {
title: 'test',
};
const validated = validateEvent(event, 'test');
expect(validated).toEqual(
expect.objectContaining({
title: expect.any(String),
subtitle: expect.any(String),
presenter: expect.any(String),
note: expect.any(String),
timeStart: expect.any(Number),
timeEnd: expect.any(Number),
isPublic: expect.any(Boolean),
skip: expect.any(Boolean),
revision: expect.any(Number),
type: expect.any(String),
id: expect.any(String),
cue: 'test',
colour: expect.any(String),
user0: expect.any(String),
user1: expect.any(String),
user2: expect.any(String),
user3: expect.any(String),
user4: expect.any(String),
user5: expect.any(String),
user6: expect.any(String),
user7: expect.any(String),
user8: expect.any(String),
user9: expect.any(String),
}),
);
});
it('fails an empty object', () => {
const event = {};
const validated = validateEvent(event, 'none');
expect(validated).toEqual(null);
});
it('makes objects strings', () => {
const event = {
title: 2,
subtitle: true,
presenter: 3.2,
note: '1899-12-30T08:00:10.000Z',
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event, 'not-used');
expect(typeof validated.title).toEqual('string');
expect(typeof validated.subtitle).toEqual('string');
expect(typeof validated.presenter).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 = validateEvent(event);
expect(typeof validated.timeStart).toEqual('number');
expect(validated.timeStart).toEqual(0);
expect(typeof validated.timeEnd).toEqual('number');
expect(validated.timeEnd).toEqual(0);
});
it('handles bad objects', () => {
const event = {
title: {},
};
// @ts-expect-error -- we know this is wrong, testing imports outside domain
const validated = validateEvent(event);
expect(typeof validated.title).toEqual('string');
});
});
describe('test makeString function', () => {
it('converts variables to string', () => {
let val = 2;
let expected = '2';
let converted = makeString(val);
expect(converted).toBe(expected);
val = 2.22222222;
expected = '2.22222222';
converted = makeString(val);
expect(converted).toBe(expected);
// @ts-expect-error -- we know this is wrong, testing imports outside domain
val = ['testing'];
expected = 'testing';
converted = makeString(val);
expect(converted).toBe(expected);
// @ts-expect-error -- we know this is wrong, testing imports outside domain
val = { doing: 'testing' };
converted = makeString(val, 'fallback');
expect(converted).toBe('fallback');
});
});
describe('test parseExcel function', () => {
it('parses the example file', async () => {
const testdata = [
['Ontime ┬À Schedule Template'],
[],
['Event Name', 'Test Event'],
['Public URL', 'www.public.com'],
['Backstage URL', 'www.backstage.com'],
['Public Info', 'test public info'],
['Backstage Info', 'test backstage info'],
[],
[],
[
'Time Start',
'Time End',
'Event Title',
'Presenter Name',
'Event Subtitle',
'End Action',
'Timer type',
'Is Public? (x)',
'Skip? (x)',
'Notes',
'User0:test0',
'User1:test1',
'User2:test2',
'User3:test3',
'User4:test4',
'User5:test5',
'User6:test6',
'user7:test7',
'user8:test8',
'user9:test9',
'Colour',
],
[
'1899-12-30T07:00:00.000Z',
'1899-12-30T08:00:10.000Z',
'Guest Welcome',
'Carlos',
'Getting things started',
'',
'',
'x',
'',
'Ballyhoo',
'a0',
'a1',
'a2',
'a3',
'a4',
'a5',
'a6',
'a7',
'a8',
'a9',
'red',
],
[
'1899-12-30T08:00:00.000Z',
'1899-12-30T08:30:00.000Z',
'A song from the hearth',
'Still Carlos',
'Derailing early',
'load-next',
'clock',
'',
'x',
'Rainbow chase',
'b0',
'',
'',
'',
'',
'b5',
'',
'',
'',
'',
'#F00',
],
[],
];
const expectedParsedProjectData = {
title: 'Test Event',
publicUrl: 'www.public.com',
backstageUrl: 'www.backstage.com',
publicInfo: 'test public info',
backstageInfo: 'test backstage info',
};
// TODO: update tests once import is resolved
const expectedParsedRundown = [
{
//timeStart: 28800000,
//timeEnd: 32410000,
title: 'Guest Welcome',
presenter: 'Carlos',
subtitle: 'Getting things started',
timerType: 'count-down',
endAction: 'none',
isPublic: true,
skip: false,
note: 'Ballyhoo',
user0: 'a0',
user1: 'a1',
user2: 'a2',
user3: 'a3',
user4: 'a4',
user5: 'a5',
user6: 'a6',
user7: 'a7',
user8: 'a8',
user9: 'a9',
colour: 'red',
type: 'event',
},
{
//timeStart: 32400000,
//timeEnd: 34200000,
title: 'A song from the hearth',
presenter: 'Still Carlos',
subtitle: 'Derailing early',
timerType: 'clock',
endAction: 'load-next',
isPublic: false,
skip: true,
note: 'Rainbow chase',
user0: 'b0',
user5: 'b5',
colour: '#F00',
type: 'event',
},
];
const parsedData = await parseExcel(testdata);
expect(parsedData.project).toStrictEqual(expectedParsedProjectData);
expect(parsedData.rundown).toBeDefined();
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
});
});
describe('test aliases import', () => {
it('imports a well defined alias', () => {
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
aliases: [
{
enabled: false,
alias: 'testalias',
pathAndParams: 'testpathAndParams',
},
],
};
const parsed = parseAliases(testData);
expect(parsed.length).toBe(1);
// generates missing id
expect(parsed[0].alias).toBeDefined();
});
});
describe('test userFields import', () => {
const model = dbModel.userFields;
it('imports a fully defined user fields', () => {
const testUserFields = {
user0: 'test0',
user1: 'test1',
user2: 'test2',
user3: 'test3',
user4: 'test4',
user5: 'test5',
user6: 'test6',
user7: 'test7',
user8: 'test8',
user9: 'test9',
};
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
userFields: testUserFields,
};
const parsed = parseUserFields(testData);
expect(parsed).toStrictEqual(testUserFields);
});
it('imports a partially defined user fields', () => {
const testUserFields = {
user0: 'test0',
user1: 'test1',
user7: 'test7',
user8: 'test8',
user9: 'test9',
};
const expected = {
...model,
...testUserFields,
};
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
userFields: testUserFields,
};
const parsed = parseUserFields(testData);
expect(parsed).toStrictEqual(expected);
});
it('handles missing user fields', () => {
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
};
const parsed = parseUserFields(testData);
expect(parsed).toStrictEqual(model);
expect(parsed).toStrictEqual(model);
});
it('ignores badly defined fields', () => {
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
userFields: {
notThis: 'this shouldng be accepted',
orThis: 'this neither',
},
};
const parsed = parseUserFields(testData);
expect(parsed).toStrictEqual(model);
});
});
describe('test views import', () => {
it('imports data from file', () => {
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
viewSettings: {},
views: {
overrideStyles: true,
},
};
const expectedParsedViewSettings = {
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
warningThreshold: 120000,
dangerColor: '#ED3333',
dangerThreshold: 60000,
endMessage: '',
overrideStyles: false,
};
const parsed = parseViewSettings(testData, false);
expect(parsed).toStrictEqual(expectedParsedViewSettings);
});
it('imports defaults to model', () => {
const testData = {
rundown: [],
settings: {
app: 'ontime',
version: 2,
},
};
const expectedParsedViewSettings = {
normalColor: '#ffffffcc',
warningColor: '#FFAB33',
warningThreshold: 120000,
dangerColor: '#ED3333',
dangerThreshold: 60000,
endMessage: '',
overrideStyles: false,
};
const parsed = parseViewSettings(testData, true);
expect(parsed).toStrictEqual(expectedParsedViewSettings);
});
});