|
2 | 2 | DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER, |
3 | 3 | EnvironmentId, |
4 | 4 | ThreadId, |
| 5 | + PreviewCaptureFailure, |
5 | 6 | } from "@t3tools/contracts"; |
6 | 7 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
7 | 8 |
|
@@ -705,11 +706,13 @@ describe("browser recording", () => { |
705 | 706 | }); |
706 | 707 |
|
707 | 708 | await startBrowserRecording("recording-tab"); |
708 | | - FakeMediaRecorder.stopError = new Error("stop failed"); |
| 709 | + const cleanupCause = new DOMException("Could not start video source", "NotReadableError"); |
| 710 | + FakeMediaRecorder.stopError = cleanupCause; |
709 | 711 |
|
710 | 712 | await expect(stopBrowserRecording("recording-tab")).rejects.toMatchObject({ |
711 | 713 | operation: "cleanup", |
712 | 714 | tabId: "recording-tab", |
| 715 | + message: "Browser recording operation cleanup failed for tab recording-tab.", |
713 | 716 | }); |
714 | 717 | expect(stopTrack).toHaveBeenCalledOnce(); |
715 | 718 | }); |
@@ -777,6 +780,81 @@ describe("browser recording", () => { |
777 | 780 | expect(events.at(-1)).toBe("clear"); |
778 | 781 | }); |
779 | 782 |
|
| 783 | + it.each([false, true])( |
| 784 | + "explains fallback video-source rejection and permits retry when cleanup fails: %s", |
| 785 | + async (cleanupFails) => { |
| 786 | + const cause = new DOMException("Could not start video source", "NotReadableError"); |
| 787 | + const cleanupCause = new Error("native stop failed"); |
| 788 | + const frames = prepareFrameFallback(); |
| 789 | + frames.canvas.captureStream.mockImplementationOnce(() => { |
| 790 | + throw cause; |
| 791 | + }); |
| 792 | + if (cleanupFails) stopScreencast.mockRejectedValueOnce(cleanupCause); |
| 793 | + |
| 794 | + const failed = startBrowserRecording("recording-tab").catch((failure: unknown) => failure); |
| 795 | + await frames.ready; |
| 796 | + await frames.receive(); |
| 797 | + const error = await failed; |
| 798 | + expect(error).toMatchObject({ |
| 799 | + _tag: "BrowserRecordingOperationError", |
| 800 | + operation: "capture-media-stream", |
| 801 | + tabId: "recording-tab", |
| 802 | + message: |
| 803 | + "Unable to start the preview video source. Make sure the capture host is unlocked " + |
| 804 | + "and its display is on, then retry. NotReadableError: Could not start video source", |
| 805 | + cause: cleanupFails ? { cause, errors: [cause, cleanupCause] } : cause, |
| 806 | + }); |
| 807 | + expect(stopScreencast).toHaveBeenCalledWith("recording-tab"); |
| 808 | + expect(FakeMediaRecorder.instances).toHaveLength(0); |
| 809 | + expect(save).not.toHaveBeenCalled(); |
| 810 | + expect(readActiveBrowserRecordingTabIds()).toEqual(new Set()); |
| 811 | + expect(useBrowserSurfaceStore.getState().activityByTabId["recording-tab"]).toBeUndefined(); |
| 812 | + |
| 813 | + const recoveredFrames = prepareFrameFallback(); |
| 814 | + const restarted = startBrowserRecording("recording-tab"); |
| 815 | + await recoveredFrames.ready; |
| 816 | + await recoveredFrames.receive(); |
| 817 | + await restarted; |
| 818 | + await stopBrowserRecording("recording-tab"); |
| 819 | + expect(getUserMedia).toHaveBeenCalledTimes(2); |
| 820 | + }, |
| 821 | + ); |
| 822 | + |
| 823 | + it("keeps unrecognized acquisition failures generic", async () => { |
| 824 | + const cause = new Error("capture failed"); |
| 825 | + cause.name = "NotReadableError"; |
| 826 | + getUserMedia.mockRejectedValueOnce(cause); |
| 827 | + |
| 828 | + await expect(startBrowserRecording("recording-tab")).rejects.toMatchObject({ |
| 829 | + operation: "capture-media-stream", |
| 830 | + cause, |
| 831 | + message: "Browser recording operation capture-media-stream failed for tab recording-tab.", |
| 832 | + }); |
| 833 | + }); |
| 834 | + |
| 835 | + it("reports a native arm diagnostic immediately and frees the tab for a retry", async () => { |
| 836 | + const cause = new PreviewCaptureFailure({ |
| 837 | + reason: "renderer-unavailable", |
| 838 | + stage: "recording-start", |
| 839 | + }); |
| 840 | + startScreencast.mockRejectedValueOnce(cause); |
| 841 | + |
| 842 | + await expect(startBrowserRecording("recording-tab")).rejects.toMatchObject({ |
| 843 | + _tag: "BrowserRecordingOperationError", |
| 844 | + operation: "start-screencast", |
| 845 | + tabId: "recording-tab", |
| 846 | + cause, |
| 847 | + message: cause.message, |
| 848 | + }); |
| 849 | + expect(getUserMedia).not.toHaveBeenCalled(); |
| 850 | + expect(readActiveBrowserRecordingTabIds()).toEqual(new Set()); |
| 851 | + expect(useBrowserSurfaceStore.getState().activityByTabId["recording-tab"]).toBeUndefined(); |
| 852 | + |
| 853 | + await startBrowserRecording("recording-tab"); |
| 854 | + await stopBrowserRecording("recording-tab"); |
| 855 | + expect(getUserMedia).toHaveBeenCalledOnce(); |
| 856 | + }); |
| 857 | + |
780 | 858 | it("times out stalled stream acquisition and stops a late stream", async () => { |
781 | 859 | vi.useFakeTimers(); |
782 | 860 | let finishCapture!: (stream: MediaStream) => void; |
|
0 commit comments