From ff8faac4656ec3b0af98d11cf84c9d1e420597db Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 2 Aug 2026 18:13:25 +0800 Subject: [PATCH] test(ui): cover realtime media abort ordering --- ui/src/pages/chat/realtime-talk-input.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ui/src/pages/chat/realtime-talk-input.test.ts b/ui/src/pages/chat/realtime-talk-input.test.ts index 4387db401e7e..d0883d9a7d52 100644 --- a/ui/src/pages/chat/realtime-talk-input.test.ts +++ b/ui/src/pages/chat/realtime-talk-input.test.ts @@ -158,6 +158,43 @@ describe("realtime Talk microphone inputs", () => { expect(getUserMedia).not.toHaveBeenCalled(); }); + it("releases media when cancellation follows browser permission resolution", async () => { + const stop = vi.fn(); + let resolveMedia: (stream: MediaStream) => void = () => undefined; + const pending = new Promise((resolve) => { + resolveMedia = resolve; + }); + vi.stubGlobal("navigator", { + mediaDevices: { getUserMedia: vi.fn(() => pending) }, + }); + const controller = new AbortController(); + const opening = openRealtimeTalkInput(undefined, { signal: controller.signal }); + + resolveMedia({ getTracks: () => [{ stop }] } as unknown as MediaStream); + controller.abort(); + + await expect(opening).rejects.toMatchObject({ name: "AbortError" }); + expect(stop).toHaveBeenCalledOnce(); + }); + + it("keeps cancellation precedence over a late media rejection", async () => { + let rejectMedia: (error: unknown) => void = () => undefined; + const pending = new Promise((_resolve, reject) => { + rejectMedia = reject; + }); + vi.stubGlobal("navigator", { + mediaDevices: { getUserMedia: vi.fn(() => pending) }, + }); + const controller = new AbortController(); + const reason = new DOMException("cancelled", "AbortError"); + const opening = openRealtimeTalkCamera(undefined, { signal: controller.signal }); + + controller.abort(reason); + rejectMedia(new DOMException("denied", "NotAllowedError")); + + await expect(opening).rejects.toBe(reason); + }); + it("acquires camera separately so camera errors cannot stop microphone input", async () => { const audio = { getTracks: () => [] } as unknown as MediaStream; const camera = { getTracks: () => [] } as unknown as MediaStream;