feat(teleprompter): add teleprompter view

This commit is contained in:
Carlos Valente
2026-08-13 05:20:46 +00:00
committed by Carlos Valente
parent c6eccec30e
commit 7fc93af909
35 changed files with 2393 additions and 116 deletions
@@ -13,3 +13,25 @@ test('View params configures timer view', async ({ page }) => {
await expect(page.getByText('TIME NOW', { exact: true })).not.toBeInViewport();
await expect(page).toHaveURL(/.*hideClock=true/);
});
test('View params configures teleprompter view', async ({ page, request }) => {
const response = await request.post('/data/db/demo');
expect(response.ok()).toBe(true);
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);
});
@@ -0,0 +1,48 @@
import { expect, test } from '@playwright/test';
test('teleprompter renders and responds to its primary controls', async ({ page, request }) => {
// Earlier feature specs edit the loaded rundown. Restore the real demo project
// instead of manufacturing test-only events: its notes are the script fixture.
const response = await request.post('/data/db/demo');
expect(response.ok()).toBe(true);
const loadResponse = await request.get('/api/load/index/5');
expect(loadResponse.ok()).toBe(true);
await page.goto('/teleprompter?script=note&flipV=true');
const view = page.getByTestId('teleprompter-view');
const scroller = page.getByTestId('teleprompter-scroller');
const speed = page.getByTestId('teleprompter-speed');
await expect(view).toBeVisible();
await expect(scroller).toBeVisible();
await expect(page.getByText('Music plays, holding slide on screens')).toBeVisible();
await expect(speed).toContainText('14');
await expect(view).toHaveCSS('transform', /^matrix\(1, 0, 0, -1/);
// Following uses layout coordinates, which must remain stable when the view's
// visual coordinates are inverted by a vertical transform.
const loadedBlock = page.locator('.teleprompter__block[data-loaded]');
await expect(loadedBlock).toBeAttached();
const expectedScrollTop = await loadedBlock.evaluate((element) => {
const scroller = element.closest('.teleprompter')?.querySelector<HTMLElement>('.teleprompter__scroller');
if (!scroller) throw new Error('Teleprompter scroller not found');
return Math.max(0, (element as HTMLElement).offsetTop - scroller.clientHeight * 0.25);
});
await expect
.poll(async () => Math.abs((await scroller.evaluate((element) => element.scrollTop)) - expectedScrollTop))
.toBeLessThan(2);
await page.goto('/teleprompter?script=note');
await page.keyboard.press('Space');
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
await page.keyboard.press('Space');
await page.keyboard.press('ArrowRight');
await page.keyboard.press('f');
await expect(speed).toContainText('15');
await expect(page).toHaveURL(/speed=15/);
await expect(page).toHaveURL(/flipH=true/);
await expect(view).toHaveCSS('transform', /^matrix\(-1/);
});