mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b273ac5254 | |||
| 971a76673b |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.5.5",
|
"@chakra-ui/react": "^2.5.5",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
export const queryClientMock = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Exported viewer link location
|
|
||||||
const minimalLocation = 'minimal';
|
|
||||||
const speakerLocation = 'speaker';
|
|
||||||
const smLocation = 'sm';
|
|
||||||
const publicLocation = 'public';
|
|
||||||
const studioLocation = 'studio';
|
|
||||||
const cuesheetLocation = 'cuesheet';
|
|
||||||
const countdownLocation = 'countdown';
|
|
||||||
const clockLocation = 'clock';
|
|
||||||
const lowerLocation = 'lower';
|
|
||||||
|
|
||||||
export const viewerLocations = [
|
|
||||||
{ link: speakerLocation, label: 'Stage timer' },
|
|
||||||
{ link: clockLocation, label: 'Clock' },
|
|
||||||
{ link: minimalLocation, label: 'Minimal timer' },
|
|
||||||
{ link: smLocation, label: 'Backstage screen' },
|
|
||||||
{ link: publicLocation, label: 'Public screen' },
|
|
||||||
{ link: lowerLocation, label: 'Lower thirds' },
|
|
||||||
{ link: studioLocation, label: 'Studio clock' },
|
|
||||||
{ link: countdownLocation, label: 'Countdown' },
|
|
||||||
{ link: cuesheetLocation, label: 'Cuesheet' },
|
|
||||||
];
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Button } from '@chakra-ui/react';
|
|
||||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
|
||||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default function EnableBtn(props) {
|
|
||||||
const { active, text, actionHandler, size = 'xs' } = props;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size={size}
|
|
||||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
|
||||||
colorScheme='blue'
|
|
||||||
variant={active ? 'solid' : 'outline'}
|
|
||||||
onClick={actionHandler}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnableBtn.propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
text: PropTypes.string,
|
|
||||||
actionHandler: PropTypes.func,
|
|
||||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
|
||||||
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
interface PauseIconBtnProps {
|
|
||||||
clickhandler: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
|
||||||
active: boolean;
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PauseIconBtn(props: PauseIconBtnProps) {
|
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
|
||||||
<IconButton
|
|
||||||
icon={<IoPause size='24px' />}
|
|
||||||
colorScheme='orange'
|
|
||||||
variant={active ? 'solid' : 'outline'}
|
|
||||||
onClick={clickhandler}
|
|
||||||
width={120}
|
|
||||||
disabled={disabled}
|
|
||||||
aria-label='Pause playback'
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default function PublicIconBtn(props) {
|
|
||||||
const { actionHandler, active, size = 'xs', ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
|
||||||
<IconButton
|
|
||||||
size={size}
|
|
||||||
icon={<FiUsers />}
|
|
||||||
colorScheme='blue'
|
|
||||||
variant={active ? 'solid' : 'outline'}
|
|
||||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PublicIconBtn.propTypes = {
|
|
||||||
actionHandler: PropTypes.func,
|
|
||||||
active: PropTypes.bool,
|
|
||||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
export default function RollIconBtn(props) {
|
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
|
||||||
<IconButton
|
|
||||||
icon={<IoTimeOutline size='24px' />}
|
|
||||||
colorScheme='blue'
|
|
||||||
variant={active ? 'solid' : 'outline'}
|
|
||||||
onClick={clickhandler}
|
|
||||||
width={120}
|
|
||||||
disabled={disabled}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RollIconBtn.propTypes = {
|
|
||||||
clickhandler: PropTypes.func,
|
|
||||||
active: PropTypes.bool,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
export default function StartIconBtn(props) {
|
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
|
||||||
<IconButton
|
|
||||||
icon={<IoPlay size='24px' />}
|
|
||||||
colorScheme='green'
|
|
||||||
variant={active ? 'solid' : 'outline'}
|
|
||||||
onClick={clickhandler}
|
|
||||||
width={120}
|
|
||||||
disabled={disabled}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StartIconBtn.propTypes = {
|
|
||||||
clickhandler: PropTypes.func,
|
|
||||||
active: PropTypes.bool,
|
|
||||||
disabled: PropTypes.bool
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
export default function TransportIconBtn(props) {
|
|
||||||
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
|
||||||
<IconButton
|
|
||||||
icon={icon}
|
|
||||||
colorScheme='white'
|
|
||||||
variant='outline'
|
|
||||||
_hover={!disabled && { bg: '#ebedf0', color: '#333' }}
|
|
||||||
onClick={clickHandler}
|
|
||||||
width={90}
|
|
||||||
disabled={disabled}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TransportIconBtn.propTypes = {
|
|
||||||
clickHandler: PropTypes.func,
|
|
||||||
icon: PropTypes.element,
|
|
||||||
tooltip: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
export default function UnloadIconBtn(props) {
|
|
||||||
const { clickHandler, disabled, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
|
||||||
<IconButton
|
|
||||||
icon={<IoStop size='22px' />}
|
|
||||||
colorScheme='red'
|
|
||||||
variant='outline'
|
|
||||||
onClick={clickHandler}
|
|
||||||
width={90}
|
|
||||||
disabled={disabled}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UnloadIconBtn.propTypes = {
|
|
||||||
clickHandler: PropTypes.func,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Alias } from 'ontime-types';
|
|
||||||
|
|
||||||
export const aliasPlaceholder: Alias = {
|
|
||||||
enabled: false,
|
|
||||||
alias: '',
|
|
||||||
pathAndParams: '',
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
export const ontimeVars = [
|
|
||||||
{
|
|
||||||
name: '$timer',
|
|
||||||
description: 'Current running timer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$title',
|
|
||||||
description: 'Current title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$presenter',
|
|
||||||
description: 'Current timer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$subtitle',
|
|
||||||
description: 'Current subtitle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$next-title',
|
|
||||||
description: 'Next title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$next-presenter',
|
|
||||||
description: 'Next timer',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '$next-subtitle',
|
|
||||||
description: 'Next subtitle',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -33,7 +33,7 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string
|
|||||||
},
|
},
|
||||||
onUpdate: {
|
onUpdate: {
|
||||||
title: 'On Every Second',
|
title: 'On Every Second',
|
||||||
subtitle: 'Triggers when timers are updated (at least once a second, can be more)',
|
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
||||||
},
|
},
|
||||||
onFinish: {
|
onFinish: {
|
||||||
title: 'On Finish',
|
title: 'On Finish',
|
||||||
|
|||||||
@@ -2,10 +2,3 @@ export const inputProps = {
|
|||||||
size: 'sm',
|
size: 'sm',
|
||||||
autoComplete: 'off',
|
autoComplete: 'off',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const portInputProps = {
|
|
||||||
...inputProps,
|
|
||||||
type: 'number',
|
|
||||||
min: '1024',
|
|
||||||
max: '65535',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.0",
|
"body-parser": "^1.20.0",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { clock } from './Clock.js';
|
|||||||
|
|
||||||
export class TimerService {
|
export class TimerService {
|
||||||
private readonly _interval: NodeJS.Timer;
|
private readonly _interval: NodeJS.Timer;
|
||||||
|
private _updateInterval: number;
|
||||||
|
private _lastUpdate: number | null;
|
||||||
|
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
timer: TimerState;
|
timer: TimerState;
|
||||||
@@ -23,10 +25,12 @@ export class TimerService {
|
|||||||
* @constructor
|
* @constructor
|
||||||
* @param {object} [timerConfig]
|
* @param {object} [timerConfig]
|
||||||
* @param {number} [timerConfig.refresh]
|
* @param {number} [timerConfig.refresh]
|
||||||
|
* @param {number} [timerConfig.updateInterval]
|
||||||
*/
|
*/
|
||||||
constructor(timerConfig?) {
|
constructor(timerConfig: { refresh?: number; updateInterval?: number } = {}) {
|
||||||
this._clear();
|
this._clear();
|
||||||
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
|
this._interval = setInterval(() => this.update(), timerConfig?.refresh ?? 1000);
|
||||||
|
this._updateInterval = timerConfig?.updateInterval ?? 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +57,8 @@ export class TimerService {
|
|||||||
this.pausedTime = 0;
|
this.pausedTime = 0;
|
||||||
this.pausedAt = null;
|
this.pausedAt = null;
|
||||||
this.secondaryTarget = null;
|
this.secondaryTarget = null;
|
||||||
|
|
||||||
|
this._lastUpdate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +99,7 @@ export class TimerService {
|
|||||||
if (this.timer.startedAt === null) {
|
if (this.timer.startedAt === null) {
|
||||||
this.timer.current = timer.duration;
|
this.timer.current = timer.duration;
|
||||||
}
|
}
|
||||||
this.update();
|
this.update(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,13 +246,10 @@ export class TimerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// force an update
|
// force an update
|
||||||
this.update();
|
this.update(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
private updateRoll() {
|
||||||
this.timer.clock = clock.timeNow();
|
|
||||||
|
|
||||||
if (this.playback === Playback.Roll) {
|
|
||||||
const tempCurrentTimer = {
|
const tempCurrentTimer = {
|
||||||
selectedEventId: this.loadedTimerId,
|
selectedEventId: this.loadedTimerId,
|
||||||
current: this.timer.current,
|
current: this.timer.current,
|
||||||
@@ -276,9 +279,9 @@ export class TimerService {
|
|||||||
if (doRollLoad) {
|
if (doRollLoad) {
|
||||||
PlaybackService.roll();
|
PlaybackService.roll();
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// we only update timer if a timer has been started
|
|
||||||
if (this.timer.startedAt !== null) {
|
private updatePlay() {
|
||||||
if (this.playback === Playback.Pause) {
|
if (this.playback === Playback.Pause) {
|
||||||
this.pausedTime = this.timer.clock - this.pausedAt;
|
this.pausedTime = this.timer.clock - this.pausedAt;
|
||||||
}
|
}
|
||||||
@@ -304,14 +307,35 @@ export class TimerService {
|
|||||||
);
|
);
|
||||||
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock);
|
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this._onUpdate();
|
update(force = false) {
|
||||||
|
this.timer.clock = clock.timeNow();
|
||||||
|
|
||||||
|
// we call integrations if we update timers
|
||||||
|
let shouldNotify = false;
|
||||||
|
if (this.playback === Playback.Roll) {
|
||||||
|
shouldNotify = true;
|
||||||
|
this.updateRoll();
|
||||||
|
} else if (this.timer.startedAt !== null) {
|
||||||
|
// we only update timer if a timer has been started
|
||||||
|
shouldNotify = true;
|
||||||
|
this.updatePlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUpdate() {
|
// we only update the store at the updateInterval
|
||||||
|
// side effects such as onFinish will still be triggered in the update functions
|
||||||
|
if (force || this.timer.clock > this._lastUpdate + this._updateInterval) {
|
||||||
|
this._lastUpdate = this.timer.clock;
|
||||||
|
this._onUpdate(shouldNotify);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUpdate(shouldNotify: boolean) {
|
||||||
eventStore.set('timer', this.timer);
|
eventStore.set('timer', this.timer);
|
||||||
|
if (shouldNotify) {
|
||||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onFinish() {
|
_onFinish() {
|
||||||
eventStore.set('timer', this.timer);
|
eventStore.set('timer', this.timer);
|
||||||
@@ -358,7 +382,7 @@ export class TimerService {
|
|||||||
}
|
}
|
||||||
this.playback = Playback.Roll;
|
this.playback = Playback.Roll;
|
||||||
this._onRoll();
|
this._onRoll();
|
||||||
this.update();
|
this.update(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoll() {
|
_onRoll() {
|
||||||
@@ -370,4 +394,5 @@ export class TimerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventTimer = new TimerService();
|
// calculate at 30fps, refresh at 1fps
|
||||||
|
export const eventTimer = new TimerService({ refresh: 32, updateInterval: 1000 });
|
||||||
|
|||||||
@@ -587,10 +587,10 @@ describe('test parseExcel function', () => {
|
|||||||
['Ontime ┬À Schedule Template'],
|
['Ontime ┬À Schedule Template'],
|
||||||
[],
|
[],
|
||||||
['Event Name', 'Test Event'],
|
['Event Name', 'Test Event'],
|
||||||
['Event URL', 'www.carlosvalente.com'],
|
['Public URL', 'www.public.com'],
|
||||||
|
['Backstage URL', 'www.backstage.com'],
|
||||||
['Public Info', 'test public info'],
|
['Public Info', 'test public info'],
|
||||||
['Backstage Info', 'test backstage info'],
|
['Backstage Info', 'test backstage info'],
|
||||||
['End Message', 'test end message'],
|
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
[
|
[
|
||||||
@@ -599,6 +599,8 @@ describe('test parseExcel function', () => {
|
|||||||
'Event Title',
|
'Event Title',
|
||||||
'Presenter Name',
|
'Presenter Name',
|
||||||
'Event Subtitle',
|
'Event Subtitle',
|
||||||
|
'End Action',
|
||||||
|
'Timer type',
|
||||||
'Is Public? (x)',
|
'Is Public? (x)',
|
||||||
'Skip? (x)',
|
'Skip? (x)',
|
||||||
'Notes',
|
'Notes',
|
||||||
@@ -620,6 +622,8 @@ describe('test parseExcel function', () => {
|
|||||||
'Guest Welcome',
|
'Guest Welcome',
|
||||||
'Carlos',
|
'Carlos',
|
||||||
'Getting things started',
|
'Getting things started',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
'x',
|
'x',
|
||||||
'',
|
'',
|
||||||
'Ballyhoo',
|
'Ballyhoo',
|
||||||
@@ -645,6 +649,8 @@ describe('test parseExcel function', () => {
|
|||||||
'A song from the hearth',
|
'A song from the hearth',
|
||||||
'Still Carlos',
|
'Still Carlos',
|
||||||
'Derailing early',
|
'Derailing early',
|
||||||
|
'clock',
|
||||||
|
'load-next',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
'Rainbow chase',
|
'Rainbow chase',
|
||||||
@@ -669,11 +675,10 @@ describe('test parseExcel function', () => {
|
|||||||
|
|
||||||
const expectedParsedEvent = {
|
const expectedParsedEvent = {
|
||||||
title: 'Test Event',
|
title: 'Test Event',
|
||||||
publicUrl: '',
|
publicUrl: 'www.public.com',
|
||||||
backstageUrl: '',
|
backstageUrl: 'www.backstage.com',
|
||||||
publicInfo: 'test public info',
|
publicInfo: 'test public info',
|
||||||
backstageInfo: 'test backstage info',
|
backstageInfo: 'test backstage info',
|
||||||
endMessage: 'test end message',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedParsedRundown = [
|
const expectedParsedRundown = [
|
||||||
@@ -683,6 +688,8 @@ describe('test parseExcel function', () => {
|
|||||||
title: 'Guest Welcome',
|
title: 'Guest Welcome',
|
||||||
presenter: 'Carlos',
|
presenter: 'Carlos',
|
||||||
subtitle: 'Getting things started',
|
subtitle: 'Getting things started',
|
||||||
|
timerType: 'count-down',
|
||||||
|
endAction: 'none',
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
note: 'Ballyhoo',
|
note: 'Ballyhoo',
|
||||||
@@ -705,6 +712,8 @@ describe('test parseExcel function', () => {
|
|||||||
title: 'A song from the hearth',
|
title: 'A song from the hearth',
|
||||||
presenter: 'Still Carlos',
|
presenter: 'Still Carlos',
|
||||||
subtitle: 'Derailing early',
|
subtitle: 'Derailing early',
|
||||||
|
timerType: 'clock',
|
||||||
|
endAction: 'load-next',
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: true,
|
skip: true,
|
||||||
note: 'Rainbow chase',
|
note: 'Rainbow chase',
|
||||||
|
|||||||
@@ -1,878 +0,0 @@
|
|||||||
import { vi } from 'vitest';
|
|
||||||
import { dbModel } from '../../models/dataModel.js';
|
|
||||||
import { parseExcel, parseJson, validateEvent } from '../parser.ts';
|
|
||||||
import { makeString, validateDuration } 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,
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: 'e25a',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
event: {
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: '',
|
|
||||||
note: '',
|
|
||||||
timeStart: 31500000,
|
|
||||||
timeEnd: 32400000,
|
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 32400000 - 31500000,
|
|
||||||
isPublic: false,
|
|
||||||
skip: false,
|
|
||||||
colour: '',
|
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: '4b31',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
};
|
|
||||||
expect(first).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('second event is as a match', () => {
|
|
||||||
const second = parseResponse?.rundown[1];
|
|
||||||
const expected = {
|
|
||||||
title: 'Good Morning',
|
|
||||||
subtitle: 'Days schedule',
|
|
||||||
presenter: 'Carlos Valente',
|
|
||||||
note: '',
|
|
||||||
timeStart: 32400000,
|
|
||||||
timeEnd: 36000000,
|
|
||||||
timeType: 'start-end',
|
|
||||||
duration: 36000000 - 32400000,
|
|
||||||
isPublic: true,
|
|
||||||
skip: true,
|
|
||||||
colour: 'red',
|
|
||||||
type: 'event',
|
|
||||||
revision: 0,
|
|
||||||
id: 'f24d',
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
};
|
|
||||||
expect(second).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loaded event settings', () => {
|
|
||||||
const eventTitle = parseResponse?.event?.title;
|
|
||||||
expect(eventTitle).toBe('This is a test definition');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('endMessage to exist but be empty', () => {
|
|
||||||
const endMessage = parseResponse?.event?.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 event data', async () => {
|
|
||||||
const emptyEventData = {
|
|
||||||
rundown: [{}, {}, {}, {}, {}, {}, {}, {}],
|
|
||||||
event: {},
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: 2,
|
|
||||||
serverPort: 4001,
|
|
||||||
lock: null,
|
|
||||||
timeFormat: '24',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsedDef = await parseJson(emptyEventData);
|
|
||||||
expect(parsedDef.event).toStrictEqual(dbModel.event);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
console.log = vi.fn();
|
|
||||||
const invalidJSON = 'some random dataset';
|
|
||||||
const parsedDef = await parseJson(invalidJSON);
|
|
||||||
expect(console.log).toHaveBeenCalledWith('ERROR: Invalid JSON format');
|
|
||||||
expect(parsedDef).toBe(-1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test event validator', () => {
|
|
||||||
it('validates a good object', () => {
|
|
||||||
const event = {
|
|
||||||
title: 'test',
|
|
||||||
};
|
|
||||||
const validated = validateEvent(event);
|
|
||||||
|
|
||||||
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),
|
|
||||||
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);
|
|
||||||
expect(validated).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('makes objects strings', () => {
|
|
||||||
const event = {
|
|
||||||
title: 2,
|
|
||||||
subtitle: true,
|
|
||||||
presenter: 3.2,
|
|
||||||
note: '1899-12-30T08:00:10.000Z',
|
|
||||||
};
|
|
||||||
const validated = validateEvent(event);
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
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: {},
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
|
|
||||||
val = ['testing'];
|
|
||||||
expected = 'testing';
|
|
||||||
converted = makeString(val);
|
|
||||||
expect(converted).toBe(expected);
|
|
||||||
|
|
||||||
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'],
|
|
||||||
['Event URL', 'www.carlosvalente.com'],
|
|
||||||
['Public Info', 'test public info'],
|
|
||||||
['Backstage Info', 'test backstage info'],
|
|
||||||
['End Message', 'test end message'],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
'Time Start',
|
|
||||||
'Time End',
|
|
||||||
'Event Title',
|
|
||||||
'Presenter Name',
|
|
||||||
'Event Subtitle',
|
|
||||||
'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',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'Rainbow chase',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'b0',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'b5',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'#F00',
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
];
|
|
||||||
|
|
||||||
const expectedParsedEvent = {
|
|
||||||
title: 'Test Event',
|
|
||||||
url: 'www.carlosvalente.com',
|
|
||||||
publicInfo: 'test public info',
|
|
||||||
backstageInfo: 'test backstage info',
|
|
||||||
endMessage: 'test end message',
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedParsedRundown = [
|
|
||||||
{
|
|
||||||
timeStart: 25200000,
|
|
||||||
timeEnd: 28810000,
|
|
||||||
title: 'Guest Welcome',
|
|
||||||
presenter: 'Carlos',
|
|
||||||
subtitle: 'Getting things started',
|
|
||||||
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: 28800000,
|
|
||||||
timeEnd: 30600000,
|
|
||||||
title: 'A song from the hearth',
|
|
||||||
presenter: 'Still Carlos',
|
|
||||||
subtitle: 'Derailing early',
|
|
||||||
isPublic: false,
|
|
||||||
skip: true,
|
|
||||||
note: 'Rainbow chase',
|
|
||||||
user0: 'b0',
|
|
||||||
user5: 'b5',
|
|
||||||
colour: '#F00',
|
|
||||||
type: 'event',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const parsedData = await parseExcel(testdata);
|
|
||||||
|
|
||||||
expect(parsedData.event).toStrictEqual(expectedParsedEvent);
|
|
||||||
expect(parsedData.rundown).toBeDefined();
|
|
||||||
expect(parsedData.rundown.title).toBe(expectedParsedRundown.title);
|
|
||||||
expect(parsedData.rundown.presenter).toBe(expectedParsedRundown.presenter);
|
|
||||||
expect(parsedData.rundown.subtitle).toBe(expectedParsedRundown.subtitle);
|
|
||||||
expect(parsedData.rundown.isPublic).toBe(expectedParsedRundown.isPublic);
|
|
||||||
expect(parsedData.rundown.skip).toBe(expectedParsedRundown.skip);
|
|
||||||
expect(parsedData.rundown.note).toBe(expectedParsedRundown.note);
|
|
||||||
expect(parsedData.rundown.type).toBe(expectedParsedRundown.type);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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].id).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,
|
|
||||||
},
|
|
||||||
views: {
|
|
||||||
overrideStyles: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const parsed = parseViewSettings(testData);
|
|
||||||
expect(parsed).toStrictEqual(testData.views);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('imports defaults to model', () => {
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const parsed = parseViewSettings(testData, true);
|
|
||||||
expect(parsed).toStrictEqual(dbModel.views);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test validateDuration()', () => {
|
|
||||||
describe('handles valid inputs', () => {
|
|
||||||
const valid = [
|
|
||||||
{ test: 'zero values', timeStart: 0, timeEnd: 0 },
|
|
||||||
{ test: 'end after start', timeStart: 0, timeEnd: 1 },
|
|
||||||
];
|
|
||||||
|
|
||||||
valid.forEach((t) => {
|
|
||||||
it(t.test, () => {
|
|
||||||
const d = validateDuration(t.timeStart, t.timeEnd);
|
|
||||||
expect(d).toBe(t.timeEnd - t.timeStart);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handles edge cases', () => {
|
|
||||||
// edge cases
|
|
||||||
const testData = [
|
|
||||||
{ test: 'negative 0', timeStart: -0, timeEnd: -0, expected: 0 },
|
|
||||||
{ test: 'end before start', timeStart: 2, timeEnd: 1, expected: 0 },
|
|
||||||
];
|
|
||||||
|
|
||||||
testData.forEach((t) => {
|
|
||||||
it(t.test, () => {
|
|
||||||
const d = validateDuration(t.timeStart, t.timeEnd);
|
|
||||||
expect(d).toBe(t.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -65,7 +65,6 @@ export const parseExcel = async (excelData) => {
|
|||||||
let publicInfoNext = false;
|
let publicInfoNext = false;
|
||||||
let backstageUrlNext = false;
|
let backstageUrlNext = false;
|
||||||
let backstageInfoNext = false;
|
let backstageInfoNext = false;
|
||||||
let endMessageNext = false;
|
|
||||||
|
|
||||||
const event: Partial<OntimeEvent> = {};
|
const event: Partial<OntimeEvent> = {};
|
||||||
|
|
||||||
@@ -81,14 +80,11 @@ export const parseExcel = async (excelData) => {
|
|||||||
eventData.publicInfo = column;
|
eventData.publicInfo = column;
|
||||||
publicInfoNext = false;
|
publicInfoNext = false;
|
||||||
} else if (backstageUrlNext) {
|
} else if (backstageUrlNext) {
|
||||||
eventData.publicUrl = column;
|
eventData.backstageUrl = column;
|
||||||
backstageUrlNext = false;
|
backstageUrlNext = false;
|
||||||
} else if (backstageInfoNext) {
|
} else if (backstageInfoNext) {
|
||||||
eventData.backstageInfo = column;
|
eventData.backstageInfo = column;
|
||||||
backstageInfoNext = false;
|
backstageInfoNext = false;
|
||||||
} else if (endMessageNext) {
|
|
||||||
eventData.endMessage = column;
|
|
||||||
endMessageNext = false;
|
|
||||||
} else if (j === timeStartIndex) {
|
} else if (j === timeStartIndex) {
|
||||||
event.timeStart = parseExcelDate(column);
|
event.timeStart = parseExcelDate(column);
|
||||||
} else if (j === timeEndIndex) {
|
} else if (j === timeEndIndex) {
|
||||||
@@ -152,9 +148,6 @@ export const parseExcel = async (excelData) => {
|
|||||||
case 'backstage info':
|
case 'backstage info':
|
||||||
backstageInfoNext = true;
|
backstageInfoNext = true;
|
||||||
break;
|
break;
|
||||||
case 'end message':
|
|
||||||
endMessageNext = true;
|
|
||||||
break;
|
|
||||||
case 'time start':
|
case 'time start':
|
||||||
case 'start':
|
case 'start':
|
||||||
timeStartIndex = j;
|
timeStartIndex = j;
|
||||||
@@ -187,6 +180,7 @@ export const parseExcel = async (excelData) => {
|
|||||||
case 'skip':
|
case 'skip':
|
||||||
skipIndex = j;
|
skipIndex = j;
|
||||||
break;
|
break;
|
||||||
|
case 'note':
|
||||||
case 'notes':
|
case 'notes':
|
||||||
notesIndex = j;
|
notesIndex = j;
|
||||||
break;
|
break;
|
||||||
@@ -254,7 +248,7 @@ export const parseExcel = async (excelData) => {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
rundown,
|
rundown,
|
||||||
eventData: eventData,
|
eventData,
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: 2,
|
version: 2,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"lighdev",
|
"lighdev",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-utils",
|
"name": "ontime-utils",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./index.ts",
|
"exports": "./index.ts",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "shared logic for ontime",
|
"description": "shared logic for ontime",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user