Files
ontime/e2e/tests/features/209-rundown-shortcuts.spec.ts
T
Claude cabfc592df fix(finder): correct result selection and dismissal
Track the highlighted result by entry ID rather than by list index.
The index was never reset when results changed, so a background
refetch that shrank the list left it pointing past the end and
selecting threw on an undefined entry. Resolving by ID falls back to
the first result instead, and keeps the user's position across
rundown edits.

Submit the entry belonging to the clicked row instead of whichever
row was highlighted. The two only agreed because a mousemove usually
precedes a click, so touch input navigated to the wrong entry.

Ignore pointer moves that do not change the cursor position: scrolling
the list under a stationary pointer fires a move event which pulled
the selection away from the keyboard cursor. Keep the highlighted row
scrolled into view while navigating.

Close the finder on the search shortcut. Mantine ignores hotkeys while
an input is focused, so the global toggle could not close the modal
once the user was typing. The global Escape handler is dropped: the
dialog already dismisses on Escape, and registering it document wide
conflicts with inline field editing.

Drop the bounds check in the index search, which compared an event
ordinal against the count of all entries. The loop below it already
returns no results when no event carries that index.

Add e2e coverage for clicking a result, picking one with the keyboard,
and closing with the shortcut. This needs the finder rows and the
rundown event row to expose test ids and selection state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzALEq9gGWwFmwTdgAiFcY
2026-08-17 14:52:49 +00:00

332 lines
15 KiB
TypeScript

