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); }); });