Files
ontime/e2e/tests/features/215-multi-edit.spec.ts
T
Claude 34ca715fb7 feat(rundown): batch edit event duration, model conflicts with a symbol
Two follow ups on multi event editing.

Duration is now editable across a selection. The batch endpoint already
applies every mutation to a cloned rundown and calls processRundown once on
commit, so a batched duration cascades correctly through linked events with
a single recalculation, and the server infers the duration lock for entries
which were locked to their end.

Start and end times remain excluded: they are absolute points in time, so
giving several events the same value collapses every linked event to a zero
duration. Only the duration is offered in a multi selection.

The optimistic update is skipped when the patch carries a duration, since
the resulting schedule cannot be resolved on the client.

Conflicting values are now marked with a dedicated symbol rather than
undefined, which was doing double duty for "the entries disagree" and "the
field is absent". The distinction was already leaking into the custom field
merge, which needed a key lookup to tell the two apart. The symbol also
makes it a type error to assign a merged value into an entry patch, where it
would previously have been dropped silently on serialisation.

Components keep taking plain optional values: the conflict is resolved once
at the editor boundary, so no indeterminate handling spreads into the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDETGBrhwgAqgTHjmrmJGJ
2026-07-25 12:39:17 +00:00

78 lines
3.9 KiB
TypeScript

import { expect, test } from '@playwright/test';
test('Editing multiple events', async ({ page }) => {
await page.goto('/editor');
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 two events with distinct titles
await page.getByRole('button', { name: 'Create Event' }).click();
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
await page.getByTestId('entry-1').getByTestId('entry__title').fill('first');
await page.getByTestId('entry-1').getByTestId('entry__title').press('Enter');
await page.getByTestId('entry-2').getByTestId('entry__title').fill('second');
await page.getByTestId('entry-2').getByTestId('entry__title').press('Enter');
const editor = page.getByTestId('editor-container');
// a single selection shows the full schedule and the event id
await page.getByTestId('entry-1').getByTestId('rundown-event').click();
await expect(editor.getByTestId('time-input-timeStart')).toBeVisible();
await expect(editor.getByLabel('Title', { exact: true })).toHaveValue('first');
// selecting both events shows a merged view
await page
.getByTestId('entry-2')
.getByTestId('rundown-event')
.click({ modifiers: ['Shift'] });
await expect(editor.locator('#eventId')).toHaveValue('2 events selected');
await expect(editor.getByText('Automations are not available when editing multiple events')).toBeVisible();
// start and end times are unique to an event, only the duration can be batched
await expect(editor.getByTestId('time-input-timeStart')).toBeHidden();
await expect(editor.getByTestId('time-input-timeEnd')).toBeHidden();
await expect(editor.getByTestId('time-input-duration')).toBeVisible();
// fields which do not agree are shown as mixed
const title = editor.getByLabel('Title', { exact: true });
await expect(title).toHaveValue('');
await expect(title).toHaveAttribute('placeholder', 'Mixed');
// leaving a mixed field without editing it does not overwrite the events
await title.click();
await page.keyboard.press('Tab');
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('first');
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('second');
// editing a field applies it to the whole selection
await title.fill('shared title');
await title.press('Enter');
await expect(page.getByTestId('entry-1').getByTestId('entry__title')).toHaveValue('shared title');
await expect(page.getByTestId('entry-2').getByTestId('entry__title')).toHaveValue('shared title');
// the value is no longer mixed
await expect(editor.getByLabel('Title', { exact: true })).toHaveValue('shared title');
// a batched duration is applied to every event and the rundown is recalculated once
const duration = editor.getByTestId('time-input-duration');
await duration.click();
await duration.fill('5m');
await duration.press('Enter');
await expect(page.getByTestId('entry-1').getByTestId('time-input-duration')).toHaveValue('00:05:00');
await expect(page.getByTestId('entry-2').getByTestId('time-input-duration')).toHaveValue('00:05:00');
// the second event is linked, so its start follows the new end of the first
await expect(page.getByTestId('entry-2').getByTestId('time-input-timeStart')).toHaveValue('00:05:00');
await expect(page.getByTestId('entry-2').getByTestId('time-input-timeEnd')).toHaveValue('00:10:00');
// going back to a single selection restores the full editor
await page.getByTestId('entry-1').getByTestId('rundown-event').click();
await expect(editor.getByTestId('time-input-timeStart')).toBeVisible();
await expect(editor.locator('#eventId')).not.toHaveValue('2 events selected');
});