Compare commits

...

23 Commits

Author SHA1 Message Date
Carlos Valente 37abd3fe47 chore(ci): prevent publishing resolver on release 2025-10-22 06:26:35 +02:00
Carlos Valente b509ee5b1f bump to version 4.0.2 2025-10-22 06:26:35 +02:00
Shobhit-Nagpal dd2b59efe5 fix: move blue line to pseudo element 2025-10-21 16:29:59 +02:00
Shobhit-Nagpal 5788785b3e fix: minor ui shift on quick add inline 2025-10-21 16:29:59 +02:00
Carlos Valente e488ad19b3 fix: ensure well formed URLs for IP address destinations 2025-10-18 15:29:16 +02:00
Carlos Valente cbc03c898c fix: correct alignment to end message 2025-10-18 15:28:47 +02:00
jwetzell 155bfbdc42 Merge pull request #1833 from cpvalente/fix/npm-action-access-error 2025-10-18 07:13:05 -05:00
jwetzell 5d3714a4fc Merge pull request #1836 from cpvalente/fix/use-flat-rundown 2025-10-18 07:12:39 -05:00
jwetzell a8a1d210d8 use flat order when building flat rundown 2025-10-17 21:46:13 -05:00
jwetzell 49de8b63a7 add setup-node step back to resolve npm access issues 2025-10-17 08:13:11 -05:00
Carlos Valente 03c9f51b47 chore: update demo project 2025-10-17 12:20:18 +02:00
Carlos Valente 8d00fd3966 bump version to 4.0.1 2025-10-17 12:20:18 +02:00
Carlos Valente 80fd21354e fix: apply styling to overtime timer 2025-10-16 21:13:26 +02:00
Carlos Valente a87b15825a fix: end message scaling 2025-10-16 21:13:26 +02:00
Carlos Valente 6a8f98c469 fix: end message only shows if timer is in overtime 2025-10-16 21:13:26 +02:00
jwetzell 8685298430 Merge pull request #1830 from cpvalente/fix/linux-project-download 2025-10-15 07:25:57 -05:00
jwetzell d43af2db57 pass options to project download to allow folders beginning with a dot 2025-10-14 19:51:15 -05:00
jwetzell ef718c48e4 Merge pull request #1828 from cpvalente/npm-trusted-publisher
update cli and resolver workflows for new NPM auth
2025-10-14 16:18:53 -05:00
Alex Christoffer Rasmussen 45e7373279 Small clean-up (#1827)
* chore: avoid shadow variable name

* chore: map could just be foreach

* clean uneeded template literal

* lint
2025-10-13 20:24:25 +02:00
jwetzell 6af2d6e021 update cli and resolver workflows for new NPM auth 2025-10-12 14:11:52 -05:00
Alex Christoffer Rasmussen 9b36f36f73 fix: next cue (#1821)
* fix next cue

* Update apps/server/src/services/runtime-service/runtime.service.ts

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-10-12 14:42:55 +02:00
Carlos Valente 3a0dbf057e chore: update docs 2025-10-12 07:48:25 +02:00
Carlos Valente dae84ab48a chore: update screenshots 2025-10-12 07:48:25 +02:00
29 changed files with 198 additions and 90 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 149 KiB

+8 -4
View File
@@ -14,6 +14,11 @@ jobs:
steps:
- uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -45,10 +50,9 @@ jobs:
- name: Copy external
run: cp -R apps/server/src/external apps/cli/external
- name: Setup pnpm auth config
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
+8 -6
View File
@@ -1,8 +1,6 @@
name: Ontime Resolver build
on:
release:
types: [published]
workflow_dispatch:
jobs:
@@ -14,6 +12,11 @@ jobs:
steps:
- uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@v4
@@ -34,10 +37,9 @@ jobs:
- name: Build project packages
run: pnpm build:resolver
- name: Setup pnpm auth config
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
-2
View File
@@ -26,8 +26,6 @@ We do our best to have most topics covered by the documentation. However, if you
Let us know!
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
We would also like to include a testimonials section in our ✨new website✨. It would be great to showcase the diversity of users running Ontime.
# Ontime
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0",
"version": "4.0.2",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "4.0.0",
"version": "4.0.2",
"private": true,
"type": "module",
"dependencies": {
@@ -55,7 +55,7 @@ export function useFlatRundown() {
// update data whenever the revision changes
useEffect(() => {
if (data.revision !== -1 || data.revision !== prevRevision) {
const flatRundown = data.order.map((id) => data.entries[id]);
const flatRundown = data.flatOrder.map((id) => data.entries[id]);
setFlatRundown(flatRundown);
setPrevRevision(data.revision);
}
@@ -175,7 +175,7 @@ describe('generateUrlPresetOptions', () => {
expect(() => generateUrlPresetOptions('test', 'invalid-url')).toThrow();
});
it('throws on on invalid route', () => {
it('throws on invalid route', () => {
expect(() => generateUrlPresetOptions('test', 'www.getontime.no/somethingelse/')).toThrow();
});
});
@@ -2,16 +2,21 @@
display: flex;
align-items: center;
gap: 1rem;
height: 0;
position: relative;
height: 1px;
background: $blue-500;
z-index: $zindex-floating;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: $blue-500;
}
}
.addButton {
position: absolute;
top: 0;
left: 0;
transform: translateY(-50%);
position: relative;
}
@@ -14,12 +14,18 @@ export default function GenerateLinkFormExport({ lockedPath }: GenerateLinkFormE
const { data: infoData } = useInfo();
const { data: urlPresetData } = useUrlPresets({ skip: lockedPath === undefined });
/**
* hostOptions are only used for local networks
* the NIF address is a local IP address: 192.168.x.x or 10.x.x.x
* We need to inject the port and protocol to create a valid URL
* eg: http://192.168.x.x:port
*/
const hostOptions = useMemo(() => {
return infoData.networkInterfaces.map((nif) => ({
value: nif.address,
value: `http://${nif.address}:${infoData.serverPort}`,
label: `${nif.name} - ${nif.address}`,
}));
}, [infoData.networkInterfaces]);
}, [infoData.networkInterfaces, infoData.serverPort]);
const pathOptions = useMemo(() => {
if (lockedPath) {
+2 -2
View File
@@ -92,10 +92,10 @@
.end-message {
text-align: center;
font-size: 11.5vw;
line-height: 0.9em;
font-weight: 600;
color: var(--timer-overtime-color-override, $timer-finished-color);
padding: 0;
max-height: 100%;
}
.timer {
@@ -117,7 +117,7 @@
// use a class instead of a phase, to allow suppressing overtime style
&--finished {
color: var(--timer-colour, var(--timer-overtime-color-override, $timer-finished-color));
color: var(--timer-overtime-color-override, $timer-finished-color);
}
&[data-phase="warning"] {
+1 -1
View File
@@ -105,7 +105,7 @@ export function getShowModifiers(
}
return {
showEndMessage: freezeOvertime && freezeMessage !== '',
showEndMessage: phase === TimerPhase.Overtime && freezeOvertime && freezeMessage !== '',
showFinished: phase === TimerPhase.Overtime,
showWarning: phase === TimerPhase.Warning,
showDanger: phase === TimerPhase.Danger,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0",
"version": "4.0.2",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0",
"version": "4.0.2",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+1 -1
View File
@@ -109,7 +109,7 @@ export async function projectDownload(req: Request, res: Response) {
return;
}
res.download(pathToFile, filename, (error) => {
res.download(pathToFile, filename, { dotfiles: 'allow' }, (error) => {
if (error) {
const message = getErrorMessage(error);
res.status(500).send({ message });
@@ -355,7 +355,7 @@ export function migrateRundown(
const newCustom: EntryCustomFields = {};
if (translationTable) {
Object.entries(custom).map(([key, value]) => {
Object.entries(custom).forEach(([key, value]) => {
const newKey = translationTable.get(key);
if (newKey) {
newCustom[newKey] = value;
@@ -19,8 +19,10 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"Warning Time",
"Danger Time",
"Skip",
"Custom Field",
"Images",
"Video Notes",
"Audio Notes",
"PowerPoint Name",
"PowerPoint Slide",
],
[
"e2163f",
@@ -33,16 +35,21 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"",
"",
"",
"Go to Settings cog wheel top left of Editor, Project > Create",
"Moderator - Emma Thompson
Speakers
- Liam Carter + Sophia Patel
- Ethan Brooks
- Lucas Bennett",
"milestone",
"",
"",
"",
"",
"Create Custom Fields to store specific text information or images.
Go to Settings > Project data > Custom fields",
"https://www.getontime.no/images/icons/ontime-logo.png",
"",
"",
"",
"",
],
[
"7eaf99",
@@ -63,6 +70,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"9bf60f",
@@ -81,8 +90,11 @@ Go to Settings > Project data > Custom fields",
"00:10:00",
"00:05:00",
"",
"Put additional info here",
"",
"Camera + PowerPoint on stream
PowerPoint on screens",
"2x Wireless Hand Helds",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"bf71a2",
@@ -103,6 +115,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"c2697f",
@@ -121,8 +135,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00",
"00:01:00",
"",
"",
"",
"Cameras on stream
PowerPoint on screens",
"1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"fa593e",
@@ -141,8 +158,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00",
"00:01:00",
"",
"",
"",
"Camera + PowerPoint on stream
PowerPoint on screens",
"2x Wireless Hand Helds",
"Session1.pptx",
"https://www.getontime.no/images/aux/demo-slide2.webp",
],
[
"a8b0b3",
@@ -163,6 +183,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"",
@@ -201,6 +223,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"0aaa7d",
@@ -213,15 +237,16 @@ Go to Settings > Project data > Custom fields",
"01:00:00",
"TRUE",
"",
"Buffet in lobby
Music plays, holding slide on screens",
"Buffet in lobby",
"count-down",
"",
"00:02:00",
"00:01:00",
"",
"",
"",
"Holding slide on screens",
"House music",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"",
@@ -260,6 +285,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"02afca",
@@ -280,6 +307,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"75ce86",
@@ -298,8 +327,14 @@ Music plays, holding slide on screens",
"00:02:00",
"00:01:00",
"",
"",
"",
"Camera + PPT + Video on stream
PowerPoint + Video on screens
Video file: Session2.mp4",
"1x Wireless Hand Held
1x Video with audio",
"Session2.pptx",
"https://www.getontime.no/images/aux/demo-slide3.webp",
],
[
"e10ed9",
@@ -318,8 +353,10 @@ Music plays, holding slide on screens",
"00:02:00",
"00:01:00",
"",
"",
"",
"Holding slide on screens",
"1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"07df89",
@@ -340,6 +377,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"",
@@ -52,7 +52,7 @@ router.get('/:rundownId/export', validateRundownExport, (req: Request, res: Resp
const customFields = getProjectCustomFields();
const buffer = generateExcelFile(rundown, customFields);
res.setHeader('Content-Disposition', `attachment;`);
res.setHeader('Content-Disposition', 'attachment;');
res.setHeader('Content-Type', EXCEL_MIME);
res.setHeader('Content-Length', buffer.length.toString());
res.status(200).send(buffer);
@@ -80,6 +80,18 @@ describe('generateAuthenticatedUrl()', () => {
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/?token=1234');
});
it('throws if provided an IP address without protocol', () => {
expect(() =>
generateShareUrl('192.168.10.173', '<<companion>>', {
lockConfig: false,
lockNav: false,
authenticate: true,
preset: undefined,
hash: '1234',
}),
).toThrowError('Invalid URL');
});
});
describe('for ontime-cloud URLs', () => {
@@ -62,6 +62,11 @@ export function generateShareUrl(
canonicalPath: string,
{ authenticate, lockConfig, lockNav, preset, prefix = routerPrefix, hash = hashedPassword }: LinkOptions,
): URL {
/**
* URL constructor will throw if given an IP address without protocol
* for the case of IP addresses, we expect that the base URL provides protocol and port
* eg: http://192.168.10.1:4001
*/
const url = new URL(baseUrl);
// companion links point to the root
+2 -2
View File
@@ -123,8 +123,8 @@ function formatTimer(number) {
* @param {number} number - The number to pad
* @returns {string} The padded number string
*/
function leftPad(number) {
return Math.floor(number).toString().padStart(2, '0');
function leftPad(val) {
return Math.floor(val).toString().padStart(2, '0');
}
}
+51 -17
View File
@@ -28,13 +28,9 @@ export const demoDb: DatabaseModel = {
type: SupportedEntry.Milestone,
cue: 'Demo',
title: 'Clear all, or Create New Project to start fresh',
note: 'Go to Settings cog wheel top left of Editor, Project > Create',
note: 'Moderator - Emma Thompson\n\nSpeakers\n- Liam Carter + Sophia Patel\n- Ethan Brooks\n- Lucas Bennett',
colour: '#9d9d9d',
custom: {
Custom_Field:
'Create Custom Fields to store specific text information or images.\n\nGo to Settings > Project data > Custom fields',
Images: 'https://www.getontime.no/images/icons/ontime-logo.png',
},
custom: {},
parent: null,
revision: 0,
},
@@ -78,7 +74,10 @@ export const demoDb: DatabaseModel = {
timeWarning: 600000,
timeDanger: 300000,
custom: {
Custom_Field: 'Put additional info here',
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
@@ -117,7 +116,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Cameras on stream\nPowerPoint on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
fa593e: {
@@ -144,7 +148,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide2.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'Session1.pptx',
},
triggers: [],
},
a8b0b3: {
@@ -187,7 +196,7 @@ export const demoDb: DatabaseModel = {
timerType: TimerType.CountDown,
countToEnd: false,
skip: false,
note: 'Buffet in lobby\nMusic plays, holding slide on screens',
note: 'Buffet in lobby',
colour: '#779BE7',
delay: 0,
dayOffset: 0,
@@ -197,7 +206,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: 'House music',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
'6b0edb': {
@@ -250,7 +264,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide3.webp',
Video_Notes: 'Camera + PPT + Video on stream\nPowerPoint + Video on screens\n\nVideo file: Session2.mp4',
Audio_Notes: '1x Wireless Hand Held\n1x Video with audio',
PowerPoint_Name: 'Session2.pptx',
},
triggers: [],
},
e10ed9: {
@@ -277,7 +296,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
'07df89': {
@@ -341,15 +365,25 @@ export const demoDb: DatabaseModel = {
},
],
customFields: {
Custom_Field: {
Video_Notes: {
type: 'text',
colour: '#FFAB33',
label: 'Video Notes',
},
Audio_Notes: {
type: 'text',
colour: '#339E4E',
label: 'Audio Notes',
},
PowerPoint_Name: {
type: 'text',
colour: '#3E75E8',
label: 'Custom Field',
label: 'PowerPoint Name',
},
Images: {
PowerPoint_Slide: {
type: 'image',
colour: '#ED3333',
label: 'Images',
label: 'PowerPoint Slide',
},
},
automation: {
@@ -50,7 +50,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false;
}
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
return false;
}
@@ -273,13 +273,14 @@ class RuntimeService {
public startByCue(cue: string): boolean {
const state = runtimeState.getState();
const rundown = getCurrentRundown();
const { playableEventOrder } = getRundownMetadata();
const { timedEventOrder } = getRundownMetadata();
const event = findNextPlayableWithCue(
rundown,
playableEventOrder,
timedEventOrder,
cue,
state.rundown.selectedEventIndex ?? undefined,
state.timer.playback === Playback.Armed, // If we are armed allow the armed event to be considered for playback
);
if (!event) {
@@ -341,6 +342,7 @@ class RuntimeService {
playableEventOrder,
cue,
state.rundown.selectedEventIndex ?? undefined,
false,
);
if (!event) {
@@ -118,24 +118,25 @@ export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventI
*/
export function findNextPlayableWithCue(
rundown: Rundown,
playableEventsOrder: EntryId[],
timedEventsOrder: EntryId[],
targetCue: string,
currentEventIndex = 0,
allowCurrent = false,
): OntimeEvent | undefined {
const lowerCaseCue = targetCue.toLowerCase();
const startFromIndex = allowCurrent ? currentEventIndex : currentEventIndex + 1;
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) {
const eventId = playableEventsOrder[i];
for (let i = startFromIndex; i < timedEventsOrder.length; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
for (let i = 0; i < currentEventIndex; i++) {
const eventId = playableEventsOrder[i];
for (let i = 0; i < startFromIndex; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
@@ -377,7 +377,7 @@ export async function upload(sheetId: string, options: ImportMap) {
const titleMetadata = Object.values(sheetMetadata)[0];
if (titleMetadata === undefined) {
throw new Error(`Sheet read failed: failed to find title row`);
throw new Error('Sheet read failed: failed to find title row');
}
const titleRow = titleMetadata['row'];
const updateRundown = Array<sheets_v4.Schema$Request>();
+4 -4
View File
@@ -11,14 +11,14 @@ describe('parseExcelDate', () => {
['1899-12-30T07:00:00.000Z', 28800000],
['1899-12-30T08:00:10.000Z', 32410000],
['1899-12-30T08:30:00.000Z', 34200000],
])(`handles %s`, (fromExcel, expected) => {
])('handles %s', (fromExcel, expected) => {
expect(parseExcelDate(fromExcel)).toBe(expected);
});
});
describe('parses a time string that passes validation', () => {
test.each([['10:00:00'], ['10:00'], ['10:00AM'], ['10:00am'], ['10:00PM'], ['10:00pm']])(
`handles %s`,
'handles %s',
(fromExcel) => {
expect(parseExcelDate(fromExcel)).not.toBe(0);
},
@@ -26,13 +26,13 @@ describe('parseExcelDate', () => {
});
describe('uses numeric fields as minutes', () => {
test.each([[1], [10], [100]])(`handles numeric fields %s`, (fromExcel) => {
test.each([[1], [10], [100]])('handles numeric fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(fromExcel * MILLIS_PER_MINUTE);
});
});
describe('returns 0 on other strings', () => {
test.each([['test'], [''], ['x']])(`handles invalid fields %s`, (fromExcel) => {
test.each([['test'], [''], ['x']])('handles invalid fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(0);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0",
"version": "4.0.2",
"description": "Time keeping for live events",
"keywords": [
"ontime",