refactor(teleprompter): drop code that was not earning its keep

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
This commit is contained in:
Claude
2026-08-13 16:42:07 +00:00
parent 15dac35748
commit 323b30ea68
9 changed files with 140 additions and 167 deletions
+7 -14
View File
@@ -38,13 +38,8 @@ test.describe('teleprompter', () => {
expect(await scrollTop(page)).toBe(0);
// run it fast so the movement is unambiguous within the wait
await page.keyboard.press('ArrowRight');
await page.keyboard.press('Space');
await page.waitForTimeout(1000);
const whileRunning = await scrollTop(page);
expect(whileRunning).toBeGreaterThan(0);
await expect.poll(() => scrollTop(page)).toBeGreaterThan(0);
await page.keyboard.press('Space');
await page.waitForTimeout(200);
@@ -57,19 +52,18 @@ test.describe('teleprompter', () => {
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 page.mouse.move(60, 60);
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.mouse.move(60, 60);
await page.keyboard.press('Space');
await expect(page.getByRole('dialog')).toHaveCount(0);
// and the claim is released when the view goes away
// and the claim is released once the view goes away
await page.goto('/timer');
await page.mouse.move(60, 60);
await expect(page.locator('data-testid=timer-view')).toBeVisible();
await page.keyboard.press('Space');
await expect(page.getByRole('dialog')).toBeVisible();
});
@@ -80,12 +74,11 @@ test.describe('teleprompter', () => {
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(300);
expect(await scrollTop(page)).toBeGreaterThan(0);
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 page.waitForTimeout(600);
expect(await scrollTop(page)).toBe(0);
await expect.poll(() => scrollTop(page)).toBe(0);
});
test('arrow keys change the speed', async ({ page }) => {