mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 21:03:29 +00:00
323b30ea68
Review pass over the view. Removed: estimateWordsPerLine and linesPerMinuteToWordsPerMinute, written for a words-per-minute readout that was never built; play and pause on the controller, which nothing outside the hook called; and three constants that were exported but never imported. The tick function was being reassigned to a ref on every render, which is a side effect during render. It only ever touched refs and state setters, so it is now a stable callback and the ref is gone. The synthetic contentKey string is replaced by the memoised blocks array it was standing in for. It was also a dependency of the ResizeObserver effect, which tore the observer down and rebuilt it for no gain: the observer already covers every reflow that changes the document. ScriptBlock was memoised but never actually memoising, because the parent built its ref callback inline and handed it a new identity every render. That also churned the follow map, unregistering and re-registering every block. The id is now bound inside the block against a stable callback. Tests: dropped six that asserted arithmetic identities or wrapped clamps rather than behaviour, and added three for branches that were untested, group titles across and back into a group, and a heading with no cue. The e2e rewind assertion waited 600ms for an eased scroll that needs about 900ms from a nudge and a second from the bottom of a long script; it now polls. The navigation menu assertion raced app hydration and now waits for the view, as the existing navigation tests do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
121 lines
4.6 KiB
TypeScript
121 lines
4.6 KiB
TypeScript
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.
|
|
*/
|
|
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('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();
|
|
});
|
|
|
|
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('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/);
|
|
});
|
|
});
|