Files
ontime/e2e/tests/features/209-rundown-shortcuts.spec.ts
T
Claude cd09476858 test(finder): wait for the dialog before keying into it
The close-shortcut test sent its second keystroke as soon as the input
was visible, which is before a dialog with a focus trap has settled.
Waiting for focus instead asserts the same behaviour against a dialog
that is actually ready to receive it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzALEq9gGWwFmwTdgAiFcY
2026-08-18 12:42:40 +00:00

477 lines
23 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');
// waiting for focus rather than visibility, so the dialog has finished opening
// before the next keystroke is sent
await expect(page.getByPlaceholder('Search...')).toBeFocused();
// 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');
// deliberately fired while the caret is still in a title field: the shortcut must not be
// swallowed by the input the user happens to be editing
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
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('Finder searches milestones by cue', 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);
// an event, and a milestone carrying its own cue
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByTestId('entry-1').click();
await page.getByTestId('entry__title').press('Escape');
await page.getByTestId('rundown-event').locator('div').filter({ hasText: '1' }).press('Alt+M');
await expect(page.getByTestId('rundown-milestone')).toHaveCount(1);
await page.getByTestId('rundown-milestone').getByPlaceholder('Cue').fill('MILE9');
await page.getByTestId('rundown-milestone').getByPlaceholder('Cue').press('Enter');
await page.getByTestId('rundown-milestone').getByPlaceholder('Title').fill('zebrafish');
await page.getByTestId('rundown-milestone').getByPlaceholder('Title').press('Enter');
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
// milestones were previously skipped by the cue search even though they carry a cue
await page.getByPlaceholder('Search...').fill('cue MILE9');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
// and remain findable by title
await page.getByPlaceholder('Search...').fill('zebrafish');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
});
test('Finder searches notes and reports the matching field', 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 page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('opening remarks');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
// the note is not shown on the rundown row, so it can only be reached by searching
await page.getByTestId('entry-1').click();
await page.getByLabel('Note', { exact: true }).fill('remember the zebrafish tank');
await page.getByLabel('Note', { exact: true }).press('Tab');
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
// a bare query now reaches the note, and the row explains which field matched
await page.getByPlaceholder('Search...').fill('zebrafish');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
await expect(page.getByTestId('finder-result-match')).toContainText('Note');
await expect(page.getByTestId('finder-result-match')).toContainText('zebrafish');
// scoping to the title excludes it again
await page.getByPlaceholder('Search...').fill('title zebrafish');
await expect(page.getByTestId('finder-result')).toHaveCount(0);
});
test('Finder filter badges scope the search', 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 page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('sound check');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
// typing first, then narrowing with a badge, keeps the search text
await page.getByPlaceholder('Search...').fill('sound');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
const filters = page.getByTestId('finder-filters');
await filters.getByRole('button', { name: 'Note', exact: true }).click();
await expect(page.getByPlaceholder('Search...')).toHaveValue('note sound');
await expect(page.getByTestId('finder-result')).toHaveCount(0);
// switching to another badge replaces the filter rather than stacking
await filters.getByRole('button', { name: 'Title', exact: true }).click();
await expect(page.getByPlaceholder('Search...')).toHaveValue('title sound');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
await expect(page.getByTestId('finder-count')).toContainText('1 result');
// custom fields defined by the project are offered as filters too
await expect(filters.getByRole('button', { name: 'Song', exact: true })).toBeVisible();
await expect(filters.getByRole('button', { name: 'Artist', exact: true })).toBeVisible();
});
test('Finder searches custom fields', 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 page.getByRole('button', { name: 'Create Event' }).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(1);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('interval');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
// custom field values are not shown on the rundown row
await page.getByTestId('entry-1').click();
await page.getByLabel('Artist', { exact: true }).fill('zebrafish collective');
await page.getByLabel('Artist', { exact: true }).press('Tab');
await page.keyboard.press('ControlOrMeta+f');
await expect(page.getByPlaceholder('Search...')).toBeVisible();
// a bare query reaches custom field values, naming the field that matched
await page.getByPlaceholder('Search...').fill('zebrafish');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
await expect(page.getByTestId('finder-result-match')).toContainText('Artist');
// and the field can be scoped explicitly
await page.getByTestId('finder-filters').getByRole('button', { name: 'Artist', exact: true }).click();
await expect(page.getByPlaceholder('Search...')).toHaveValue('Artist zebrafish');
await expect(page.getByTestId('finder-result')).toHaveCount(1);
});
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();
});