mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
8a31cc5018
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
161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
import { type Page, expect, test } from '@playwright/test';
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
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();
|
|
await expect(page.getByText(scriptMarker).first()).toBeVisible();
|
|
});
|
|
|
|
test('asks for a script source when none is selected', async ({ page }) => {
|
|
await page.goto('/teleprompter');
|
|
|
|
await expect(page.getByTestId('teleprompter-view')).toBeVisible();
|
|
await expect(page.getByText('Select which field holds the script in the view options')).toBeVisible();
|
|
});
|
|
|
|
test('space starts and stops the scroll', async ({ page }) => {
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
|
|
expect(await scrollTop(page)).toBe(0);
|
|
|
|
await page.keyboard.press('Space');
|
|
await expect.poll(() => scrollTop(page)).toBeGreaterThan(0);
|
|
|
|
await page.keyboard.press('Space');
|
|
await page.waitForTimeout(200);
|
|
const afterPause = await scrollTop(page);
|
|
await page.waitForTimeout(500);
|
|
|
|
expect(await scrollTop(page)).toBe(afterPause);
|
|
});
|
|
|
|
test('space drives playback instead of opening the navigation menu', async ({ page }) => {
|
|
// the navigation menu binds Space globally, the teleprompter claims it back
|
|
await page.goto('/timer');
|
|
await expect(page.locator('data-testid=timer-view')).toBeVisible();
|
|
await page.keyboard.press('Space');
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
await page.keyboard.press('Space');
|
|
await expect(page.getByRole('dialog')).toHaveCount(0);
|
|
|
|
// and the claim is released once the view goes away
|
|
await page.goto('/timer');
|
|
await expect(page.locator('data-testid=timer-view')).toBeVisible();
|
|
await page.keyboard.press('Space');
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
});
|
|
|
|
test('arrow keys nudge and home rewinds', async ({ page }) => {
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
|
|
await page.keyboard.press('ArrowDown');
|
|
await page.keyboard.press('ArrowDown');
|
|
await expect.poll(() => scrollTop(page)).toBeGreaterThan(0);
|
|
|
|
// the rewind eases rather than snapping, so poll rather than guess a duration
|
|
await page.keyboard.press('Home');
|
|
await expect.poll(() => scrollTop(page)).toBe(0);
|
|
});
|
|
|
|
test('keeps a scroll it did not make itself', async ({ page }) => {
|
|
// the animation frame loop runs for the lifetime of the view, so it has to
|
|
// notice when something else moves the scroller, otherwise a scrollbar drag
|
|
// or find in page would be snapped back on the next frame
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
|
|
await scroller(page).evaluate((element) => {
|
|
element.scrollTop = 400;
|
|
});
|
|
|
|
await page.waitForTimeout(500);
|
|
expect(await scrollTop(page)).toBe(400);
|
|
});
|
|
|
|
test('arrow keys change the speed', async ({ page }) => {
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
|
|
const readout = page.getByTestId('teleprompter-speed');
|
|
const before = Number(await readout.innerText().then((text) => text.replace(/\D/g, '')));
|
|
|
|
await page.keyboard.press('ArrowRight');
|
|
const faster = Number(await readout.innerText().then((text) => text.replace(/\D/g, '')));
|
|
expect(faster).toBeGreaterThan(before);
|
|
|
|
await page.keyboard.press('ArrowLeft');
|
|
const slower = Number(await readout.innerText().then((text) => text.replace(/\D/g, '')));
|
|
expect(slower).toBe(before);
|
|
});
|
|
|
|
test('f mirrors the view for a beam splitter rig', async ({ page }) => {
|
|
await page.goto(teleprompterUrl);
|
|
await expect(scroller(page)).toBeVisible();
|
|
|
|
const view = page.getByTestId('teleprompter-view');
|
|
await page.keyboard.press('f');
|
|
|
|
// a horizontal flip is a negative x scale in the computed matrix
|
|
const transform = await view.evaluate((element) => getComputedStyle(element).transform);
|
|
expect(transform.startsWith('matrix(-1')).toBe(true);
|
|
});
|
|
|
|
test('honours the flip and reading line params', async ({ page }) => {
|
|
await page.goto('/teleprompter?script=note&flipV=true&readingLine=arrows');
|
|
|
|
const view = page.getByTestId('teleprompter-view');
|
|
const transform = await view.evaluate((element) => getComputedStyle(element).transform);
|
|
// a vertical flip leaves x positive and makes the y scale negative
|
|
expect(transform).toMatch(/^matrix\(1, 0, 0, -1/);
|
|
});
|
|
});
|