From 50599eccd2dacd1ce8e82ea4bb3ded05612b783b Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 2 Nov 2025 14:35:23 +0100 Subject: [PATCH] refactor: show countdown if less than 10 min from start --- .../src/views/countdown/countdown.utils.ts | 28 ++--- .../src/views/timeline/TimelineSections.tsx | 4 +- .../src/views/timeline/timeline.utils.ts | 2 +- e2e/tests/features/212-time-until.spec.ts | 111 ++++++++++++------ 4 files changed, 86 insertions(+), 59 deletions(-) diff --git a/apps/client/src/views/countdown/countdown.utils.ts b/apps/client/src/views/countdown/countdown.utils.ts index a7f603e81..2f7895eb1 100644 --- a/apps/client/src/views/countdown/countdown.utils.ts +++ b/apps/client/src/views/countdown/countdown.utils.ts @@ -1,9 +1,8 @@ import { EntryId, MaybeNumber, OffsetMode, OntimeEntry, OntimeEvent, OntimeReport, Playback } from 'ontime-types'; -import { getExpectedStart, MILLIS_PER_MINUTE, removeSeconds } from 'ontime-utils'; +import { getExpectedStart, MILLIS_PER_MINUTE, millisToString, removeLeadingZero } from 'ontime-utils'; import { useCountdownSocket } from '../../common/hooks/useSocket'; import { ExtendedEntry } from '../../common/utils/rundownMetadata'; -import { timerPlaceholderMin } from '../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../common/utils/time'; import { type TranslationKey, useTranslation } from '../../translation/TranslationProvider'; @@ -36,23 +35,6 @@ export const timerProgress: TimerMessage = { done: 'countdown.ended', }; -export function getFormattedTime( - value: MaybeNumber, - status: ProgressStatus, - minText: string, - secText: string, - dueText: string, -) { - if (value === null) return timerPlaceholderMin; - if (status === 'future' || status === 'live') { - if (value <= 0) return dueText.toUpperCase(); - return formatDuration(value, value > MILLIS_PER_MINUTE * 2) - .replace('m', `${minText} `) - .replace('s', secText); - } - return removeSeconds(formatTime(value)); -} - /** * Returns a parsed timer and relevant status message * Handles events in different days but disregards whether an event has actually played @@ -65,7 +47,11 @@ export function useSubscriptionDisplayData( const bigDuration = (value: number) => { if (value <= 0) return getLocalizedString('countdown.overtime').toUpperCase(); - return formatDuration(value, value > MILLIS_PER_MINUTE * 2) + if (value < MILLIS_PER_MINUTE * 10) { + return removeLeadingZero(millisToString(value)); + } + + return formatDuration(value, value > MILLIS_PER_MINUTE * 10) .replace('m', `${getLocalizedString('common.minutes')} `) .replace('s', getLocalizedString('common.seconds')); }; @@ -90,7 +76,7 @@ export function useSubscriptionDisplayData( return { status: 'pending', statusDisplay: getLocalizedString(timerProgress['pending']), - timeDisplay: ' ', + timeDisplay: ' ', }; } diff --git a/apps/client/src/views/timeline/TimelineSections.tsx b/apps/client/src/views/timeline/TimelineSections.tsx index 296534deb..13545af73 100644 --- a/apps/client/src/views/timeline/TimelineSections.tsx +++ b/apps/client/src/views/timeline/TimelineSections.tsx @@ -31,7 +31,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect if (timeToStart <= 0) { nextStatus = dueText; } else { - nextStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2); + nextStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 10); } } @@ -40,7 +40,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect if (timeToStart <= 0) { followedByStatus = dueText; } else { - followedByStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2); + followedByStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 10); } } diff --git a/apps/client/src/views/timeline/timeline.utils.ts b/apps/client/src/views/timeline/timeline.utils.ts index a1334dde8..ab4cd85dc 100644 --- a/apps/client/src/views/timeline/timeline.utils.ts +++ b/apps/client/src/views/timeline/timeline.utils.ts @@ -82,7 +82,7 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str return 'pending'; } - return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2); + return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 10); } interface ScopedRundownData { diff --git a/e2e/tests/features/212-time-until.spec.ts b/e2e/tests/features/212-time-until.spec.ts index 1569b6aa2..8f55b5f80 100644 --- a/e2e/tests/features/212-time-until.spec.ts +++ b/e2e/tests/features/212-time-until.spec.ts @@ -1,4 +1,4 @@ -import { expect, Locator, test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test('time until absolute', async ({ context }) => { const editor = await context.newPage(); @@ -27,45 +27,76 @@ test('time until absolute', async ({ context }) => { await editor.getByTestId('entry-4').getByTestId('rundown-event').click(); ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue()); - await countdown.goto('/countdown?' + ids.join('&sub=')); + await countdown.goto(`/countdown?${ids.join('&sub=')}`); - const locatorElements2 = [ - editor.getByTestId('entry-2').getByTestId('rundown-event'), - op.getByTestId('2').getByTestId('time-until'), - timeline.getByTestId('2'), - timeline.getByTestId('next'), - countdown.getByTestId('2'), - ]; - const locatorElements3 = [ - editor.getByTestId('entry-3').getByTestId('rundown-event'), - op.getByTestId('3').getByTestId('time-until'), - timeline.getByTestId('3'), - timeline.getByTestId('followedBy'), - countdown.getByTestId('3'), - ]; - const locatorElements4 = [ - editor.getByTestId('entry-4').getByTestId('rundown-event'), - op.getByTestId('4').getByTestId('time-until'), - timeline.getByTestId('4'), - countdown.getByTestId('4'), - ]; + // Create reusable locator references for different elements + const entry2 = { + editorEvent: editor.getByTestId('entry-2').getByTestId('rundown-event'), + opTimeUntil: op.getByTestId('2').getByTestId('time-until'), + timelineItem: timeline.getByTestId('2'), + timelineNext: timeline.getByTestId('next'), + countdownItem: countdown.getByTestId('2'), + }; + + const entry3 = { + editorEvent: editor.getByTestId('entry-3').getByTestId('rundown-event'), + opTimeUntil: op.getByTestId('3').getByTestId('time-until'), + timelineItem: timeline.getByTestId('3'), + timelineFollowedBy: timeline.getByTestId('followedBy'), + countdownItem: countdown.getByTestId('3'), + }; + + const entry4 = { + editorEvent: editor.getByTestId('entry-4').getByTestId('rundown-event'), + opTimeUntil: op.getByTestId('4').getByTestId('time-until'), + timelineItem: timeline.getByTestId('4'), + countdownItem: countdown.getByTestId('4'), + }; await editor.getByRole('button', { name: 'Absolute' }).click(); await editor.getByTestId('entry-1').getByLabel('Start event').click(); await expect(editor.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h await editor.getByLabel('Pause event').click(); - await testTimes(locatorElements2, '9m'); - await testTimes(locatorElements3, '19m'); - await testTimes(locatorElements4, '29m'); + // 1. initial check + await expect(entry2.editorEvent).toContainText('9m'); + await expect(entry2.opTimeUntil).toContainText('9m'); + await expect(entry2.timelineItem).toContainText('9m59s'); + await expect(entry2.timelineNext).toContainText('9m59s'); + await expect(entry2.countdownItem).toContainText('10:00'); + + await expect(entry3.editorEvent).toContainText('19m'); + await expect(entry3.opTimeUntil).toContainText('19m'); + await expect(entry3.timelineItem).toContainText('19m'); + await expect(entry3.timelineFollowedBy).toContainText('19m'); + await expect(entry3.countdownItem).toContainText('19m'); + + await expect(entry4.editorEvent).toContainText('29m'); + await expect(entry4.opTimeUntil).toContainText('29m'); + await expect(entry4.timelineItem).toContainText('29m'); + await expect(entry4.countdownItem).toContainText('29m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').click(); await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); - await testTimes(locatorElements2, '5h59m'); - await testTimes(locatorElements3, '6h9m'); - await testTimes(locatorElements4, '6h19m'); + // 2. check after duration change + await expect(entry2.editorEvent).toContainText('5h59m'); + await expect(entry2.opTimeUntil).toContainText('5h59m'); + await expect(entry2.timelineItem).toContainText('5h59m'); + await expect(entry2.timelineNext).toContainText('5h59m'); + await expect(entry2.countdownItem).toContainText('5h59m'); + + await expect(entry3.editorEvent).toContainText('6h9m'); + await expect(entry3.opTimeUntil).toContainText('6h9m'); + await expect(entry3.timelineItem).toContainText('6h9m'); + await expect(entry3.timelineFollowedBy).toContainText('6h9m'); + await expect(entry3.countdownItem).toContainText('6h9m'); + + await expect(entry4.editorEvent).toContainText('6h19m'); + await expect(entry4.opTimeUntil).toContainText('6h19m'); + await expect(entry4.timelineItem).toContainText('6h19m'); + await expect(entry4.countdownItem).toContainText('6h19m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').click(); await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); @@ -75,14 +106,24 @@ test('time until absolute', async ({ context }) => { await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); - await testTimes(locatorElements2, '30s'); - await testTimes(locatorElements3, '10m'); - await testTimes(locatorElements4, '20m'); -}); + // 3. check after final duration change + await expect(entry2.editorEvent).toContainText('30s'); + await expect(entry2.opTimeUntil).toContainText('30s'); + await expect(entry2.timelineItem).toContainText('30s'); + await expect(entry2.timelineNext).toContainText('30s'); + await expect(entry2.countdownItem).toContainText('0:30'); -async function testTimes(locators: Locator[], value: string) { - await locators.map(async (locator) => await expect(locator).toContainText(value)); -} + await expect(entry3.editorEvent).toContainText('10m'); + await expect(entry3.opTimeUntil).toContainText('10m'); + await expect(entry3.timelineItem).toContainText('10m'); + await expect(entry3.timelineFollowedBy).toContainText('10m'); + await expect(entry3.countdownItem).toContainText('10m'); + + await expect(entry4.editorEvent).toContainText('20m'); + await expect(entry4.opTimeUntil).toContainText('20m'); + await expect(entry4.timelineItem).toContainText('20m'); + await expect(entry4.countdownItem).toContainText('20m'); +}); test('time until relative', async ({ context }) => { const editor = await context.newPage();