Update test time until (#1809)

* test views

test timeline

* timeline show seconds when less than 2m

* fix: countown typings

* refactor: backstage schedule item

* chore: lint
This commit is contained in:
Alex Christoffer Rasmussen
2025-10-07 12:27:44 +02:00
parent 57315595a5
commit c4e6215b29
13 changed files with 183 additions and 137 deletions
@@ -71,7 +71,7 @@ function OperatorEvent({
]);
return (
<div className={operatorClasses} ref={selectedRef} onContextMenu={handleLongPress} {...mouseHandlers}>
<div className={operatorClasses} data-testid={cue} ref={selectedRef} onContextMenu={handleLongPress} {...mouseHandlers}>
<div className={style.binder} style={{ ...cueColours }}>
<span className={style.cue}>{cue}</span>
</div>
@@ -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 <span className={style.timeUntil}>{timeUntilString}</span>;
return <span className={style.timeUntil} data-testid='time-until'>{timeUntilString}</span>;
}
@@ -50,7 +50,7 @@ export function StartTimes() {
<Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
</div>
<div className={style.labelledElement}>
<div className={style.labelledElement} data-testid='actual-start-time'>
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
<span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
</div>
@@ -33,6 +33,7 @@ export default function Schedule({ className }: ScheduleProps) {
skip={event.skip}
title={event.title}
timeEnd={event.timeEnd}
cue={event.cue}
/>
);
})}
@@ -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 (
<ExpectedScheduleItem
timeStart={timeStart}
dayOffset={dayOffset}
delay={delay}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
countToEnd={countToEnd}
duration={duration}
colour={colour}
skip={skip}
title={title}
/>
);
}
if (delay > 0) {
return (
<DelayedScheduleItem
timeStart={timeStart}
delay={delay}
colour={colour}
skip={skip}
timeEnd={timeEnd}
title={title}
/>
);
}
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
<li className={cx(['entry', skip && 'entry--skip'])} data-testid={cue}>
<div className='entry-times'>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
{showExpected ? (
<ExpectedScheduleItem
timeStart={timeStart}
dayOffset={dayOffset}
delay={delay}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
countToEnd={countToEnd}
duration={duration}
colour={colour}
/>
) : delay > 0 ? (
<DelayedScheduleItem timeStart={timeStart} delay={delay} colour={colour} timeEnd={timeEnd} />
) : (
<PlannedScheduleItem timeStart={timeStart} timeEnd={timeEnd} colour={colour} />
)}
</div>
<div className='entry-title'>{title}</div>
</li>
);
}
function PlannedScheduleItem({
timeStart,
timeEnd,
colour,
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'colour'>) {
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
return (
<>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
</>
);
}
function DelayedScheduleItem({
timeStart,
timeEnd,
title,
colour,
skip,
delay,
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'title' | 'colour' | 'skip' | 'delay'>) {
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'colour' | 'delay'>) {
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
const delayedStart = formatTime(timeStart + delay, formatOptions);
const delayedEnd = formatTime(timeEnd + delay, formatOptions);
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
<div className='entry-times'>
<span className='entry-times--delayed'>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
</span>
<span className='entry-times--delay'>
<SuperscriptTime time={delayedStart} />
<SuperscriptTime time={delayedEnd} />
</span>
</div>
<div className='entry-title'>{title}</div>
</li>
<>
<span className='entry-times--delayed'>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
</span>
<span className='entry-times--delay'>
<SuperscriptTime time={delayedStart} />
<SuperscriptTime time={delayedEnd} />
</span>
</>
);
}
@@ -133,9 +128,7 @@ function ExpectedScheduleItem({
countToEnd,
colour,
duration,
skip,
title,
}: Omit<ScheduleItemProps, 'timeEnd'>) {
}: Omit<ScheduleItemProps, 'timeEnd' | 'cue' | 'skip' | 'title'>) {
const expectedStartData = useExpectedStartData();
const { expectedStart, expectedEnd, plannedEnd } = getExpectedTimesFromExtendedEvent(
{
@@ -151,15 +144,12 @@ function ExpectedScheduleItem({
);
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
<div className='entry-times'>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} />
</div>
<div className='entry-title'>{title}</div>
</li>
<>
<span className='entry-colour' style={{ backgroundColor: colour }} />
<ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} />
</>
);
}
@@ -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 (
<div className='list-container'>
{events.map((event: OntimeEvent, index: number) => {
{events.map((event, index) => {
const title = event.title || '{no title}';
const isSelected = selectedIds.has(event.id);
@@ -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}
>
<div className='sub__binder' style={{ '--user-color': event.colour }} />
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
@@ -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}
/>
);
@@ -25,6 +25,7 @@ interface TimelineEntryProps {
isLinkedToLoaded: boolean;
title: string;
width: number;
cue: string;
ref?: RefObject<HTMLDivElement | null>;
}
@@ -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' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
<div
@@ -111,7 +111,7 @@
color: $red-500;
}
.section-content--next {
.section-content--followedBy .section-content--next {
color: $green-500;
}
@@ -1,4 +1,5 @@
import { OntimeEvent } from 'ontime-types';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { useExpectedStartData } from '../../common/hooks/useSocket';
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
@@ -30,7 +31,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
if (timeToStart <= 0) {
nextStatus = dueText;
} else {
nextStatus = formatDuration(timeToStart);
nextStatus = formatDuration(timeToStart, timeToStart > 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'
/>
</div>
);
@@ -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 (
<div className={sectionClasses}>
<div className={sectionClasses} data-testid={category}>
<div className='section-title'>
<span className='section-title__label'>{title}</span>
{status && <span className='section-title__status'>{status}</span>}
@@ -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 {
+104 -56
View File
@@ -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<string>();
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();
});