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 ( 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 }}> <div className={style.binder} style={{ ...cueColours }}>
<span className={style.cue}>{cue}</span> <span className={style.cue}>{cue}</span>
</div> </div>
@@ -167,5 +167,5 @@ function TimeUntil({ timeStart, delay, dayOffset, totalGap, isLinkedToLoaded }:
const isDue = timeUntil < MILLIS_PER_SECOND; const isDue = timeUntil < MILLIS_PER_SECOND;
const timeUntilString = isDue ? 'DUE' : `${formatDuration(Math.abs(timeUntil), timeUntil > 2 * MILLIS_PER_MINUTE)}`; 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} />} /> <Tooltip text='Planned start time' render={<TbCalendarPin className={style.icon} />} />
<span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span> <span className={cx([style.time, plannedStart === null && style.muted])}>{plannedStartText}</span>
</div> </div>
<div className={style.labelledElement}> <div className={style.labelledElement} data-testid='actual-start-time'>
<Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} /> <Tooltip text='Actual start time' render={<TbCalendarClock className={style.icon} />} />
<span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span> <span className={cx([style.time, actualStart === null && style.muted])}>{formattedTime(actualStart)}</span>
</div> </div>
@@ -33,6 +33,7 @@ export default function Schedule({ className }: ScheduleProps) {
skip={event.skip} skip={event.skip}
title={event.title} title={event.title}
timeEnd={event.timeEnd} timeEnd={event.timeEnd}
cue={event.cue}
/> />
); );
})} })}
@@ -29,6 +29,7 @@ type ScheduleItemProps = Pick<
| 'skip' | 'skip'
| 'title' | 'title'
| 'timeEnd' | 'timeEnd'
| 'cue'
>; >;
export default function ScheduleItem({ export default function ScheduleItem({
@@ -43,84 +44,78 @@ export default function ScheduleItem({
skip, skip,
title, title,
timeEnd, timeEnd,
cue,
}: ScheduleItemProps) { }: ScheduleItemProps) {
const { showExpected } = useScheduleOptions(); 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 ( return (
<li className={cx(['entry', skip && 'entry--skip'])}> <li className={cx(['entry', skip && 'entry--skip'])} data-testid={cue}>
<div className='entry-times'> <div className='entry-times'>
<span className='entry-colour' style={{ backgroundColor: colour }} /> {showExpected ? (
<SuperscriptTime time={start} /> <ExpectedScheduleItem
timeStart={timeStart}
<SuperscriptTime time={end} /> 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>
<div className='entry-title'>{title}</div> <div className='entry-title'>{title}</div>
</li> </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({ function DelayedScheduleItem({
timeStart, timeStart,
timeEnd, timeEnd,
title,
colour, colour,
skip,
delay, delay,
}: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'title' | 'colour' | 'skip' | 'delay'>) { }: Pick<ScheduleItemProps, 'timeStart' | 'timeEnd' | 'colour' | 'delay'>) {
const start = formatTime(timeStart, formatOptions); const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions); const end = formatTime(timeEnd, formatOptions);
const delayedStart = formatTime(timeStart + delay, formatOptions); const delayedStart = formatTime(timeStart + delay, formatOptions);
const delayedEnd = formatTime(timeEnd + delay, formatOptions); const delayedEnd = formatTime(timeEnd + delay, formatOptions);
return ( return (
<li className={cx(['entry', skip && 'entry--skip'])}> <>
<div className='entry-times'> <span className='entry-times--delayed'>
<span className='entry-times--delayed'> <span className='entry-colour' style={{ backgroundColor: colour }} />
<span className='entry-colour' style={{ backgroundColor: colour }} /> <SuperscriptTime time={start} />
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
<SuperscriptTime time={end} /> </span>
</span> <span className='entry-times--delay'>
<span className='entry-times--delay'> <SuperscriptTime time={delayedStart} />
<SuperscriptTime time={delayedStart} />
<SuperscriptTime time={delayedEnd} />
<SuperscriptTime time={delayedEnd} /> </span>
</span> </>
</div>
<div className='entry-title'>{title}</div>
</li>
); );
} }
@@ -133,9 +128,7 @@ function ExpectedScheduleItem({
countToEnd, countToEnd,
colour, colour,
duration, duration,
skip, }: Omit<ScheduleItemProps, 'timeEnd' | 'cue' | 'skip' | 'title'>) {
title,
}: Omit<ScheduleItemProps, 'timeEnd'>) {
const expectedStartData = useExpectedStartData(); const expectedStartData = useExpectedStartData();
const { expectedStart, expectedEnd, plannedEnd } = getExpectedTimesFromExtendedEvent( const { expectedStart, expectedEnd, plannedEnd } = getExpectedTimesFromExtendedEvent(
{ {
@@ -151,15 +144,12 @@ function ExpectedScheduleItem({
); );
return ( return (
<li className={cx(['entry', skip && 'entry--skip'])}> <>
<div className='entry-times'> <span className='entry-colour' style={{ backgroundColor: colour }} />
<span className='entry-colour' style={{ backgroundColor: colour }} /> <ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
<ExpectedTime expectedTime={expectedStart} plannedTime={timeStart} />
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} />
<ExpectedTime expectedTime={expectedEnd} plannedTime={plannedEnd} /> </>
</div>
<div className='entry-title'>{title}</div>
</li>
); );
} }
@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5'; import { IoArrowBack, IoClose, IoSaveOutline } from 'react-icons/io5';
import { useNavigate } from 'react-router'; 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 Button from '../../common/components/buttons/Button';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
@@ -12,7 +12,7 @@ import { makeSubscriptionsUrl } from './countdown.utils';
import './Countdown.scss'; import './Countdown.scss';
interface CountdownSelectProps { interface CountdownSelectProps {
events: OntimeEvent[]; events: PlayableEvent[];
subscriptions: EntryId[]; subscriptions: EntryId[];
disableEdit: () => void; disableEdit: () => void;
} }
@@ -52,7 +52,7 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
return ( return (
<div className='list-container'> <div className='list-container'>
{events.map((event: OntimeEvent, index: number) => { {events.map((event, index) => {
const title = event.title || '{no title}'; const title = event.title || '{no title}';
const isSelected = selectedIds.has(event.id); const isSelected = selectedIds.has(event.id);
@@ -109,6 +109,7 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
key={event.id} key={event.id}
ref={isLive ? selectedRef : undefined} ref={isLive ? selectedRef : undefined}
className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])} className={cx(['sub', isLive && 'sub--live', isArmed && 'sub--armed'])}
data-testid={event.cue}
> >
<div className='sub__binder' style={{ '--user-color': event.colour }} /> <div className='sub__binder' style={{ '--user-color': event.colour }} />
<ScheduleTime event={countdownEvent} showExpected={showExpected} /> <ScheduleTime event={countdownEvent} showExpected={showExpected} />
@@ -101,6 +101,7 @@ function Timeline({ firstStart, rundown, selectedEventId, totalDuration }: Timel
isLinkedToLoaded={event.isLinkedToLoaded} isLinkedToLoaded={event.isLinkedToLoaded}
dayOffset={event.dayOffset} dayOffset={event.dayOffset}
title={event.title} title={event.title}
cue={event.cue}
width={position.width} width={position.width}
/> />
); );
@@ -25,6 +25,7 @@ interface TimelineEntryProps {
isLinkedToLoaded: boolean; isLinkedToLoaded: boolean;
title: string; title: string;
width: number; width: number;
cue: string;
ref?: RefObject<HTMLDivElement | null>; ref?: RefObject<HTMLDivElement | null>;
} }
@@ -46,6 +47,7 @@ export function TimelineEntry({
isLinkedToLoaded, isLinkedToLoaded,
title, title,
width, width,
cue,
ref, ref,
}: TimelineEntryProps) { }: TimelineEntryProps) {
const formattedStartTime = formatTime(start, formatOptions); const formattedStartTime = formatTime(start, formatOptions);
@@ -67,6 +69,7 @@ export function TimelineEntry({
left: `${left}px`, left: `${left}px`,
width: `${width}px`, width: `${width}px`,
}} }}
data-testid={cue}
> >
{status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />} {status === 'live' ? <ActiveBlock /> : <div data-status={status} className={style.timelineBlock} />}
<div <div
@@ -111,7 +111,7 @@
color: $red-500; color: $red-500;
} }
.section-content--next { .section-content--followedBy .section-content--next {
color: $green-500; color: $green-500;
} }
@@ -1,4 +1,5 @@
import { OntimeEvent } from 'ontime-types'; import { OntimeEvent } from 'ontime-types';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { useExpectedStartData } from '../../common/hooks/useSocket'; import { useExpectedStartData } from '../../common/hooks/useSocket';
import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { ExtendedEntry } from '../../common/utils/rundownMetadata';
@@ -30,7 +31,7 @@ export default function TimelineSections({ now, next, followedBy }: TimelineSect
if (timeToStart <= 0) { if (timeToStart <= 0) {
nextStatus = dueText; nextStatus = dueText;
} else { } 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) { if (timeToStart <= 0) {
followedByStatus = dueText; followedByStatus = dueText;
} else { } 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')} title={getLocalizedString('timeline.followedby')}
status={followedByStatus} status={followedByStatus}
content={followedByText} content={followedByText}
category='next' category='followedBy'
/> />
</div> </div>
); );
@@ -4,7 +4,7 @@ import { MaybeString } from 'ontime-types';
import { cx } from '../../../common/utils/styleUtils'; import { cx } from '../../../common/utils/styleUtils';
interface SectionProps { interface SectionProps {
category: 'now' | 'next'; category: 'now' | 'next' | 'followedBy';
content: MaybeString; content: MaybeString;
title: string; title: string;
status?: string; status?: string;
@@ -16,7 +16,7 @@ function TimelineSection({ category, content, title, status }: SectionProps) {
const sectionClasses = cx(['section', category === 'now' && 'section--now']); const sectionClasses = cx(['section', category === 'now' && 'section--now']);
const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']); const contentClasses = cx(['section-content', content ? `section-content--${category}` : 'section-content--subdue']);
return ( return (
<div className={sectionClasses}> <div className={sectionClasses} data-testid={category}>
<div className='section-title'> <div className='section-title'>
<span className='section-title__label'>{title}</span> <span className='section-title__label'>{title}</span>
{status && <span className='section-title__status'>{status}</span>} {status && <span className='section-title__status'>{status}</span>}
@@ -8,6 +8,7 @@ import {
getTimeFrom, getTimeFrom,
isNewLatest, isNewLatest,
MILLIS_PER_HOUR, MILLIS_PER_HOUR,
MILLIS_PER_MINUTE,
} from 'ontime-utils'; } from 'ontime-utils';
import { ExtendedEntry } from '../../common/utils/rundownMetadata'; import { ExtendedEntry } from '../../common/utils/rundownMetadata';
@@ -81,7 +82,7 @@ export function getStatusLabel(timeToStart: number, status: ProgressStatus): str
return 'pending'; return 'pending';
} }
return formatDuration(timeToStart); return formatDuration(timeToStart, timeToStart > MILLIS_PER_MINUTE * 2);
} }
interface ScopedRundownData { 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 }) => { test('time until absolute', async ({ context }) => {
await page.goto('http://localhost:4001/editor'); 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 editor.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click(); await editor.getByRole('button', { name: 'Delete all' }).click();
await page.getByRole('button', { name: 'Create Event' }).click(); await editor.getByRole('button', { name: 'Create Event' }).click();
await page.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: 'Event', exact: true }).nth(1).click(); await editor.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: 'Event', exact: true }).nth(1).click();
await page.getByRole('button', { name: 'Absolute' }).click(); await editor.getByTestId('entry-1').getByTestId('rundown-event').click();
await page.getByTestId('entry-1').getByLabel('Start event').click(); const ids = new Array<string>();
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 ids.push(await editor.getByTestId('editor-container').getByLabel('Event ID (read only)').inputValue());
await page.getByLabel('Pause event').click(); 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 countdown.goto('/countdown?' + ids.join('&sub='));
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click(); const locatorElements2 = [
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h'); editor.getByTestId('entry-2').getByTestId('rundown-event'),
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); 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 editor.getByRole('button', { name: 'Absolute' }).click();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m'); await editor.getByTestId('entry-1').getByLabel('Start event').click();
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m'); 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 testTimes(locatorElements2, '9m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); await testTimes(locatorElements3, '19m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); await testTimes(locatorElements4, '29m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); 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 }) => { async function testTimes(locators: Locator[], value: string) {
await page.goto('http://localhost:4001/editor'); await locators.map(async (locator) => await expect(locator).toContainText(value));
}
await page.getByRole('button', { name: 'Clear all' }).click(); test('time until relative', async ({ context }) => {
await page.getByRole('button', { name: 'Delete all' }).click(); const editor = await context.newPage();
editor.goto('http://localhost:4001/editor');
await page.getByRole('button', { name: 'Create Event' }).click(); await editor.getByRole('button', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Event' }).nth(4).click(); await editor.getByRole('button', { name: 'Delete all' }).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: 'Relative' }).click(); await editor.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').getByLabel('Start event').click(); await editor.getByRole('button', { name: 'Event' }).nth(4).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 editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await page.getByLabel('Pause event').click(); await editor.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m'); await editor.getByRole('button', { name: 'Relative' }).click();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m'); await editor.getByTestId('entry-1').getByLabel('Start event').click();
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m'); 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 expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('9m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h'); await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('19m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('29m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('6h');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter');
await page.getByTestId('entry-1').getByTestId('time-input-duration').click(); await expect(editor.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('5h59m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s'); await expect(editor.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('6h9m');
await page.getByTestId('entry-1').getByTestId('time-input-duration').press('Enter'); await expect(editor.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('6h19m');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('30s'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').click();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toContainText('10m'); await editor.getByTestId('entry-1').getByTestId('time-input-duration').fill('30s');
await expect(page.getByTestId('entry-4').getByTestId('rundown-event')).toContainText('20m'); 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();
}); });