diff --git a/src/App.svelte b/src/App.svelte index 8b29354..9b37c7b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -262,7 +262,7 @@ case 'j': case 'J': e.preventDefault(); - shuttleRate = adjustShuttle(-1, shuttleRate); + shuttleRate = adjustShuttle(-1); break; case 'k': case 'K': @@ -272,7 +272,7 @@ case 'l': case 'L': e.preventDefault(); - shuttleRate = adjustShuttle(1, shuttleRate); + shuttleRate = adjustShuttle(1); break; case 'Delete': case 'Backspace': { diff --git a/src/lib/transport/playback.ts b/src/lib/transport/playback.ts index 25a45cd..7894705 100644 --- a/src/lib/transport/playback.ts +++ b/src/lib/transport/playback.ts @@ -84,11 +84,12 @@ export function resetShuttleRate() { } } -export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number { +export function adjustShuttle(dir: 1 | -1): number { const videoEl = getVideo(); - if (!videoEl) return shuttleRate; + if (!videoEl) return 1; - const nextRate = Math.max(0.25, Math.min(4, shuttleRate + dir * 0.5)); + const current = videoEl.playbackRate; + const nextRate = Math.max(0.25, Math.min(4, current + dir * 0.25)); videoEl.playbackRate = nextRate; if (videoEl.paused) { void videoEl.play(); @@ -121,14 +122,13 @@ export function toggleFullscreen(): void { console.error('Exit fullscreen failed:', e); }); } else { - const videoEl = getVideo(); - if (!videoEl) return; - if (videoEl.requestFullscreen) { - videoEl.requestFullscreen().catch((e) => { + const el = document.documentElement; + if (el.requestFullscreen) { + el.requestFullscreen().catch((e) => { console.error('Fullscreen request failed:', e); }); - } else if ((videoEl as HTMLVideoElement & { webkitEnterFullscreen?: () => void }).webkitEnterFullscreen) { - (videoEl as HTMLVideoElement & { webkitEnterFullscreen: () => void }).webkitEnterFullscreen(); + } else if ((el as HTMLElement & { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen) { + (el as HTMLElement & { webkitRequestFullscreen: () => void }).webkitRequestFullscreen(); } } } diff --git a/tests/lib/transport/playback.test.ts b/tests/lib/transport/playback.test.ts index 7436cc0..f1145ee 100644 --- a/tests/lib/transport/playback.test.ts +++ b/tests/lib/transport/playback.test.ts @@ -19,6 +19,7 @@ import { isFullscreenActive, setPlaybackRate, getPlaybackRate, + adjustShuttle, } from '$lib/transport/playback'; function createMockVideoElement(): HTMLVideoElement { @@ -85,9 +86,11 @@ describe('playback — fullscreen helpers', () => { expect(isFullscreenActive()).toBe(false); }); - it('toggleFullscreen calls requestFullscreen when not fullscreen', () => { + it('toggleFullscreen calls requestFullscreen on document.documentElement', () => { + const spy = vi.fn().mockResolvedValue(undefined); + document.documentElement.requestFullscreen = spy; toggleFullscreen(); - expect(mockVideo.requestFullscreen).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); }); it('isFullscreenActive returns true when fullscreen element exists', () => { @@ -127,3 +130,59 @@ describe('playback — playback rate helpers', () => { expect(getPlaybackRate()).toBe(1); }); }); + +describe('playback — adjustShuttle', () => { + let mockVideo: HTMLVideoElement; + + beforeEach(() => { + mockVideo = createMockVideoElement(); + setVideoElement(mockVideo); + mockVideo.playbackRate = 1; + Object.defineProperty(mockVideo, 'paused', { + value: true, + writable: true, + configurable: true, + }); + mockVideo.play = vi.fn().mockResolvedValue(undefined); + }); + + it('increases rate by 0.25 when dir is 1', () => { + const result = adjustShuttle(1); + expect(result).toBe(1.25); + expect(mockVideo.playbackRate).toBe(1.25); + }); + + it('decreases rate by 0.25 when dir is -1', () => { + const result = adjustShuttle(-1); + expect(result).toBe(0.75); + expect(mockVideo.playbackRate).toBe(0.75); + }); + + it('reads current rate from video element, not external state', () => { + mockVideo.playbackRate = 0.75; + const result = adjustShuttle(1); + expect(result).toBe(1); + }); + + it('clamps to minimum 0.25', () => { + mockVideo.playbackRate = 0.25; + const result = adjustShuttle(-1); + expect(result).toBe(0.25); + }); + + it('clamps to maximum 4', () => { + mockVideo.playbackRate = 4; + const result = adjustShuttle(1); + expect(result).toBe(4); + }); + + it('starts playback if paused', () => { + adjustShuttle(1); + expect(mockVideo.play).toHaveBeenCalled(); + }); + + it('returns 1 when no video element', () => { + setVideoElement(null); + expect(adjustShuttle(1)).toBe(1); + }); +});