| ... | @@ -1797,6 +1797,216 @@ describe("PreviewManager", () => { | ... | @@ -1797,6 +1797,216 @@ describe("PreviewManager", () => { |
| 1797 | ), | 1797 | ), |
| 1798 | ); | 1798 | ); |
| 1799 | | 1799 | |
| | 1800 | effectIt.effect("keeps window unthrottled until the final frame capture stops", () => |
| | 1801 | withManager((manager) => |
| | 1802 | Effect.gen(function* () { |
| | 1803 | const setBackgroundThrottling = vi.fn(); |
| | 1804 | const capturePage = vi.fn(async () => ({ |
| | 1805 | toJPEG: () => Buffer.from("recording-frame"), |
| | 1806 | getSize: () => ({ width: 1280, height: 720 }), |
| | 1807 | })); |
| | 1808 | const webContentsById = new Map([ |
| | 1809 | [41, makeTestPreviewWebContents(capturePage, 41)], |
| | 1810 | [42, makeTestPreviewWebContents(capturePage, 42)], |
| | 1811 | ]); |
| | 1812 | fromId.mockImplementation((id) => |
| | 1813 | id === undefined ? null : (webContentsById.get(id) ?? null), |
| | 1814 | ); |
| | 1815 | |
| | 1816 | yield* manager.createTab("tab_capture_throttling_1"); |
| | 1817 | yield* manager.createTab("tab_capture_throttling_2"); |
| | 1818 | yield* manager.registerWebview("tab_capture_throttling_1", 41); |
| | 1819 | yield* manager.registerWebview("tab_capture_throttling_2", 42); |
| | 1820 | yield* manager.setMainWindow({ |
| | 1821 | isDestroyed: () => false, |
| | 1822 | once: vi.fn(), |
| | 1823 | webContents: { setBackgroundThrottling }, |
| | 1824 | } as never); |
| | 1825 | |
| | 1826 | yield* manager.startRecording("tab_capture_throttling_1"); |
| | 1827 | yield* manager.startRecording("tab_capture_throttling_2"); |
| | 1828 | expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); |
| | 1829 | |
| | 1830 | yield* manager.stopRecording("tab_capture_throttling_1"); |
| | 1831 | expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); |
| | 1832 | |
| | 1833 | yield* manager.stopRecording("tab_capture_throttling_2"); |
| | 1834 | expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true]]); |
| | 1835 | }), |
| | 1836 | ), |
| | 1837 | ); |
| | 1838 | |
| | 1839 | effectIt.effect("does not commit failed starts and retries throttle restoration", () => |
| | 1840 | withManager((manager) => |
| | 1841 | Effect.gen(function* () { |
| | 1842 | const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>(); |
| | 1843 | const capturePage = vi.fn(async () => ({ |
| | 1844 | toJPEG: () => Buffer.from("recording-frame"), |
| | 1845 | getSize: () => ({ width: 1280, height: 720 }), |
| | 1846 | })); |
| | 1847 | fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); |
| | 1848 | |
| | 1849 | yield* manager.createTab("tab_capture_throttling_failure"); |
| | 1850 | yield* manager.registerWebview("tab_capture_throttling_failure", 42); |
| | 1851 | yield* manager.setMainWindow({ |
| | 1852 | isDestroyed: () => false, |
| | 1853 | once: vi.fn(), |
| | 1854 | webContents: { setBackgroundThrottling }, |
| | 1855 | } as never); |
| | 1856 | |
| | 1857 | setBackgroundThrottling.mockImplementationOnce(() => { |
| | 1858 | throw new Error("start throttling update failed"); |
| | 1859 | }); |
| | 1860 | const failedStart = yield* Effect.exit( |
| | 1861 | manager.startRecording("tab_capture_throttling_failure"), |
| | 1862 | ); |
| | 1863 | expect(Exit.isFailure(failedStart)).toBe(true); |
| | 1864 | |
| | 1865 | yield* manager.startRecording("tab_capture_throttling_failure"); |
| | 1866 | expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false]]); |
| | 1867 | |
| | 1868 | setBackgroundThrottling.mockImplementationOnce(() => { |
| | 1869 | throw new Error("stop throttling update failed"); |
| | 1870 | }); |
| | 1871 | yield* manager.stopRecording("tab_capture_throttling_failure"); |
| | 1872 | expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false], [true], [true]]); |
| | 1873 | |
| | 1874 | yield* manager.startRecording("tab_capture_throttling_failure"); |
| | 1875 | yield* manager.stopRecording("tab_capture_throttling_failure"); |
| | 1876 | expect(setBackgroundThrottling.mock.calls).toEqual([ |
| | 1877 | [false], |
| | 1878 | [false], |
| | 1879 | [true], |
| | 1880 | [true], |
| | 1881 | [false], |
| | 1882 | [true], |
| | 1883 | ]); |
| | 1884 | }), |
| | 1885 | ), |
| | 1886 | ); |
| | 1887 | |
| | 1888 | effectIt.effect("does not publish a replacement window when capture reconciliation fails", () => |
| | 1889 | withManager((manager) => |
| | 1890 | Effect.gen(function* () { |
| | 1891 | const setBackgroundThrottling = vi.fn(() => { |
| | 1892 | throw new Error("replacement throttling update failed"); |
| | 1893 | }); |
| | 1894 | const capturePage = vi.fn(async () => ({ |
| | 1895 | toJPEG: () => Buffer.from("recording-frame"), |
| | 1896 | getSize: () => ({ width: 1280, height: 720 }), |
| | 1897 | })); |
| | 1898 | fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); |
| | 1899 | |
| | 1900 | yield* manager.createTab("tab_capture_replacement_failure"); |
| | 1901 | yield* manager.registerWebview("tab_capture_replacement_failure", 42); |
| | 1902 | yield* manager.startRecording("tab_capture_replacement_failure"); |
| | 1903 | |
| | 1904 | const failedReplacement = yield* Effect.exit( |
| | 1905 | manager.setMainWindow({ |
| | 1906 | isDestroyed: () => false, |
| | 1907 | once: vi.fn(), |
| | 1908 | webContents: { setBackgroundThrottling }, |
| | 1909 | } as never), |
| | 1910 | ); |
| | 1911 | expect(Exit.isFailure(failedReplacement)).toBe(true); |
| | 1912 | |
| | 1913 | yield* manager.stopRecording("tab_capture_replacement_failure"); |
| | 1914 | expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); |
| | 1915 | }), |
| | 1916 | ), |
| | 1917 | ); |
| | 1918 | |
| | 1919 | effectIt.effect("ignores close events from replaced main windows", () => |
| | 1920 | withManager((manager) => |
| | 1921 | Effect.gen(function* () { |
| | 1922 | let closeFirstWindow: (() => void) | undefined; |
| | 1923 | const firstWindowThrottling = vi.fn(); |
| | 1924 | const replacementWindowThrottling = vi.fn(); |
| | 1925 | const capturePage = vi.fn(async () => ({ |
| | 1926 | toJPEG: () => Buffer.from("recording-frame"), |
| | 1927 | getSize: () => ({ width: 1280, height: 720 }), |
| | 1928 | })); |
| | 1929 | fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); |
| | 1930 | |
| | 1931 | yield* manager.createTab("tab_replaced_window_close"); |
| | 1932 | yield* manager.registerWebview("tab_replaced_window_close", 42); |
| | 1933 | yield* manager.setMainWindow({ |
| | 1934 | isDestroyed: () => false, |
| | 1935 | once: vi.fn((event: string, listener: () => void) => { |
| | 1936 | if (event === "closed") closeFirstWindow = listener; |
| | 1937 | }), |
| | 1938 | webContents: { setBackgroundThrottling: firstWindowThrottling }, |
| | 1939 | } as never); |
| | 1940 | yield* manager.setMainWindow({ |
| | 1941 | isDestroyed: () => false, |
| | 1942 | once: vi.fn(), |
| | 1943 | webContents: { setBackgroundThrottling: replacementWindowThrottling }, |
| | 1944 | } as never); |
| | 1945 | |
| | 1946 | closeFirstWindow?.(); |
| | 1947 | yield* manager.startRecording("tab_replaced_window_close"); |
| | 1948 | expect(firstWindowThrottling).not.toHaveBeenCalled(); |
| | 1949 | expect(replacementWindowThrottling.mock.calls).toEqual([[false]]); |
| | 1950 | yield* manager.stopRecording("tab_replaced_window_close"); |
| | 1951 | expect(replacementWindowThrottling.mock.calls).toEqual([[false], [true]]); |
| | 1952 | }), |
| | 1953 | ), |
| | 1954 | ); |
| | 1955 | |
| | 1956 | effectIt.effect("releases frame capture when the main window closes", () => |
| | 1957 | withManager((manager) => |
| | 1958 | Effect.gen(function* () { |
| | 1959 | let closeMainWindow: (() => void) | undefined; |
| | 1960 | const firstWindowThrottling = vi.fn(); |
| | 1961 | const replacementWindowThrottling = vi.fn(); |
| | 1962 | const capturePage = vi.fn(async () => ({ |
| | 1963 | toJPEG: () => Buffer.from("recording-frame"), |
| | 1964 | getSize: () => ({ width: 1280, height: 720 }), |
| | 1965 | })); |
| | 1966 | const webContentsById = new Map([ |
| | 1967 | [42, makeTestPreviewWebContents(capturePage, 42)], |
| | 1968 | [43, makeTestPreviewWebContents(capturePage, 43)], |
| | 1969 | ]); |
| | 1970 | fromId.mockImplementation((id) => |
| | 1971 | id === undefined ? null : (webContentsById.get(id) ?? null), |
| | 1972 | ); |
| | 1973 | |
| | 1974 | yield* manager.createTab("tab_window_close_recording"); |
| | 1975 | yield* manager.createTab("tab_window_close_race"); |
| | 1976 | yield* manager.registerWebview("tab_window_close_recording", 42); |
| | 1977 | yield* manager.registerWebview("tab_window_close_race", 43); |
| | 1978 | yield* manager.setMainWindow({ |
| | 1979 | isDestroyed: () => false, |
| | 1980 | once: vi.fn((event: string, listener: () => void) => { |
| | 1981 | if (event === "closed") closeMainWindow = listener; |
| | 1982 | }), |
| | 1983 | webContents: { setBackgroundThrottling: firstWindowThrottling }, |
| | 1984 | } as never); |
| | 1985 | yield* manager.startRecording("tab_window_close_recording"); |
| | 1986 | expect(firstWindowThrottling.mock.calls).toEqual([[false]]); |
| | 1987 | |
| | 1988 | closeMainWindow?.(); |
| | 1989 | const racedStart = yield* Effect.exit(manager.startRecording("tab_window_close_race")); |
| | 1990 | expect(Exit.isFailure(racedStart)).toBe(true); |
| | 1991 | if (Exit.isFailure(racedStart)) { |
| | 1992 | expect(Option.getOrThrow(Cause.findErrorOption(racedStart.cause))).toMatchObject({ |
| | 1993 | _tag: "PreviewMainWindowClosedError", |
| | 1994 | tabId: "tab_window_close_race", |
| | 1995 | }); |
| | 1996 | } |
| | 1997 | yield* Effect.yieldNow; |
| | 1998 | yield* Effect.yieldNow; |
| | 1999 | |
| | 2000 | yield* manager.setMainWindow({ |
| | 2001 | isDestroyed: () => false, |
| | 2002 | once: vi.fn(), |
| | 2003 | webContents: { setBackgroundThrottling: replacementWindowThrottling }, |
| | 2004 | } as never); |
| | 2005 | expect(replacementWindowThrottling).not.toHaveBeenCalled(); |
| | 2006 | }), |
| | 2007 | ), |
| | 2008 | ); |
| | 2009 | |
| 1800 | effectIt.effect("captures hidden preview recordings independently for concurrent tabs", () => | 2010 | effectIt.effect("captures hidden preview recordings independently for concurrent tabs", () => |
| 1801 | withManager((manager) => | 2011 | withManager((manager) => |
| 1802 | Effect.gen(function* () { | 2012 | Effect.gen(function* () { |
| ... | @@ -2106,6 +2316,8 @@ describe("PreviewManager", () => { | ... | @@ -2106,6 +2316,8 @@ describe("PreviewManager", () => { |
| 2106 | effectIt.effect("shares background frame capture between recording and picture-in-picture", () => | 2316 | effectIt.effect("shares background frame capture between recording and picture-in-picture", () => |
| 2107 | withManager((manager) => | 2317 | withManager((manager) => |
| 2108 | Effect.gen(function* () { | 2318 | Effect.gen(function* () { |
| | 2319 | const setBackgroundThrottling = vi.fn(); |
| | 2320 | const mainWindowWebContents = { setBackgroundThrottling }; |
| 2109 | const jpeg = Buffer.from("shared-preview-frame"); | 2321 | const jpeg = Buffer.from("shared-preview-frame"); |
| 2110 | const capturePage = vi.fn(async () => ({ | 2322 | const capturePage = vi.fn(async () => ({ |
| 2111 | toJPEG: () => jpeg, | 2323 | toJPEG: () => jpeg, |
| ... | @@ -2113,6 +2325,7 @@ describe("PreviewManager", () => { | ... | @@ -2113,6 +2325,7 @@ describe("PreviewManager", () => { |
| 2113 | })); | 2325 | })); |
| 2114 | fromId.mockReturnValue({ | 2326 | fromId.mockReturnValue({ |
| 2115 | id: 42, | 2327 | id: 42, |
| | 2328 | hostWebContents: mainWindowWebContents, |
| 2116 | isDestroyed: () => false, | 2329 | isDestroyed: () => false, |
| 2117 | getType: () => "webview", | 2330 | getType: () => "webview", |
| 2118 | getURL: () => "https://example.com", | 2331 | getURL: () => "https://example.com", |
| ... | @@ -2165,6 +2378,12 @@ describe("PreviewManager", () => { | ... | @@ -2165,6 +2378,12 @@ describe("PreviewManager", () => { |
| 2165 | const states: PreviewManager.PreviewTabState[] = []; | 2378 | const states: PreviewManager.PreviewTabState[] = []; |
| 2166 | const recordingFrames: DesktopPreviewRecordingFrame[] = []; | 2379 | const recordingFrames: DesktopPreviewRecordingFrame[] = []; |
| 2167 | | 2380 | |
| | 2381 | yield* manager.setMainWindow({ |
| | 2382 | isDestroyed: () => false, |
| | 2383 | once: vi.fn(), |
| | 2384 | webContents: mainWindowWebContents, |
| | 2385 | } as never); |
| | 2386 | |
| 2168 | yield* manager.subscribeStateChanges((_tabId, state) => | 2387 | yield* manager.subscribeStateChanges((_tabId, state) => |
| 2169 | Effect.sync(() => { | 2388 | Effect.sync(() => { |
| 2170 | states.push(state); | 2389 | states.push(state); |
| ... | @@ -2179,6 +2398,7 @@ describe("PreviewManager", () => { | ... | @@ -2179,6 +2398,7 @@ describe("PreviewManager", () => { |
| 2179 | yield* manager.registerWebview("tab_pip", 42); | 2398 | yield* manager.registerWebview("tab_pip", 42); |
| 2180 | yield* manager.openPictureInPicture("tab_pip"); | 2399 | yield* manager.openPictureInPicture("tab_pip"); |
| 2181 | | 2400 | |
| | 2401 | expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); |
| 2182 | expect(browserWindowConstructor).toHaveBeenCalledWith( | 2402 | expect(browserWindowConstructor).toHaveBeenCalledWith( |
| 2183 | expect.objectContaining({ | 2403 | expect.objectContaining({ |
| 2184 | alwaysOnTop: true, | 2404 | alwaysOnTop: true, |
| ... | @@ -2224,6 +2444,7 @@ describe("PreviewManager", () => { | ... | @@ -2224,6 +2444,7 @@ describe("PreviewManager", () => { |
| 2224 | expect(recordingFrames).toHaveLength(1); | 2444 | expect(recordingFrames).toHaveLength(1); |
| 2225 | | 2445 | |
| 2226 | yield* manager.stopRecording("tab_pip"); | 2446 | yield* manager.stopRecording("tab_pip"); |
| | 2447 | expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); |
| 2227 | const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length; | 2448 | const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length; |
| 2228 | yield* TestClock.adjust(100); | 2449 | yield* TestClock.adjust(100); |
| 2229 | expect(capturePage).toHaveBeenCalledTimes(3); | 2450 | expect(capturePage).toHaveBeenCalledTimes(3); |
| ... | @@ -2232,7 +2453,11 @@ describe("PreviewManager", () => { | ... | @@ -2232,7 +2453,11 @@ describe("PreviewManager", () => { |
| 2232 | ); | 2453 | ); |
| 2233 | expect(recordingFrames).toHaveLength(1); | 2454 | expect(recordingFrames).toHaveLength(1); |
| 2234 | | 2455 | |
| | 2456 | setBackgroundThrottling.mockImplementationOnce(() => { |
| | 2457 | throw new Error("picture-in-picture throttling restore failed"); |
| | 2458 | }); |
| 2235 | yield* manager.closePictureInPicture("tab_pip"); | 2459 | yield* manager.closePictureInPicture("tab_pip"); |
| | 2460 | expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true], [true]]); |
| 2236 | expect(pictureInPictureWindow.close).toHaveBeenCalledOnce(); | 2461 | expect(pictureInPictureWindow.close).toHaveBeenCalledOnce(); |
| 2237 | expect(states.at(-1)?.pictureInPicture).toBe(false); | 2462 | expect(states.at(-1)?.pictureInPicture).toBe(false); |
| 2238 | const capturesAfterClose = capturePage.mock.calls.length; | 2463 | const capturesAfterClose = capturePage.mock.calls.length; |