mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-03 05:19:08 +00:00
6589bcceee
The params editor could not represent a boolean which defaults to true. Two bugs stacked: - ParamInput fell back with `isStringBoolean(param) ?? defaultValue`, but isStringBoolean answers false for an absent param rather than nothing, so the ?? never fired and every switch opened off whatever its option said. - An unchecked checkbox is absent from the form data rather than present and false, so switching one off wrote no param and the parser fell back to the default the user was trying to leave. Either one alone is invisible while every boolean defaults to false, which is why this surfaced with the teleprompter. Together they made the whole panel look inert: the switch showed off, the view showed on, and Apply did nothing. Prompter changes from the review: - Speed is calibrated against the reading rate rather than picked for feel. 30 lines per minute was about 300 words per minute, roughly twice a broadcast read; the default is now 12, measured at 129 wpm on the default column. The ceiling comes down from 200 to 40 so the arrows stay useful. - Smaller, denser defaults: 40px over 1.3 line height in an 80% column, which is 21 lines on a 1080p screen where the old defaults gave 11. - The reading line is a marker one line tall in the gutter beside the text, replacing the rule across the words and the pair of margin arrows. The option is a boolean now that there is one style rather than three. - Space always drives the transport. It was deferring to whichever control had focus, so a prompter stopped responding to the pedal after anyone touched a button; the overlay drops focus after a pointer press instead. Enter still activates a focused control, so the overlay stays keyboard operable. - The help dialog is laid out as the rundown shortcuts panel is, down to the Kbd keycaps and the grouping, and no longer explains foot pedals. Headings stay on the same left rail as the script rather than centred: they are signposts for the operator, and a second alignment would give the eye something new to find at every segment change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
143 lines
5.3 KiB
TypeScript
143 lines
5.3 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('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);
|
|
});
|
|
});
|