mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 20:39:18 +00:00
671df541ef
Two defects surfaced while reviewing the feature. The search shortcut was dead while any input had focus. The hotkey hook skips input elements by default, so the shortcut did nothing while editing an entry title, which is exactly when a user reaches for it. Opt out of that behaviour and drop the local handler that partially worked around it, so a single binding both opens and closes. Milestones were skipped by the cue search even though they carry a cue and display it in the rundown, so filtering by cue could never find one. They are already covered by the title search. Adds e2e coverage for both, and fires the shortcut from a focused input in the existing keyboard test so the first defect stays fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzALEq9gGWwFmwTdgAiFcY
369 lines
17 KiB
TypeScript
369 lines
17 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');
|
|
|
|
// 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('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();
|
|
});
|