mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-03 13:29:06 +00:00
fix(views): make boolean view params work, and retune the prompter
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
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { seedScript } from '../utils/seedScript';
|
||||
|
||||
test('View params configures timer view', async ({ page }) => {
|
||||
await page.goto('/timer');
|
||||
|
||||
@@ -13,3 +15,29 @@ test('View params configures timer view', async ({ page }) => {
|
||||
await expect(page.getByText('TIME NOW', { exact: true })).not.toBeInViewport();
|
||||
await expect(page).toHaveURL(/.*hideClock=true/);
|
||||
});
|
||||
|
||||
/**
|
||||
* An option which defaults to true is the case which breaks: the switch has to
|
||||
* open on, and switching it off has to reach the URL. A checkbox which is off
|
||||
* sends nothing at all, so an option in that state is only representable by
|
||||
* writing it out explicitly.
|
||||
*/
|
||||
test('View params can switch off an option which defaults to on', async ({ page }) => {
|
||||
await seedScript(page);
|
||||
await page.goto('/teleprompter?script=note');
|
||||
|
||||
const readingMarker = page.locator('.teleprompter__reading-marker');
|
||||
await expect(readingMarker).toBeVisible();
|
||||
|
||||
await page.mouse.move(Math.random() * 100, Math.random() * 100);
|
||||
await page.getByTestId('navigation__toggle-settings').click();
|
||||
|
||||
const readingLineSwitch = page.locator('label:has(input[name="readingLine"]) [role="switch"]');
|
||||
await expect(readingLineSwitch).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
await readingLineSwitch.click();
|
||||
await page.getByTestId('apply-view-params').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*readingLine=false/);
|
||||
await expect(readingMarker).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
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';
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -18,25 +16,6 @@ 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);
|
||||
@@ -150,11 +129,14 @@ test.describe('teleprompter', () => {
|
||||
});
|
||||
|
||||
test('honours the flip and reading line params', async ({ page }) => {
|
||||
await page.goto('/teleprompter?script=note&flipV=true&readingLine=arrows');
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user