import { expect, test } from '@playwright/test';
test('Copy-paste', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
// create event
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').click();
await page.getByLabel('Cue', { exact: true }).click();
await page.getByLabel('Cue', { exact: true }).fill('4');
await page.getByLabel('Cue', { exact: true }).press('Enter');
await page.getByTestId('entry-1').click();
await page.getByTestId('entry__title').click();
await page.getByTestId('entry__title').fill('test');
await page.getByTestId('entry__title').press('Enter');
// copy paste below
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).click();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).press('ControlOrMeta+c');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '4' }).press('ControlOrMeta+v');
// assert
await expect(page.getByTestId('entry-2')).toBeVisible();
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('test');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('4');
});
test('Cut-paste', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').getByTestId('entry__title').click();
await page.getByTestId('entry-1').getByTestId('entry__title').fill('first');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await page.getByTestId('entry-2').getByTestId('entry__title').click();
await page.getByTestId('entry-2').getByTestId('entry__title').fill('second');
await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter');
// cut first event, paste below second
await page.getByTestId('entry-1').getByTestId('rundown-event').getByText('1').click();
await page.getByTestId('entry-1').getByTestId('rundown-event').filter({ hasText: '1' }).press('ControlOrMeta+x');
await page.getByTestId('entry-2').getByTestId('rundown-event').getByText('2').click();
await page.getByTestId('entry-2').getByTestId('rundown-event').filter({ hasText: '2' }).press('ControlOrMeta+v');
// we can verify that the entries have swapped places
const events = await page.getByTestId('entry__title').all();
await expect(events[0]).toHaveValue('second');
await expect(events[1]).toHaveValue('first');
});
test('Move', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
// copy move down
await page.getByTestId('entry-1').getByTestId('rundown-event').getByText('1').click();
await page
.getByTestId('entry-1')
.getByTestId('rundown-event')
.filter({ hasText: '1' })
.press('Alt+Control+ArrowDown');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('1');
// move entry three up twice, waiting for each reorder before targeting its new row
await page.getByTestId('entry-3').getByTestId('rundown-event').getByText('3').click();
await page
.getByTestId('entry-3')
.getByTestId('rundown-event')
.filter({ hasText: '3' })
.press('Alt+ControlOrMeta+ArrowUp');
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toContainText('3');
await page
.getByTestId('entry-2')
.getByTestId('rundown-event')
.filter({ hasText: '3' })
.press('Alt+ControlOrMeta+ArrowUp');
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toContainText('3');
});
test('Add group', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByTestId('rundown-group')).toHaveCount(0);
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-group')).toHaveCount(0);
await page.getByPlaceholder(/event title/i).fill('test');
await page.getByTestId('entry-1').click();
// add group below
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+G');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-group')).toHaveCount(1);
await page.getByTestId('rundown-group').getByTestId('entry__title').fill('group below');
// add group above
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+G');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-group')).toHaveCount(2);
await page.getByTestId('entry__title').first().fill('group above');
await expect(page.getByTestId(/entry__title/i).first()).toHaveValue('group above');
await expect(page.getByTestId(/entry__title/i).nth(2)).toHaveValue('group below');
await expect(page.getByTestId('entry-1').getByTestId(/entry__title/)).toHaveValue('test');
});
test('Add delay', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(0);
await page.getByTestId('entry-1').click();
await page.getByTestId('entry__title').press('Escape');
// add delay below
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+D');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(1);
await expect(page.getByTestId('delay-input')).toBeVisible();
// add delay above
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+D');
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await expect(page.getByTestId('rundown-delay')).toHaveCount(2);
await expect(page.getByTestId('entry-0').getByTestId('delay-input')).toBeVisible();
});
test('Add event', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
// create events
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').click();
await page.getByTestId('entry__title').press('Escape');
// add event below
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+E');
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await expect(page.getByTestId('entry-2').getByTestId('rundown-event').getByText('2')).toBeVisible();
// add event above
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Shift+E');
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toContainText('1');
});
test('Delete event', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.goto('/rundown');
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
// create event
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
// delete event
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).click();
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+Backspace');
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Create Event' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create Group' })).toBeVisible();
});
test('Find in rundown', async ({ page }) => {
await page.goto('/rundown');
await expect(page.getByTestId('panel-rundown')).toBeVisible();
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByPlaceholder('Search...')).toBeHidden();
});
test('Close finder with the search shortcut', async ({ page }) => {
await page.goto('/rundown');
await expect(page.getByTestId('panel-rundown')).toBeVisible();
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
// the shortcut has to close the finder while the caret is in the search field
await page.getByPlaceholder('Search...').press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeHidden();
});
test('Finder navigates to the clicked result', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
// create three events which all match the same search
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(3);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('finder one');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.getByTestId('entry-2').getByTestId('entry__title').fill('finder two');
await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter');
await page.getByTestId('entry-3').getByTestId('entry__title').fill('finder three');
await page.getByTestId('entry-3').getByTestId('entry__title').press('Enter');
await page.keyboard.press('ControlOrMeta+f');
await page.getByPlaceholder('Search...').fill('finder');
await expect(page.getByTestId('finder-result')).toHaveCount(3);
/**
* Dispatch the click without moving the pointer first, which is what a touch device does.
* The finder used to submit whichever row was highlighted rather than the one being clicked.
*/
await page.getByTestId('finder-result').nth(2).dispatchEvent('click');
await expect(page.getByPlaceholder('Search...')).toBeHidden();
await expect(page.getByTestId('entry-3').getByTestId('rundown-event')).toHaveAttribute('data-selected', 'true');
await expect(page.getByTestId('entry-1').getByTestId('rundown-event')).toHaveAttribute('data-selected', 'false');
});
test('Finder navigates to the result picked with the keyboard', async ({ page }) => {
await page.goto('/rundown');
await page.getByRole('button', { name: 'Edit' }).click();
// clear rundown
await page.getByRole('button', { name: 'Rundown menu' }).click();
await page.getByRole('menuitem', { name: 'Clear all' }).click();
await page.getByRole('button', { name: 'Delete all' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(0);
// create two events which both match the same search
await page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('finder one');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.getByRole('button', { name: 'Event' }).nth(4).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await page.getByTestId('entry-2').getByTestId('entry__title').fill('finder two');
await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter');
await page.keyboard.press('ControlOrMeta+f');
await page.getByPlaceholder('Search...').fill('finder');
await expect(page.getByTestId('finder-result')).toHaveCount(2);
// the first result is highlighted on open, so one step down lands on the second
await page.getByPlaceholder('Search...').press('ArrowDown');
await expect(page.getByTestId('finder-result').nth(1)).toHaveAttribute('data-selected', 'true');
await page.getByPlaceholder('Search...').press('Enter');
await expect(page.getByPlaceholder('Search...')).toBeHidden();
await expect(page.getByTestId('entry-2').getByTestId('rundown-event')).toHaveAttribute('data-selected', 'true');
});
test('Open settings', async ({ page }) => {
await page.goto('/editor');
await expect(page.getByTestId('editor-container')).toBeVisible();
await page.keyboard.press('ControlOrMeta+,');
await expect(page.getByRole('button', { name: 'Close settings' })).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('button', { name: 'Close settings' })).toBeHidden();
});