diff --git a/apps/client/src/features/operator/operator-event/OperatorEvent.tsx b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx index 113ba3484..c2e77add3 100644 --- a/apps/client/src/features/operator/operator-event/OperatorEvent.tsx +++ b/apps/client/src/features/operator/operator-event/OperatorEvent.tsx @@ -71,7 +71,7 @@ function OperatorEvent({ ]); return ( -
+
{cue}
@@ -167,5 +167,5 @@ function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }: const isDue = timeUntil < MILLIS_PER_SECOND; const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`; - return {timeUntilString}; + return {timeUntilString}; } diff --git a/apps/client/src/features/overview/composite/TimeElements.tsx b/apps/client/src/features/overview/composite/TimeElements.tsx index df018e237..ca59accaf 100644 --- a/apps/client/src/features/overview/composite/TimeElements.tsx +++ b/apps/client/src/features/overview/composite/TimeElements.tsx @@ -50,7 +50,7 @@ export function StartTimes() { } /> {plannedStartText}
-
+
} /> {formattedTime(actualStart)}
diff --git a/apps/client/src/views/common/schedule/Schedule.tsx b/apps/client/src/views/common/schedule/Schedule.tsx index ae88df2ac..070fad599 100644 --- a/apps/client/src/views/common/schedule/Schedule.tsx +++ b/apps/client/src/views/common/schedule/Schedule.tsx @@ -33,6 +33,7 @@ export default function Schedule({ className }: ScheduleProps) { skip={event.skip} title={event.title} timeEnd={event.timeEnd} + cue={event.cue} /> ); })} diff --git a/apps/client/src/views/common/schedule/ScheduleItem.tsx b/apps/client/src/views/common/schedule/ScheduleItem.tsx index fd69337ff..f04cbdb78 100644 --- a/apps/client/src/views/common/schedule/ScheduleItem.tsx +++ b/apps/client/src/views/common/schedule/ScheduleItem.tsx @@ -29,6 +29,7 @@ type ScheduleItemProps = Pick< | 'skip' | 'title' | 'timeEnd' + | 'cue' >; export default function ScheduleItem({ @@ -43,84 +44,78 @@ export default function ScheduleItem({ skip, title, timeEnd, + cue, }: ScheduleItemProps) { const { showExpected } = useScheduleOptions(); - if (showExpected) { - return ( - - ); - } - - if (delay > 0) { - return ( - - ); - } - - const start = formatTime(timeStart, formatOptions); - const end = formatTime(timeEnd, formatOptions); return ( -
  • +
  • - - - → - + {showExpected ? ( + + ) : delay > 0 ? ( + + ) : ( + + )}
    {title}
  • ); } +function PlannedScheduleItem({ + timeStart, + timeEnd, + colour, +}: Pick) { + const start = formatTime(timeStart, formatOptions); + const end = formatTime(timeEnd, formatOptions); + + return ( + <> + + + → + + + ); +} + function DelayedScheduleItem({ timeStart, timeEnd, - title, colour, - skip, delay, -}: Pick) { +}: Pick) { const start = formatTime(timeStart, formatOptions); const end = formatTime(timeEnd, formatOptions); const delayedStart = formatTime(timeStart + delay, formatOptions); const delayedEnd = formatTime(timeEnd + delay, formatOptions); return ( -
  • -
    - - - - → - - - - - → - - -
    -
    {title}
    -
  • + <> + + + + → + + + + + → + + + ); } @@ -133,9 +128,7 @@ function ExpectedScheduleItem({ countToEnd, colour, duration, - skip, - title, -}: Omit) { +}: Omit) { const expectedStartData = useExpectedStartData(); const { expectedStart, expectedEnd, plannedEnd } = getExpectedTimesFromExtendedEvent( { @@ -151,15 +144,12 @@ function ExpectedScheduleItem({ ); return ( -
  • -
    - - - → - -
    -
    {title}
    -
  • + <> + + + → + + ); } diff --git a/apps/client/src/views/countdown/CountdownSelect.tsx b/apps/client/src/views/countdown/CountdownSelect.tsx index a14131e0f..725026e22 100644 --- a/apps/client/src/views/countdown/CountdownSelect.tsx +++ b/apps/client/src/views/countdown/CountdownSelect.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5'; import { useNavigate } from 'react-router'; -import { EntryId, OntimeEvent } from 'ontime-types'; +import { EntryId, PlayableEvent } from 'ontime-types'; import Button from '../../common/components/buttons/Button'; import { cx } from '../../common/utils/styleUtils'; @@ -12,7 +12,7 @@ import { makeSubscriptionsUrl } from './countdown.utils'; import './Countdown.scss'; interface CountdownSelectProps { - events: OntimeEvent[]; + events: PlayableEvent[]; subscriptions: EntryId[]; disableEdit: () => void; } @@ -52,7 +52,7 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }: return (
    - {events.map((event: OntimeEvent, index: number) => { + {events.map((event, index) => { const title = event.title || '{no title}'; const isSelected = selectedIds.has(event.id); diff --git a/apps/client/src/views/countdown/CountdownSubscriptions.tsx b/apps/client/src/views/countdown/CountdownSubscriptions.tsx index d51ea298c..ba004a304 100644 --- a/apps/client/src/views/countdown/CountdownSubscriptions.tsx +++ b/apps/client/src/views/countdown/CountdownSubscriptions.tsx @@ -109,6 +109,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode key={event.id} ref={isLive ? selectedRef : undefined} className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])} + data-testid={event.cue} >
    diff --git a/apps/client/src/views/timeline/Timeline.tsx b/apps/client/src/views/timeline/Timeline.tsx index dc15fbbee..1dc0134f3 100644 --- a/apps/client/src/views/timeline/Timeline.tsx +++ b/apps/client/src/views/timeline/Timeline.tsx @@ -101,6 +101,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel isLinkedToLoaded={event.isLinkedToLoaded} dayOffset={event.dayOffset} title={event.title} + cue={event.cue} width={position.width} /> ); diff --git a/apps/client/src/views/timeline/TimelineEntry.tsx b/apps/client/src/views/timeline/TimelineEntry.tsx index 3c12e3878..c7f5b3578 100644 --- a/apps/client/src/views/timeline/TimelineEntry.tsx +++ b/apps/client/src/views/timeline/TimelineEntry.tsx @@ -25,6 +25,7 @@ interface TimelineEntryProps { isLinkedToLoaded: boolean; title: string; width: number; + cue: string; ref?: RefObject; } @@ -46,6 +47,7 @@ export function TimelineEntry({ isLinkedToLoaded, title, width, + cue, ref, }: TimelineEntryProps) { const formattedStartTime = formatTime(start, formatOptions); @@ -67,6 +69,7 @@ export function TimelineEntry({ left: `${left}px`, width: `${width}px`, }} + data-testid={cue} > {status === 'live' ? :
    }
    MILLIS_PER_MINUTE * 2); } } @@ -39,7 +40,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect if (timeToStart <= 0) { followedByStatus = dueText; } else { - followedByStatus = formatDuration(timeToStart); + followedByStatus = formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2); } } @@ -56,7 +57,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect title={getLocalizedString('timeline.followedby')} status={followedByStatus} content={followedByText} - category='next' + category='followedBy' />
    ); diff --git a/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx b/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx index 1d5983b3b..2eb2ebe25 100644 --- a/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx +++ b/apps/client/src/views/timeline/timeline-section/TimelineSection.tsx @@ -4,7 +4,7 @@ import { MaybeString } from 'ontime-types'; import { cx } from '../../../common/utils/styleUtils'; interface SectionProps { - category: 'now' | 'next'; + category: 'now' | 'next' | 'followedBy'; content: MaybeString; title: string; status?: string; @@ -16,7 +16,7 @@ function TimelineSection({ category, content, title, status }: SectionProps) { const sectionClasses = cx(['section', category === 'now' && 'section--now']); const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']); return ( -
    +
    {title} {status && {status}} diff --git a/apps/client/src/views/timeline/timeline.utils.ts b/apps/client/src/views/timeline/timeline.utils.ts index ccca7601f..a1334dde8 100644 --- a/apps/client/src/views/timeline/timeline.utils.ts +++ b/apps/client/src/views/timeline/timeline.utils.ts @@ -8,6 +8,7 @@ import { getTimeFrom, isNewLatest, MILLIS_PER_HOUR, + MILLIS_PER_MINUTE, } from 'ontime-utils'; import { ExtendedEntry } from '../../common/utils/rundownMetadata'; @@ -81,7 +82,7 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str return 'pending'; } - return formatDuration(timeToStart); + return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2); } interface ScopedRundownData { diff --git a/e2e/tests/features/212-time-until.spec.ts b/e2e/tests/features/212-time-until.spec.ts index 50d6802db..ace6e1d5b 100644 --- a/e2e/tests/features/212-time-until.spec.ts +++ b/e2e/tests/features/212-time-until.spec.ts @@ -1,77 +1,125 @@ -import { expect, test } from '@playwright/test'; +import { expect, Locator, Page, test } from '@playwright/test'; -test('time until absolute', async ({ page }) => { - await page.goto('http://localhost:4001/editor'); +test('time until absolute', async ({ context }) => { + const editor = await context.newPage(); + const op = await context.newPage(); + const timeline = await context.newPage(); + const countdown = await context.newPage(); + await editor.goto('/editor'); + await op.goto('/op'); + await timeline.goto('/timeline'); - await page.getByRole('button', { name: 'Clear all' }).click(); - await page.getByRole('button', { name: 'Delete all' }).click(); + await editor.getByRole('button', { name: 'Clear all' }).click(); + await editor.getByRole('button', { name: 'Delete all' }).click(); - await page.getByRole('button', { name: 'Create Event' }).click(); - await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); - await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); - await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); + await editor.getByRole('button', { name: 'Create Event' }).click(); + await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); + await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); + await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); - await page.getByRole('button', { name: 'Absolute' }).click(); - await page.getByTestId('entry-1').getByLabel('Start event').click(); - await expect(page.getByTestId('offset')).not.toContainText('00:00:00'); // This might be a bad test requires that the test is not run at 0h - await page.getByLabel('Pause event').click(); + await editor.getByTestId('entry-1').getByTestId('rundown-event').click(); + const ids = new Array(); + ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue()); + await editor.getByTestId('entry-2').getByTestId('rundown-event').click(); + ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue()); + await editor.getByTestId('entry-3').getByTestId('rundown-event').click(); + ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue()); + await editor.getByTestId('entry-4').getByTestId('rundown-event').click(); + ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue()); - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m'); + await countdown.goto('/countdown?' + ids.join('&sub=')); - await page.getByTestId('entry-1').getByTestId('time-input-duration').click(); - await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h'); - await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); + 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'), + ]; - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m'); + 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 page.getByTestId('entry-1').getByTestId('time-input-duration').click(); - await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); - await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); + await testTimes(locatorElements2, '9m'); + await testTimes(locatorElements3, '19m'); + await testTimes(locatorElements4, '29m'); - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); + 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'); + + await editor.getByTestId('entry-1').getByTestId('time-input-duration').click(); + await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); + await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); + + await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); + 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'); }); -test('time until relative', async ({ page }) => { - await page.goto('http://localhost:4001/editor'); +async function testTimes(locators: Locator[], value: string) { + await locators.map(async (locator) => await expect(locator).toContainText(value)); +} - await page.getByRole('button', { name: 'Clear all' }).click(); - await page.getByRole('button', { name: 'Delete all' }).click(); +test('time until relative', async ({ context }) => { + const editor = await context.newPage(); + editor.goto('http://localhost:4001/editor'); - await page.getByRole('button', { name: 'Create Event' }).click(); - await page.getByRole('button', { name: 'Event' }).nth(4).click(); - await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); - await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); + await editor.getByRole('button', { name: 'Clear all' }).click(); + await editor.getByRole('button', { name: 'Delete all' }).click(); - await page.getByRole('button', { name: 'Relative' }).click(); - await page.getByTestId('entry-1').getByLabel('Start event').click(); - await expect(page.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s - await page.getByLabel('Pause event').click(); + await editor.getByRole('button', { name: 'Create Event' }).click(); + await editor.getByRole('button', { name: 'Event' }).nth(4).click(); + await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); + await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click(); - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m'); + await editor.getByRole('button', { name: 'Relative' }).click(); + await editor.getByTestId('entry-1').getByLabel('Start event').click(); + await expect(editor.getByTestId('offset')).toContainText('00:00:00'); // This might be a bad test as it ruires the evaluation to happen within 1s + await editor.getByLabel('Pause event').click(); - await page.getByTestId('entry-1').getByTestId('time-input-duration').click(); - await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h'); - await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); + await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m'); + await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m'); + await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m'); - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m'); + 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 page.getByTestId('entry-1').getByTestId('time-input-duration').click(); - await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); - await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); + await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m'); + await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m'); + await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m'); - await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); - await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); - await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); + await editor.getByTestId('entry-1').getByTestId('time-input-duration').click(); + await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); + await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); - await page.getByRole('button', { name: 'Absolute' }).click(); + await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); + await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); + await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); + + await editor.getByRole('button', { name: 'Absolute' }).click(); });