fix(teleprompter): address review findings and the e2e ordering failure

The e2e run failed because 214-rundown-switch-edit loads a fresh rundown,
leaving the teleprompter spec with nothing to read. The spec now seeds its
own script entry, idempotently, so it no longer depends on which rundown a
previous spec happened to leave loaded.

Review findings:

- PresetView rendered outside ViewLoader, so a view reached through a preset
  never got the project's CSS override stylesheet.
- The help overlay was a bare div. It is now a Dialog, so focus moves into it
  and back out, Escape closes it instead of rewinding the script, and the
  prompter keymap stands down while it is open.
- Space is left unbound rather than made a no-op when a view claims it.
  useHotkeys calls preventDefault before reaching the handler, so an early
  return still swallowed the key and stopped Space activating a focused button.
- The scroll observer attached on mount only, so a scroller which mounted later
  (the empty state resolving into a script) was never measured and could not
  play. Callback refs attach it whenever the elements appear.
- jumpToEnd marked the end state before the document had been measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
Claude
2026-08-17 19:04:32 +00:00
parent 8253f1c119
commit 8a31cc5018
8 changed files with 160 additions and 51 deletions
+30 -5
View File
@@ -1,12 +1,15 @@
import { type Page, expect, test } from '@playwright/test';
/**
* The note field is used as the script source throughout: every event in the
* test fixture has one, which keeps these tests independent of how custom field
* keys happen to be spelled in the fixture.
* The note field is the script source throughout, so that these tests do not
* depend on how custom field keys happen to be spelled.
*/
const teleprompterUrl = '/teleprompter?script=note';
const scriptMarker = 'E2E prompter script';
/** long enough that the document scrolls well past a screen */
const scriptText = `${scriptMarker}. `.repeat(40);
function scroller(page: Page) {
return page.getByTestId('teleprompter-scroller');
}
@@ -15,14 +18,36 @@ function scrollTop(page: Page) {
return scroller(page).evaluate((element) => element.scrollTop);
}
/**
* Puts a known script into whichever rundown happens to be loaded.
*
* These tests used to read the notes of the uploaded fixture, which made them
* depend on every spec that runs before them: 214 creates a fresh rundown and
* leaves it loaded, so by the time this file ran there were no notes anywhere
* and the view was showing its empty state. Seeding is idempotent, so the
* rundown gains one event no matter how many tests run.
*/
async function seedScript(page: Page) {
const rundown = await (await page.request.get('/data/rundowns/current')).json();
const alreadySeeded = rundown.flatOrder.some((id: string) => rundown.entries[id]?.note?.startsWith(scriptMarker));
if (alreadySeeded) return;
await page.request.post(`/data/rundowns/${rundown.id}/entry`, {
data: { type: 'event', title: 'Teleprompter e2e', note: scriptText },
});
}
test.describe('teleprompter', () => {
test.beforeEach(async ({ page }) => {
await seedScript(page);
});
test('shows the script from the selected source', async ({ page }) => {
await page.goto(teleprompterUrl);
await expect(page.getByTestId('teleprompter-view')).toBeVisible();
await expect(scroller(page)).toBeVisible();
// the fixture uses cue style notes, the first event is Albania
await expect(page.getByText('SF1.01', { exact: true })).toBeVisible();
await expect(page.getByText(scriptMarker).first()).toBeVisible();
});
test('asks for a script source when none is selected', async ({ page }) => {