From 661f8a600f578f6449e0d196e9b6d8185087afad Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:36:32 -0400 Subject: [PATCH] fix(web): update the timeline when video duration loads --- .github/workflows/ci.yml | 1 + .../web/__tests__/unit/playback-store.test.ts | 83 +++++++++++++++++++ .../_components/playback/playback-store.ts | 3 +- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 apps/web/__tests__/unit/playback-store.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57810c00587..9d712a75a5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,7 @@ jobs: __tests__/unit/linkified-text.test.ts __tests__/unit/loom-import-ui.test.ts __tests__/unit/media-player-store-refs.test.ts + __tests__/unit/playback-store.test.ts __tests__/unit/seo-demo-videos.test.ts __tests__/unit/signed-baa-card.test.ts __tests__/unit/summary-editor.test.ts diff --git a/apps/web/__tests__/unit/playback-store.test.ts b/apps/web/__tests__/unit/playback-store.test.ts new file mode 100644 index 00000000000..5022e68b5a0 --- /dev/null +++ b/apps/web/__tests__/unit/playback-store.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from "vitest"; +import { createPlaybackStore } from "@/app/s/[videoId]/_components/playback/playback-store"; + +describe("playback store", () => { + it.each(["loadedmetadata", "durationchange"])( + "publishes the loaded duration on %s while paused at zero", + (event) => { + const video = Object.assign(new EventTarget(), { + currentTime: 0, + duration: Number.NaN, + paused: true, + ended: false, + }); + const store = createPlaybackStore(); + let observedDuration = 0; + store.subscribe(() => { + observedDuration = store.getDuration(); + }); + store.attach(video as unknown as HTMLVideoElement); + + video.duration = 120; + video.dispatchEvent(new Event(event)); + + expect(observedDuration).toBe(120); + expect(store.getCurrentTime()).toBe(0); + store.detach(); + }, + ); + + it("publishes duration changes without repeating unchanged samples", () => { + const video = Object.assign(new EventTarget(), { + currentTime: 5, + duration: 30, + paused: true, + ended: false, + }); + const store = createPlaybackStore(30); + store.attach(video as unknown as HTMLVideoElement); + const subscriber = vi.fn(() => store.getDuration()); + store.subscribe(subscriber); + subscriber.mockClear(); + + video.duration = 45; + video.dispatchEvent(new Event("durationchange")); + video.dispatchEvent(new Event("loadedmetadata")); + video.dispatchEvent(new Event("timeupdate")); + + expect(subscriber).toHaveBeenCalledExactlyOnceWith(5); + expect(subscriber).toHaveReturnedWith(45); + store.detach(); + }); + + it("keeps time updates and the last finite duration until detached", () => { + const video = Object.assign(new EventTarget(), { + currentTime: 0, + duration: 30, + paused: true, + ended: false, + }); + const store = createPlaybackStore(); + store.attach(video as unknown as HTMLVideoElement); + const subscriber = vi.fn(() => store.getDuration()); + store.subscribe(subscriber); + subscriber.mockClear(); + + video.duration = Number.POSITIVE_INFINITY; + video.dispatchEvent(new Event("durationchange")); + expect(subscriber).not.toHaveBeenCalled(); + expect(store.getDuration()).toBe(30); + + video.currentTime = 6; + video.dispatchEvent(new Event("timeupdate")); + expect(subscriber).toHaveBeenCalledExactlyOnceWith(6); + expect(subscriber).toHaveReturnedWith(30); + + store.detach(); + video.currentTime = 7; + video.duration = 45; + video.dispatchEvent(new Event("durationchange")); + video.dispatchEvent(new Event("timeupdate")); + expect(subscriber).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/web/app/s/[videoId]/_components/playback/playback-store.ts b/apps/web/app/s/[videoId]/_components/playback/playback-store.ts index c6e38546207..0bc286e1382 100644 --- a/apps/web/app/s/[videoId]/_components/playback/playback-store.ts +++ b/apps/web/app/s/[videoId]/_components/playback/playback-store.ts @@ -46,11 +46,12 @@ export function createPlaybackStore(fallbackDuration: number | null = 0) { /** Pull `currentTime`/`duration` off the element and notify time subscribers. */ const sample = (force = false) => { + const previousDuration = duration; if (element && isFiniteDuration(element.duration)) { duration = element.duration; } const next = element?.currentTime ?? time; - if (next === time && !force) return; + if (next === time && duration === previousDuration && !force) return; time = next; emitTime(); };