mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 20:09:11 +00:00
feat(teleprompter): stop playback at the end of the event
A user reading a podcast script found the prompter ran past the end of the event and on into the next one. An event is a unit of time with a stop of its own, but auto-scroll bounded against the end of the whole document, so nothing held it at the boundary. At a conference that means the prompter reads the next speaker's script while they are still walking on. Playback now runs to the end of the segment being read and parks there, saying so. Loading the next event releases it through the existing follow, and pressing play again carries on into the next segment for a reader who wants to keep going. Only playback is held: a jump, a page or a nudge is the reader asking to leave the segment and still crosses freely. The segment is chosen once when playback starts, and held by identity rather than as a pixel bound. Re-deciding it against a position which is moving let the bound outrun the reader and never arrive; holding the id rather than the offset keeps the stop on the same words when an edit moves the script underneath it. Add an option to show only the event being played, for the case where the rest of the script is a distraction. It falls back to the whole script while nothing is playing, where narrowing would leave a blank screen. The script of the event being played is now held out from the rest, which stays readable so a reader who is ahead or behind can still find their place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
@@ -117,6 +117,61 @@ test('shift and the vertical arrows walk the reader event by event', async ({ pa
|
||||
await expect.poll(() => eventUnderReadingLine(page)).toBe(headings[1]);
|
||||
});
|
||||
|
||||
test('playback stops at the end of the event instead of reading on into the next', async ({ page, request }) => {
|
||||
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&followLoaded=false&speed=40');
|
||||
|
||||
const scroller = page.getByTestId('teleprompter-scroller');
|
||||
await expect(scroller).toBeVisible();
|
||||
|
||||
// park the reading line just short of the first event's end, so the run to
|
||||
// the boundary takes a moment rather than the length of the segment
|
||||
const segmentEnd = await scroller.evaluate((element) => {
|
||||
const block = element.querySelector<HTMLElement>('.teleprompter__block');
|
||||
if (!block) throw new Error('No script block found');
|
||||
const end = block.offsetTop + block.offsetHeight - element.clientHeight * 0.25;
|
||||
element.scrollTop = end - 30;
|
||||
return end;
|
||||
});
|
||||
|
||||
await page.keyboard.press('Space');
|
||||
|
||||
await expect(page.getByTestId('teleprompter-parked')).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => Math.abs((await scroller.evaluate((element) => element.scrollTop)) - segmentEnd))
|
||||
.toBeLessThan(3);
|
||||
|
||||
// and it stays there, rather than carrying on after a beat
|
||||
await page.waitForTimeout(500);
|
||||
expect(Math.abs((await scroller.evaluate((element) => element.scrollTop)) - segmentEnd)).toBeLessThan(3);
|
||||
|
||||
// pressing play again is how the reader moves on to the next event
|
||||
await page.keyboard.press('Space');
|
||||
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeGreaterThan(segmentEnd + 5);
|
||||
});
|
||||
|
||||
test('onlyPlaying narrows the script to the event being played', async ({ page, request }) => {
|
||||
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');
|
||||
await expect(page.locator('.teleprompter__block').first()).toBeVisible();
|
||||
const whole = await page.locator('.teleprompter__block').count();
|
||||
expect(whole).toBeGreaterThan(1);
|
||||
|
||||
await page.goto('/teleprompter?script=note&onlyPlaying=true');
|
||||
|
||||
const blocks = page.locator('.teleprompter__block');
|
||||
await expect(blocks).toHaveCount(1);
|
||||
await expect(blocks.first()).toHaveAttribute('data-loaded', 'true');
|
||||
});
|
||||
|
||||
test('follow tolerates a small scroll and breaks on a real one, like the operator view', async ({ page, request }) => {
|
||||
const response = await request.post('/data/db/demo');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user