From b8d668a3b2b8a70e2f634878f5b01c02f598f56d Mon Sep 17 00:00:00 2001 From: Romil Bijarnia Date: Wed, 11 Nov 2026 21:06:59 +1100 Subject: [PATCH] fix(web): treat cancelled preview popouts as cancellation --- .../serverPictureInPicture.browser.test.ts | 55 +++++++++++++++++++ .../web/src/browser/serverPictureInPicture.ts | 1 + 2 files changed, 56 insertions(+) create mode 100644 apps/web/src/browser/serverPictureInPicture.browser.test.ts diff --git a/apps/web/src/browser/serverPictureInPicture.browser.test.ts b/apps/web/src/browser/serverPictureInPicture.browser.test.ts new file mode 100644 index 0000000000000..d1f439fae786c --- /dev/null +++ b/apps/web/src/browser/serverPictureInPicture.browser.test.ts @@ -0,0 +1,55 @@ +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { expect, it, vi } from "vite-plus/test"; + +const mocks = vi.hoisted(() => ({ readAccess: vi.fn() })); + +vi.mock(import("~/state/previewStream"), () => ({ + readPreviewStreamAccess: mocks.readAccess, +})); + +import { closeServerPictureInPicture, openServerPictureInPicture } from "./serverPictureInPicture"; + +function setup() { + return { + [Symbol.dispose]() { + closeServerPictureInPicture(); + mocks.readAccess.mockReset(); + }, + }; +} + +it("cancels pending playback without reporting a failed popout", async () => { + using _cleanup = setup(); + const opening = openServerPictureInPicture({ + environmentId: EnvironmentId.make("environment"), + threadId: ThreadId.make("thread"), + tabId: "tab", + seed: null, + }); + const video = document.querySelector("video")!; + const tracks = (video.srcObject as MediaStream).getTracks(); + closeServerPictureInPicture(); + await expect(opening).resolves.toBeUndefined(); + expect(video.isConnected).toBe(false); + expect(tracks.length).toBeGreaterThan(0); + expect(tracks.every((track) => track.readyState === "ended")).toBe(true); + expect(mocks.readAccess).not.toHaveBeenCalled(); +}); + +it("still reports playback failures for an active popout", async () => { + using _cleanup = setup(); + const error = new Error("Playback failed"); + using _play = vi.spyOn(HTMLMediaElement.prototype, "play").mockRejectedValue(error); + const opening = openServerPictureInPicture({ + environmentId: EnvironmentId.make("environment"), + threadId: ThreadId.make("thread"), + tabId: "tab", + seed: null, + }); + const video = document.querySelector("video")!; + const tracks = (video.srcObject as MediaStream).getTracks(); + await expect(opening).rejects.toBe(error); + expect(video.isConnected).toBe(false); + expect(tracks.every((track) => track.readyState === "ended")).toBe(true); + expect(mocks.readAccess).not.toHaveBeenCalled(); +}); diff --git a/apps/web/src/browser/serverPictureInPicture.ts b/apps/web/src/browser/serverPictureInPicture.ts index fda9ec7fa7294..04b6d3ddd8dbd 100644 --- a/apps/web/src/browser/serverPictureInPicture.ts +++ b/apps/web/src/browser/serverPictureInPicture.ts @@ -175,6 +175,7 @@ export async function openServerPictureInPicture(input: { video.webkitSetPresentationMode?.("picture-in-picture"); } } catch (error) { + if (stopped) return; onLeave(); throw error; }