Files
ontime/e2e/tests/features/215-teleprompter.spec.ts
T
Claude bb8f147575 fix(teleprompter): keep the transport row fixed and label every control
The follow control was rendered only while the follow could be re-engaged,
so it appeared and vanished mid show and moved every control after it. An
operator reaching for pause would press whatever had slid under their
finger. It is now always present and goes disabled when there is nothing to
re-engage, which is how the rest of the row already behaved.

The controls also could not explain themselves: the follow target in
particular is not a guessable icon. Each one now carries a tooltip naming
the action and its key, using the shared Tooltip the editor uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
2026-08-18 18:24:05 +00:00

164 lines
6.0 KiB
TypeScript

import { type Page, expect, test } from '@playwright/test';
import { scriptMarker, seedScript } from '../utils/seedScript';
/**
* 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';
function scroller(page: Page) {
return page.getByTestId('teleprompter-scroller');
}
function scrollTop(page: Page) {
return scroller(page).evaluate((element) => element.scrollTop);
}
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('keeps every transport control in place', async ({ page }) => {
// a control which appears mid show moves the ones after it, and the operator
// reaches for pause and presses whatever slid under their finger instead
await page.goto(teleprompterUrl);
await expect(scroller(page)).toBeVisible();
const controls = page.locator('.teleprompter__controls button');
const follow = page.getByTestId('teleprompter-follow');
await expect(controls).toHaveCount(6);
await expect(follow).toBeVisible();
await expect(follow).toBeDisabled();
// taking over by hand offers the follow back, without restacking the row
await page.mouse.move(640, 300);
await page.mouse.wheel(0, 200);
await expect(controls).toHaveCount(6);
await expect(follow).toBeVisible();
});
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=false');
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/);
// a boolean which defaults to true has to be switchable off from the url
await expect(page.locator('.teleprompter__reading-marker')).toHaveCount(0);
});
});