Files
ontime/apps/server/src/services/__tests__/timerUtils.test.ts
T
Alex Christoffer Rasmussen 722e045b20 Revert "Session version" (#1649)
* Revert "refactor: add version to session endpoint"

This reverts commit cd40a6e55e.

* Revert "Remove public event feature (#1645)"

This reverts commit 08d9e24871.

* Revert "Fix: rearrange playing event (#1640)"

This reverts commit 0649678dca.

* Revert "refactor: style tweaks to edit css modal"

This reverts commit a00ec2d02a.

* Revert "refactor: remove usages of framer-motion"

This reverts commit 54a74ccc2a.

* Revert "Upgrade expressjs (#1633)"

This reverts commit 6f3ab274bd.

* Revert "Refactor: require trigger in all events objects (#1636)"

This reverts commit 90870ecfb6.

* Revert "Fix: Correct boundary condition in applyDelay"

This reverts commit b640e0e181.

* Revert "let vite be the proxy to the dev server (#1630)"

This reverts commit c41fe824cf.

* Revert "refactor: migrate custom fields to transactions"

This reverts commit 62c8319d70.

* Revert "refactor: simplify validations"

This reverts commit b1d23467a2.

* Revert "refactor: create transaction system and apply to adding entry (#1620)"

This reverts commit 9a62daf047.

* Revert "Refactor: better rounding (#1594)"

This reverts commit b9ab1c6fd7.

* Revert "refactor: improve reorder logic"

This reverts commit c1054711b0.

* Revert "chore: simplify URLs"

This reverts commit a4d4f29a37.

* Revert "refactor: order is single source of truth"

This reverts commit 2793aadea0.

* Revert "refactor: refetch targets is enum"

This reverts commit e7cfb7d9d9.

* Revert "refactor: small ux improvements"

This reverts commit 256a851c9b.

* Revert "refactor: remove trivially inferred numEvents"

This reverts commit 4ab9c81cb8.

* Revert "feat: duplicate groups"

This reverts commit 2f13d6c89e.

* Revert "feat: create group from entry selection"

This reverts commit f1f7bad25e.

* Revert "feat: create block from rundown empty"

This reverts commit a8b52a48f7.

* Revert "fix: collapsed blocks dont render children"

This reverts commit cd0999b2ab.

* Revert "refactor: type cleanup and test improvements"

This reverts commit b4c60f3f04.

* Revert "feat: allow dissolving a block"

This reverts commit 5cefad3666.

* Revert "fix: uncontrolled prop on controlled component"

This reverts commit 7a6ecd8c34.

* Revert "refactor: improve return of reorder"

This reverts commit ba96ecfd91.

* Revert "refactor: mutations on batch elements must have IDs"

This reverts commit 7bed3757f2.

* Revert "chore: upgrade dependencies"

This reverts commit e2e755b1d2.

* Revert "refactor: extract utility to merge two arrays"

This reverts commit a77d23109d.

* Revert "refactor: change network mode defaults"

This reverts commit 0eb3b8d382.

* Revert "fix: delete nested events"

This reverts commit 0021185288.

* Revert "fix: add event at end of block"

This reverts commit 94c72ff4f6.

* Revert "refactor: make finder available in exported rundown"

This reverts commit e4c08dc9b2.

* Revert "assert non null and update test (#1604)"

This reverts commit ec74af0d62.

* Revert "Fix project renumber (#1597)"

This reverts commit b6d72dd082.

* Revert "Refactor: WebSocket from flush queue to one patch (#1595)"

This reverts commit 31c311daf0.

* Revert "Refactor: ms for api calls (#1593)"

This reverts commit e9b3cc6090.

* Revert "fix test (#1601)"

This reverts commit 543b04a097.

* Revert "fix: rebase master"

This reverts commit d39b85b6e6.

* Revert "chore: correct test path"

This reverts commit bbe107bb2b.

* Revert "refactor: extract rundown parsing"

This reverts commit c616240db1.

* Revert "chore: improve convention entry <> event"

This reverts commit 166be66ce3.

* Revert "refactor: maintain flat orders"

This reverts commit 4180d0a337.

* Revert "refactor: implement operations on nested events"

This reverts commit 78108e316c.

* Revert "refactor: process events in rundown"

This reverts commit 3bb8b70915.

* Revert "chore: improve convention entry <> event"

This reverts commit 1c4f13a0ed.

* Revert "chore: rename currentBlock > parent"

This reverts commit 3ca0abad53.

* Revert "refactor: fix delay positioning in gaps"

This reverts commit 030c8f897f.

* Revert "refactor(e2e): skip flaky test"

This reverts commit 68175cfa3b.

* Revert "refactor: improve project loading"

This reverts commit fd8f757851.

* Revert "refactor: gather group metadata"

This reverts commit 876d111c61.

* Revert "refactor: swap maintains schedule"

This reverts commit 730cb95c04.

* Revert "chore: rename files"

This reverts commit 3c388d4fb5.

* Revert "refactor: restructure model to contain an object of rundowns"

This reverts commit 2e23718d73.

* Revert "refactor: clearer relationship on rundown elements"

This reverts commit 89ea8c470b.

* Revert "refactor: use strict typing"

This reverts commit 178640bfc4.

* Revert "refactor: remove stop as a possible end action"

This reverts commit 4ed38340e0.

* Revert "refactor: restructure model to contain an object of rundowns"

This reverts commit 69eb9a5eff.

* Revert "chore: remove IDE files"

This reverts commit 351425127a.

* Revert "refactor: remove unused and legacy code"

This reverts commit 95f2ba37cc.
2025-06-20 12:28:20 +02:00

1299 lines
31 KiB
TypeScript

import { dayInMs, millisToString } from 'ontime-utils';
import { EndAction, Playback, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import {
getCurrent,
getExpectedFinish,
getRuntimeOffset,
getTimerPhase,
normaliseEndTime,
skippedOutOfEvent,
} from '../timerUtils.js';
import type { RuntimeState } from '../../stores/runtimeState.js';
describe('getExpectedFinish()', () => {
it('is null if we havent started', () => {
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 0,
duration: 10,
finishedAt: null,
startedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(null);
});
it('is finishedAt if defined', () => {
const state = {
eventNow: {
timeEnd: 20,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 0,
duration: 10,
finishedAt: 20, // <---- finished at
startedAt: 10,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(20);
});
it('calculates the finish time', () => {
const state = {
eventNow: {
timeEnd: 11,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 0,
duration: 10,
finishedAt: null,
startedAt: 1,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(11);
});
it('adds paused and added times', () => {
const state = {
eventNow: {
timeEnd: 11,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 20,
duration: 10,
finishedAt: null,
startedAt: 1,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(31);
});
it('added time could be negative', () => {
const state = {
eventNow: {
timeEnd: 11,
timerType: TimerType.CountDown,
},
timer: {
addedTime: -10,
duration: 10,
finishedAt: null,
startedAt: 1,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
});
it('user could add enough time for it to be negative', () => {
const state = {
eventNow: {
timeEnd: 11,
timerType: TimerType.CountDown,
},
timer: {
addedTime: -100,
duration: 10,
finishedAt: null,
startedAt: 1,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
});
it('timer can have no duration', () => {
const state = {
eventNow: {
timeEnd: 0,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 0,
duration: 0,
finishedAt: null,
startedAt: 1,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(1);
});
it('finish can be the day after', () => {
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
timer: {
addedTime: 0,
duration: dayInMs,
finishedAt: null,
startedAt: 10,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(10);
});
describe('on timers of type time-to-end', () => {
it('finish time is as schedule + added time', () => {
const state = {
eventNow: {
timeEnd: 30,
countToEnd: true,
},
timer: {
addedTime: 10,
duration: dayInMs,
finishedAt: null,
startedAt: 10,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
expect(calculatedFinish).toBe(40);
});
it('handles events that finish the day after', () => {
const state = {
eventNow: {
timeEnd: 600000, // 00:10:00
countToEnd: true,
},
timer: {
addedTime: 0,
finishedAt: null,
startedAt: 79200000, // 22:00:00
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 79200000,
plannedEnd: 600000,
},
} as RuntimeState;
const calculatedFinish = getExpectedFinish(state);
// expected finish is not a duration but a point in time
expect(calculatedFinish).toBe(600000);
});
});
});
describe('getCurrent()', () => {
it('is duration if it hasnt started', () => {
const state = {
eventNow: {
timeEnd: 30,
timerType: TimerType.CountDown,
},
clock: 0,
timer: {
addedTime: 10,
duration: 111, // <-- we take the duration value
startedAt: null,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(111);
});
it('is the remaining time in clock', () => {
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
clock: 1,
timer: {
addedTime: 0,
duration: 10,
startedAt: 0,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(9);
});
it('accounts for added times', () => {
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
clock: 1,
timer: {
addedTime: 10,
duration: 10,
startedAt: 0,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(19);
});
it('counts over midnight', () => {
const state = {
eventNow: {
timeEnd: 20,
timerType: TimerType.CountDown,
},
clock: 10,
timer: {
addedTime: 0,
duration: dayInMs + 10,
startedAt: 10,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(dayInMs + 10);
});
it('rolls over midnight', () => {
const state = {
eventNow: {
timeEnd: 20,
timerType: TimerType.CountDown,
},
clock: 5,
timer: {
addedTime: 0,
duration: dayInMs + 10,
startedAt: 10,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(15);
});
it('midnight holds delays', () => {
const state = {
eventNow: {
timeEnd: 20,
timerType: TimerType.CountDown,
},
clock: 5,
timer: {
addedTime: 20,
duration: dayInMs + 10,
startedAt: 10,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(35);
});
describe('on timers of type count-to-end', () => {
it('current time is the time to end even if it hasnt started, this is weird, but by design', () => {
const state = {
eventNow: {
timeEnd: 100,
countToEnd: true,
},
clock: 30,
timer: {
addedTime: 0,
duration: 100,
startedAt: null,
finishedAt: null,
},
runtime: {
plannedEnd: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(70);
});
it('current time is the time to end', () => {
const state = {
eventNow: {
timeEnd: 100,
countToEnd: true,
},
clock: 30,
timer: {
addedTime: 0,
duration: 100,
startedAt: 10,
finishedAt: null,
},
runtime: {
plannedEnd: 100,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(70);
});
it('current time is the time to end + added time', () => {
const state = {
eventNow: {
timeEnd: 100,
countToEnd: true,
},
clock: 30,
timer: {
addedTime: 7,
duration: 100,
startedAt: 10,
finishedAt: null,
},
runtime: {
plannedEnd: 100,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(77);
});
it('handles events that finish the day after', () => {
const state = {
eventNow: {
timeStart: 79200000, // 22:00:00
timeEnd: 600000, // 00:10:00
countToEnd: true,
},
clock: 79500000, // 22:05:00
timer: {
addedTime: 0,
duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00
finishedAt: null,
},
runtime: {
actualStart: 79200000,
plannedEnd: 600000,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(dayInMs - 79500000 + 600000);
});
it('handles events that were started late', () => {
const state = {
clock: 82000000, // 22:46:40 <--- starting 16 min after the scheduled end
eventNow: {
timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00
duration: 3600000, // 01:00:00
countToEnd: true,
},
timer: {
addedTime: 0,
duration: Infinity, // not relevant,
startedAt: 79200000, // 22:00:00
finishedAt: null,
},
runtime: {
actualStart: 82000000, // 22:46:40 <--- started now
plannedEnd: 81000000, // 22:30:00
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const current = getCurrent(state);
expect(current).toBe(81000000 - 82000000); // <-- planned end - now
});
});
});
describe('getExpectedFinish() and getCurrentTime() combined', () => {
it('without added times, they combine to be duration', () => {
const duration = 10;
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
clock: 0,
timer: {
addedTime: 0,
duration,
startedAt: 0,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state);
const elapsed = duration - current;
expect(expectedFinish).toBe(10);
expect(elapsed).toBe(0);
expect(current).toBe(10);
expect(elapsed + current).toBe(10);
});
it('added times influence expected finish', () => {
const duration = 10;
const state = {
eventNow: {
timeEnd: 10,
timerType: TimerType.CountDown,
},
clock: 5,
timer: {
addedTime: 3,
duration,
startedAt: 0,
finishedAt: null,
},
_timer: {
pausedAt: null,
},
} as RuntimeState;
const expectedFinish = getExpectedFinish(state);
const current = getCurrent(state);
const elapsed = duration - current;
expect(expectedFinish).toBe(13);
expect(elapsed).toBe(2);
expect(current).toBe(8);
});
});
describe('skippedOutOfEvent()', () => {
const testSkipLimit = 32;
it('does not consider an event end as a skip', () => {
const startedAt = 1000;
const duration = 1000;
const expectedFinish = startedAt + duration;
const previousTime = expectedFinish - testSkipLimit / 2;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock += testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
it('allows rolling backwards in an event', () => {
const startedAt = 1000;
const duration = 1000;
const expectedFinish = startedAt + duration;
const previousTime = startedAt + testSkipLimit / 2;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock += testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
it('accounts for crossing midnight', () => {
const startedAt = dayInMs - testSkipLimit;
const expectedFinish = 10;
const previousTime = dayInMs - 1;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock = testSkipLimit - 2;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
it('allows rolling backwards in an event across midnight', () => {
const startedAt = dayInMs - testSkipLimit;
const expectedFinish = 10;
const previousTime = startedAt + 1;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock -= testSkipLimit;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
});
it('finds skip forwards out of event', () => {
const startedAt = 1000;
const duration = 1000;
const expectedFinish = startedAt + duration;
const previousTime = expectedFinish - testSkipLimit / 2;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock += testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
it('finds skip backwards out of event', () => {
const startedAt = 1000;
const duration = 1000;
const expectedFinish = startedAt + duration;
const previousTime = startedAt + testSkipLimit / 2;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock -= testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
it('finds skip forwards out of event across midnight', () => {
const startedAt = dayInMs - testSkipLimit;
const expectedFinish = 10;
const previousTime = dayInMs - 3;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock = testSkipLimit - 2;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
it('finds skip backwards out of event across midnight', () => {
const startedAt = dayInMs - testSkipLimit;
const expectedFinish = 10;
const previousTime = startedAt + 1;
const state = {
clock: previousTime,
timer: {
expectedFinish,
startedAt,
},
} as RuntimeState;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(false);
state.clock -= testSkipLimit + 1;
expect(skippedOutOfEvent(state, previousTime, testSkipLimit)).toBe(true);
});
});
test('normaliseEndTime()', () => {
const t1 = {
start: 10,
end: 20,
};
const t1_expected = 20;
expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected);
const t2 = {
start: 10 + dayInMs,
end: 20,
};
const t2_expected = 20 + dayInMs;
expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected);
const t3 = {
start: 10,
end: 10,
};
const t3_expected = 10;
expect(normaliseEndTime(t3.start, t3.end)).toBe(t3_expected);
});
describe('getRuntimeOffset()', () => {
it('is the difference between scheduled and when we actually started', () => {
const state = {
eventNow: {
id: '1',
timeStart: 100,
},
timer: {
startedAt: 150,
addedTime: 0,
current: 0,
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 150,
},
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-50);
});
it('added time subtracts time offset (positive offset)', () => {
const state = {
eventNow: {
id: '1',
timeStart: 100,
},
timer: {
startedAt: 150, // we started 50ms delayed
addedTime: 10, // we compensated with 10ms
current: 10, // we are 10ms into the timer
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 150,
},
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-60);
});
it('considers running overtime (negative offset)', () => {
const state = {
eventNow: {
id: '1',
timeStart: 100,
timeEnd: 140,
},
timer: {
startedAt: 100, // we started ontime
current: -10, // we are 10 seconds over
addedTime: 0, // we have not compensated with added time
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 100,
},
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-10);
});
it('paused time is delayed time (negative offset)', () => {
const state = {
eventNow: {
id: '1',
timeStart: 100,
timeEnd: 150,
},
clock: 150,
timer: {
startedAt: 100, // started on time
current: 25, // are 25ms into it
addedTime: 0,
},
_timer: {
pausedAt: 125, // we have been paused for 25ms (see clock)
},
runtime: {
actualStart: 100,
},
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-25);
});
it('offset doesnt exist if we havent started', () => {
const state = {
clock: 78480789,
eventNow: {
id: 'd6a2ce',
timeStart: 77400000,
timeEnd: 81000000,
duration: 3600000,
timeStrategy: 'lock-duration',
linkStart: null,
},
runtime: {
selectedEventIndex: 0,
numEvents: 2,
offset: -77400000,
plannedStart: 77400000,
plannedEnd: 84600000,
actualStart: null,
expectedEnd: null,
},
timer: {
addedTime: 0,
current: 3600000,
duration: 3600000,
elapsed: null,
expectedFinish: null,
finishedAt: null,
playback: 'armed',
secondaryTimer: null,
startedAt: null,
},
_timer: { pausedAt: null },
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(0);
});
it('handles loaded event', () => {
const state = {
clock: 79521653,
eventNow: {
id: '835242',
timeStart: 81000000,
timeEnd: 84600000,
duration: 3600000,
timeStrategy: 'lock-duration',
linkStart: null,
endAction: 'none',
timerType: 'count-down',
delay: 0,
},
runtime: {
selectedEventIndex: 1,
numEvents: 2,
offset: -81000000,
plannedStart: 77400000,
plannedEnd: 84600000,
actualStart: 79443403,
expectedEnd: null,
},
timer: {
addedTime: 0,
current: 3600000,
duration: 3600000,
elapsed: null,
expectedFinish: null,
finishedAt: null,
playback: 'armed',
secondaryTimer: null,
startedAt: null,
},
_timer: { pausedAt: null },
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(81000000 - 79521653); // clock - timestart
});
it('with time-to-end, offsets dont exist if we are not in overtime', () => {
const state = {
clock: 80000000, // 22:13:20
eventNow: {
id: 'd6a2ce',
type: 'event',
title: '',
timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00
duration: 3600000, // 01:00:00
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
isPublic: true,
skip: false,
note: '',
colour: '',
cue: '1',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
delay: 0,
},
runtime: {
selectedEventIndex: 0,
numEvents: 1,
offset: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
expectedEnd: 81600000, // 22:40:00
},
timer: {
addedTime: 0,
current: 1600000,
duration: 3600000,
elapsed: 2000000,
expectedFinish: 81600000,
finishedAt: null,
playback: Playback.Play,
secondaryTimer: null,
startedAt: 78000000,
},
_timer: { pausedAt: null },
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(0);
});
it('with time-to-end, offset is the overtime', () => {
const state = {
clock: 82000000, // 22:46:40
eventNow: {
id: 'd6a2ce',
type: 'event',
title: '',
timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00
duration: 3600000, // 01:00:00
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
isPublic: true,
skip: false,
note: '',
colour: '',
cue: '1',
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
delay: 0,
},
runtime: {
selectedEventIndex: 0,
numEvents: 1,
offset: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 78000000, // 21:40:00
expectedEnd: 81600000, // 22:40:00
},
timer: {
addedTime: -200000,
current: -400000,
duration: 3600000,
elapsed: 4000000,
expectedFinish: 81600000,
finishedAt: null,
playback: Playback.Play,
secondaryTimer: null,
startedAt: 78000000,
},
_timer: { pausedAt: null },
} as RuntimeState;
const { absoluteOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-400000); // <--- offset is always the overtime
});
it('handles time-to-end started after the end time', () => {
const state = {
clock: 82000000, // 22:46:40 <--- starting 16m 40s after the scheduled end
eventNow: {
id: 'd6a2ce',
timeStart: 77400000, // 21:30:00
timeEnd: 81000000, // 22:30:00
duration: 3600000, // 01:00:00
timeStrategy: TimeStrategy.LockEnd,
linkStart: null,
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
},
runtime: {
selectedEventIndex: 0,
numEvents: 1,
offset: 0,
plannedStart: 77400000, // 21:30:00
plannedEnd: 81000000, // 22:30:00
actualStart: 82000000, // 22:46:40 <--- started now
expectedEnd: 82000000 + 3600000, // <--- now + duration
},
timer: {
addedTime: 0,
current: 0,
duration: 3600000,
elapsed: 0,
expectedFinish: 82000000 + 3600000, // <--- now + duration
finishedAt: null,
playback: Playback.Play,
secondaryTimer: null,
startedAt: 82000000, // <--- started now
},
_timer: { pausedAt: null },
} as RuntimeState;
const updateCurrent = getCurrent(state);
state.timer.current = updateCurrent;
const { absoluteOffset } = getRuntimeOffset(state);
expect(millisToString(absoluteOffset)).toBe('-00:16:40');
expect(absoluteOffset).toBe(81000000 - 82000000); // <-- planned end - now
});
});
describe('getRelativeOffset()', () => {
it('relative offset is 0 when starting at the planed time', () => {
const state = {
eventNow: {
id: '1',
timeStart: 150,
},
timer: {
startedAt: 150,
addedTime: 0,
current: 0,
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 150,
plannedStart: 150,
},
} as RuntimeState;
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(0);
expect(relativeOffset).toBe(0);
});
it('relative offset is 0 when starting after the planed time', () => {
const state = {
eventNow: {
id: '1',
timeStart: 100,
},
timer: {
startedAt: 150,
addedTime: 0,
current: 0,
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 150,
plannedStart: 100,
},
} as RuntimeState;
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(-50);
expect(relativeOffset).toBe(0);
});
it('relative offset is 0 when starting before the planed time', () => {
const state = {
eventNow: {
id: '1',
timeStart: 150,
},
timer: {
startedAt: 100,
addedTime: 0,
current: 0,
},
_timer: {
pausedAt: null,
},
runtime: {
actualStart: 100,
plannedStart: 150,
},
} as RuntimeState;
const { absoluteOffset, relativeOffset } = getRuntimeOffset(state);
expect(absoluteOffset).toBe(50);
expect(relativeOffset).toBe(0);
});
});
describe('getTimerPhase()', () => {
it('should be None if the timer is not running', () => {
const state = {
timer: {
addedTime: 0,
current: null,
duration: null,
elapsed: null,
expectedFinish: null,
finishedAt: null,
playback: Playback.Stop,
phase: TimerPhase.None,
secondaryTimer: null,
startedAt: null,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.None);
});
it('can be in overtime', () => {
const state = {
timer: {
addedTime: 0,
current: -50,
duration: 1000,
playback: Playback.Play,
},
eventNow: {
timeDanger: 100,
timeWarning: 200,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Overtime);
});
it('can be danger', () => {
const state = {
timer: {
addedTime: 0,
current: 0,
duration: 1000,
playback: Playback.Play,
},
eventNow: {
timeDanger: 100,
timeWarning: 200,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Danger);
});
it('can be warning', () => {
const state = {
timer: {
addedTime: 0,
current: 150,
duration: 1000,
playback: Playback.Play,
},
eventNow: {
timeDanger: 100,
timeWarning: 200,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Warning);
});
it('it default if the timer is playing and there is none of the above', () => {
const state = {
timer: {
addedTime: 0,
current: 250,
duration: 1000,
playback: Playback.Play,
},
eventNow: {
timeDanger: 100,
timeWarning: 200,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Default);
});
it('#1042 identifies waiting to roll', () => {
const state = {
clock: 55691050,
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 1,
offset: 0,
plannedStart: 55860000,
plannedEnd: 55880000,
actualStart: null,
expectedEnd: null,
},
timer: {
addedTime: 0,
current: null,
duration: null,
elapsed: 0,
expectedFinish: null,
finishedAt: null,
phase: 'none',
playback: 'roll',
secondaryTimer: 168950,
startedAt: null,
},
_timer: {
forceFinish: null,
pausedAt: null,
},
_rundown: {
totalDelay: 0,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Pending);
});
it('#1042 identifies waiting to roll', () => {
const state = {
clock: 55691050,
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 1,
offset: 0,
plannedStart: 55860000,
plannedEnd: 55880000,
actualStart: null,
expectedEnd: null,
},
timer: {
addedTime: 0,
current: null,
duration: null,
elapsed: 0,
expectedFinish: null,
finishedAt: null,
phase: 'none',
playback: 'roll',
secondaryTimer: 168950,
startedAt: null,
},
_timer: {
forceFinish: null,
pausedAt: null,
},
_rundown: {
totalDelay: 0,
},
} as RuntimeState;
const phase = getTimerPhase(state);
expect(phase).toBe(TimerPhase.Pending);
});
});