import { it as effectIt } from "@effect/vitest"; import { DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER } from "@t3tools/contracts"; import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; import { TestClock } from "effect/testing"; import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as BrowserSession from "./BrowserSession.ts"; import * as PreviewManager from "./Manager.ts"; describe("fitPictureInPictureContentSize", () => { it("preserves the PiP content area across aspect-ratio changes", () => { expect(PreviewManager.fitPictureInPictureContentSize([480, 320], 16 / 9)).toEqual([523, 294]); expect(PreviewManager.fitPictureInPictureContentSize([480, 320], 9 / 16)).toEqual([294, 523]); }); it("does not collapse toward the minimum size when orientation changes repeatedly", () => { const portrait = PreviewManager.fitPictureInPictureContentSize([523, 294], 9 / 16); const landscape = PreviewManager.fitPictureInPictureContentSize(portrait, 16 / 9); expect(portrait).toEqual([294, 523]); expect(landscape).toEqual([523, 294]); }); }); describe("recordingFileExtension", () => { it("derives the artifact extension from the recorder's actual mime type", () => { expect(PreviewManager.recordingFileExtension("video/mp4;codecs=avc1.640028")).toBe("mp4"); expect(PreviewManager.recordingFileExtension("video/webm;codecs=vp9")).toBe("webm"); expect(PreviewManager.recordingFileExtension("video/x-matroska")).toBe("matroska"); }); }); describe("isPreviewRefreshShortcut", () => { const input = (overrides: Partial = {}) => ({ type: "keyDown", key: "r", meta: true, control: false, shift: false, alt: false, ...overrides, }) as Electron.Input; it("recognizes the platform refresh chord without matching modified variants", () => { expect(PreviewManager.isPreviewRefreshShortcut(input())).toBe(true); expect(PreviewManager.isPreviewRefreshShortcut(input({ meta: false, control: true }))).toBe( true, ); expect(PreviewManager.isPreviewRefreshShortcut(input({ shift: true }))).toBe(false); expect(PreviewManager.isPreviewRefreshShortcut(input({ type: "keyUp" }))).toBe(false); }); }); describe("isPreviewEditingShortcut", () => { const input = (platform: NodeJS.Platform, key: string, overrides: Partial = {}) => ({ type: "keyDown", key, meta: platform === "darwin", control: platform !== "darwin", shift: false, alt: false, ...overrides, }) as Electron.Input; it.each(["darwin", "linux", "win32"] as const)( "allows native editing chords on %s without allowing host shortcuts", (platform) => { for (const key of ["a", "c", "v", "x", "z", "V"]) { expect(PreviewManager.isPreviewEditingShortcut(input(platform, key), platform)).toBe(true); } const redo = platform === "win32" ? input(platform, "y") : input(platform, "z", { shift: true }); expect(PreviewManager.isPreviewEditingShortcut(redo, platform)).toBe(true); expect( PreviewManager.isPreviewEditingShortcut( input(platform, "v", { shift: true, alt: platform === "darwin" }), platform, ), ).toBe(true); for (const key of ["k", ",", "w", "j", "q", "+", "=", "-", "0", "r", "F12"]) { expect(PreviewManager.isPreviewEditingShortcut(input(platform, key), platform)).toBe(false); } for (const modifiers of [ { meta: false, control: false }, { meta: true, control: true }, { meta: platform !== "darwin", control: platform === "darwin" }, { alt: true }, { shift: true, alt: platform !== "darwin" }, ]) { expect( PreviewManager.isPreviewEditingShortcut(input(platform, "v", modifiers), platform), ).toBe(false); } expect( PreviewManager.isPreviewEditingShortcut(input(platform, "a", { shift: true }), platform), ).toBe(false); }, ); it("recognizes macOS Paste and Match Style when Option changes the key to a symbol", () => { const pasteAndMatchStyle = input("darwin", "◊", { code: "KeyV", alt: true, shift: true }); expect(PreviewManager.isPreviewEditingShortcut(pasteAndMatchStyle, "darwin")).toBe(true); for (const modifiers of [ { code: "KeyC" }, { alt: false }, { shift: false }, { control: true }, ]) { expect( PreviewManager.isPreviewEditingShortcut({ ...pasteAndMatchStyle, ...modifiers }, "darwin"), ).toBe(false); } }); }); describe("previewWindowOpenAction", () => { const details = (overrides: { readonly url?: string; readonly disposition?: Electron.HandlerDetails["disposition"]; }) => ({ url: "https://accounts.google.com/o/oauth2/auth", disposition: "new-window" as Electron.HandlerDetails["disposition"], ...overrides, }); it("opens a real window for scripted popups so the opener survives", () => { // OAuth SDKs read a null `window.open()` as a blocked popup, and they need // the opener alive to receive the credential back. expect(PreviewManager.previewWindowOpenAction(details({}))).toBe("popup"); expect( PreviewManager.previewWindowOpenAction(details({ url: "http://localhost:5173/auth" })), ).toBe("popup"); }); it("keeps target=_blank links in the preview tab", () => { expect(PreviewManager.previewWindowOpenAction(details({ disposition: "foreground-tab" }))).toBe( "navigate", ); expect(PreviewManager.previewWindowOpenAction(details({ disposition: "background-tab" }))).toBe( "navigate", ); }); it("does not hand a window to schemes that cannot be hardened", () => { // A popup skips the `will-attach-webview` hardening, so it only gets a window // when its preferences can be overridden. Chromium copies the guest's // preferences for `about:blank` and forbids overriding them. for (const url of [ "about:blank", "javascript:alert(1)", "file:///etc/passwd", "vscode://vscode-remote/ssh-remote+box/tmp", "not a url", ]) { expect(PreviewManager.previewWindowOpenAction(details({ url }))).toBe("navigate"); } }); }); const { browserWindowConstructor, clipboardItemConstructor, createFromPath, fromId, getFocusedWebContents, mkdir, showItemInFolder, webviewSend, writeFile, writeClipboard, } = vi.hoisted(() => ({ browserWindowConstructor: vi.fn(), clipboardItemConstructor: vi.fn(), createFromPath: vi.fn((): { readonly isEmpty: () => boolean; readonly toPNG: () => Buffer } => ({ isEmpty: () => false, toPNG: () => Buffer.from("png"), })), fromId: vi.fn<(_id?: number) => Electron.WebContents | null>((_id?: number) => null), getFocusedWebContents: vi.fn(() => null), mkdir: vi.fn((_path: string) => undefined), showItemInFolder: vi.fn(), webviewSend: vi.fn(), writeFile: vi.fn((_path: string, _data: Uint8Array) => undefined), writeClipboard: vi.fn(async () => undefined), })); vi.mock("electron", () => ({ BrowserWindow: browserWindowConstructor, ClipboardItem: class { constructor(data: Record) { clipboardItemConstructor(data); } }, clipboard: { write: writeClipboard, }, nativeImage: { createFromPath, }, shell: { showItemInFolder, }, session: { fromPartition: vi.fn(), }, webContents: { fromId, getFocusedWebContents, }, })); const browserSessionLayer = Layer.succeed( BrowserSession.BrowserSession, BrowserSession.BrowserSession.of({ getPartition: () => Effect.succeed("persist:t3code-preview-test"), isPartition: (partition) => partition.startsWith("persist:t3code-preview-"), getSession: () => Effect.die("unexpected getSession"), clearCookies: () => Effect.void, clearCache: () => Effect.void, }), ); const environmentLayer = Layer.succeed( DesktopEnvironment.DesktopEnvironment, DesktopEnvironment.DesktopEnvironment.of({ browserArtifactsDir: "/tmp/t3/dev/browser-artifacts", dirname: "/tmp/t3/desktop", path: { join: (...parts: ReadonlyArray) => parts.join("/"), }, } as DesktopEnvironment.DesktopEnvironment["Service"]), ); const fileSystemLayer = FileSystem.layerNoop({ makeDirectory: (path) => Effect.sync(() => { mkdir(path); }), writeFile: (path, data) => Effect.sync(() => { writeFile(path, data); }), }); const layer = PreviewManager.layer.pipe( Layer.provideMerge(browserSessionLayer), Layer.provideMerge(environmentLayer), Layer.provideMerge(fileSystemLayer), Layer.provideMerge(Path.layer), Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")), ); const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError); const withManager = ( use: ( manager: PreviewManager.PreviewManager["Service"], ) => Effect.Effect, ) => Effect.gen(function* () { const manager = yield* PreviewManager.PreviewManager; return yield* use(manager); }).pipe(Effect.provide(layer), Effect.scoped); interface TestCapturedPreviewImage { readonly toJPEG: () => Buffer; readonly getSize: () => { readonly width: number; readonly height: number }; } type TestDisplayMediaHandler = ( request: { readonly frame: { readonly frameTreeNodeId: number } | null }, callback: (streams: { video?: unknown }) => void, ) => void; interface TestHostWebContents { readonly id: number; readonly mainFrame: { readonly frameTreeNodeId: number }; readonly executeJavaScript: ReturnType; readonly isDestroyed: () => boolean; readonly session: { readonly setDisplayMediaRequestHandler: ReturnType; }; readonly displayMediaHandler: () => TestDisplayMediaHandler | undefined; } type TestPreviewWebContents = Electron.WebContents & { readonly setBackgroundThrottling: ReturnType void>>; }; const makeTestHostWebContents = (): TestHostWebContents => { let handler: TestDisplayMediaHandler | undefined; return { id: 7, mainFrame: { frameTreeNodeId: 7 }, executeJavaScript: vi.fn(async () => true), isDestroyed: () => false, session: { setDisplayMediaRequestHandler: vi.fn((next: TestDisplayMediaHandler) => { handler = next; }), }, displayMediaHandler: () => handler, }; }; const makeTestPreviewWebContents = ( capturePage: () => Promise, id = 42, hostWebContents: TestHostWebContents = makeTestHostWebContents(), ) => { const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>(); return { id, mainFrame: { routingId: id }, hostWebContents, executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), setBackgroundThrottling, isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, capturePage, } as unknown as TestPreviewWebContents; }; /** Two ready tabs (41, 42) sharing one window, so they contend for the single display-media slot. */ const setupRecordingRaceTabs = (manager: PreviewManager.PreviewManager["Service"]) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("unused-recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const host = makeTestHostWebContents(); const destroyedIds = new Set(); const makeWebContents = (id: number) => Object.assign(makeTestPreviewWebContents(capturePage, id, host), { executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), isDestroyed: () => destroyedIds.has(id), }); const webContentsById = new Map([ [41, makeWebContents(41)], [42, makeWebContents(42)], ]); fromId.mockImplementation((id) => id === undefined ? null : (webContentsById.get(id) ?? null), ); yield* manager.createTab("tab_race_a"); yield* manager.createTab("tab_race_b"); yield* manager.registerWebview("tab_race_a", 41); yield* manager.registerWebview("tab_race_b", 42); const grants: Array<{ video?: unknown }> = []; return { host, grants, destroy: (id: number) => destroyedIds.add(id), takeGrant: (frame = host.mainFrame) => host.displayMediaHandler()?.({ frame }, (value) => { grants.push(value); }), }; }); const TEST_FAVICON = "data:image/png;base64,cG5n"; const makeSourcePng = (width = 1, height = 1): Buffer => { const buffer = Buffer.alloc(24); Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(buffer); buffer.writeUInt32BE(width, 16); buffer.writeUInt32BE(height, 20); return buffer; }; const makeFaviconWebContents = (options?: { readonly fetch?: (url: string, init?: RequestInit) => Promise; readonly id?: number; readonly rasterize?: (code: string) => Promise; readonly url?: string; }) => { const sourcePng = makeSourcePng(); const listeners = new Map void>(); let currentUrl = options?.url ?? "http://localhost:3200/"; let destroyed = false; let loading = false; const fetch = vi.fn( options?.fetch ?? (async () => new Response(new Uint8Array(sourcePng), { headers: { "content-type": "image/png" }, })), ); const executeJavaScriptInIsolatedWorld = vi.fn( async (_worldId: number, scripts: ReadonlyArray<{ readonly code: string }>) => options?.rasterize ? options.rasterize(scripts[0]?.code ?? "") : TEST_FAVICON, ); const reload = vi.fn(); const loadURL = vi.fn(async (url: string) => { currentUrl = url; }); const off = vi.fn(); const debuggerOff = vi.fn(); const webContents = { id: options?.id ?? 42, isDestroyed: () => destroyed, getType: () => "webview", getURL: () => currentUrl, getTitle: () => "Preview", isLoading: () => loading, isDevToolsOpened: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, reload, reloadIgnoringCache: vi.fn(), loadURL, on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), off, ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, session: { fetch }, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), executeJavaScriptInIsolatedWorld, debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: debuggerOff, }, }; return { executeJavaScriptInIsolatedWorld, fetch, debuggerOff, listeners, loadURL, off, reload, setDestroyed: (value: boolean) => { destroyed = value; }, setLoading: (value: boolean) => { loading = value; }, setUrl: (url: string) => { currentUrl = url; }, webContents: webContents as never, }; }; const settle = function* (until: () => boolean) { for (let attempt = 0; attempt < 30 && !until(); attempt++) { yield* Effect.promise(() => Promise.resolve()); } }; const makeTestPictureInPictureWindow = (loadURL: () => Promise = async () => undefined) => { const listeners = new Map void>(); const webContentsListeners = new Map void>(); const send = vi.fn(); let destroyed = false; const webContents = { on: vi.fn((event: string, listener: () => void) => { webContentsListeners.set(event, listener); }), off: vi.fn((event: string) => { webContentsListeners.delete(event); }), send, }; const pictureInPictureWindow = { isDestroyed: vi.fn(() => destroyed), once: vi.fn((event: string, listener: () => void) => { listeners.set(event, listener); }), setAlwaysOnTop: vi.fn(), setVisibleOnAllWorkspaces: vi.fn(), setAspectRatio: vi.fn(), getContentSize: vi.fn(() => [480, 320]), setContentSize: vi.fn(), loadURL: vi.fn(loadURL), showInactive: vi.fn(() => { if (destroyed) throw new Error("Picture-in-picture window is closed."); }), close: vi.fn(() => { if (destroyed) return; destroyed = true; listeners.get("closed")?.(); }), get webContents() { if (destroyed) throw new Error("Picture-in-picture window is closed."); return webContents; }, }; return { pictureInPictureWindow, send, webContentsListeners }; }; describe("PreviewManager", () => { beforeEach(() => { browserWindowConstructor.mockReset(); fromId.mockClear(); getFocusedWebContents.mockReset(); getFocusedWebContents.mockReturnValue(null); mkdir.mockClear(); writeFile.mockClear(); showItemInFolder.mockClear(); clipboardItemConstructor.mockClear(); writeClipboard.mockClear(); createFromPath.mockClear(); webviewSend.mockClear(); }); effectIt.effect("keeps preview shortcuts out of the host window", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); const sendInputEvent = vi.fn(); const hostWebContents = { sendInputEvent }; Object.assign(preview.webContents, { hostWebContents }); fromId.mockReturnValue(preview.webContents); getFocusedWebContents.mockReturnValue(preview.webContents as never); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: hostWebContents, } as never); yield* manager.createTab("tab_keys"); yield* manager.registerWebview("tab_keys", 42); expect( (preview.webContents as Electron.WebContents).setIgnoreMenuShortcuts, ).toHaveBeenCalledWith(true); const beforeInput = preview.listeners.get("before-input-event")!; for (const control of [false, true]) { for (const key of ["k", ",", "w", "j", "q", "+"]) { for (const type of ["keyDown", "keyUp"]) { const preventDefault = vi.fn(); beforeInput( { preventDefault } as never, { type, key, meta: !control, control, shift: key === "j", alt: false } as never, ); yield* Effect.yieldNow; expect(preventDefault).not.toHaveBeenCalled(); expect( (preview.webContents as Electron.WebContents).setIgnoreMenuShortcuts, ).toHaveBeenLastCalledWith(true); } } } expect(sendInputEvent).not.toHaveBeenCalled(); const preventDefault = vi.fn(); beforeInput( { preventDefault } as never, { type: "keyDown", key: "r", meta: true, control: false, shift: false, alt: false, } as never, ); yield* Effect.yieldNow; expect(preventDefault).toHaveBeenCalledOnce(); expect(preview.reload).toHaveBeenCalledOnce(); expect(sendInputEvent).not.toHaveBeenCalled(); }), ), ); effectIt.effect("preserves focused browser editing in tabs and sign-in popups", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); yield* manager.createTab("tab_editing"); yield* manager.registerWebview("tab_editing", 42); const popup = makeFaviconWebContents({ id: 43 }); preview.listeners.get("did-create-window")!({ webContents: popup.webContents } as never); expect( (popup.webContents as Electron.WebContents).setIgnoreMenuShortcuts, ).toHaveBeenCalledWith(true); for (const browser of [preview, popup]) { const contents = browser.webContents as Electron.WebContents; getFocusedWebContents.mockReturnValue(browser.webContents as never); const beforeInput = browser.listeners.get("before-input-event")!; const preventDefault = vi.fn(); const input = { type: "keyDown", key: "v", meta: true, control: false, shift: false, alt: false, }; beforeInput({ preventDefault } as never, input as never); expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(false); // Releasing Command must not disable native fallback for the pending paste. beforeInput( { preventDefault } as never, { ...input, type: "keyUp", key: "Meta", meta: false } as never, ); expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(false); beforeInput({ preventDefault } as never, { ...input, key: "w" } as never); expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true); // An injected paste in an unfocused guest cannot edit the active renderer. getFocusedWebContents.mockReturnValue(null); beforeInput({ preventDefault } as never, input as never); expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true); expect(preventDefault).not.toHaveBeenCalled(); } }), ), ); effectIt.effect("reports an unregistered webview as temporarily unavailable", () => withManager((manager) => Effect.gen(function* () { expect(yield* manager.automationStatus("tab_1")).toEqual({ available: false, visible: true, tabId: "tab_1", url: null, title: null, loading: false, }); yield* manager.createTab("tab_1"); expect(yield* manager.automationStatus("tab_1")).toEqual({ available: false, visible: true, tabId: "tab_1", url: null, title: null, loading: false, }); expect(fromId).not.toHaveBeenCalled(); }), ), ); effectIt.effect("rejects a destroyed webview during registration", () => withManager((manager) => Effect.gen(function* () { const getType = vi.fn(() => "webview" as const); fromId.mockReturnValue({ id: 42, isDestroyed: () => true, getType, } as never); yield* manager.createTab("tab_destroyed_registration"); const exit = yield* Effect.exit(manager.registerWebview("tab_destroyed_registration", 42)); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Option.getOrThrow(Cause.findErrorOption(exit.cause))).toMatchObject({ _tag: "PreviewWebContentsNotFoundError", tabId: "tab_destroyed_registration", webContentsId: 42, }); } expect(getType).not.toHaveBeenCalled(); }), ), ); effectIt.effect("isolates failed state listeners and continues delivery", () => { const loggedErrors: Array = []; const logger = Logger.make(({ message }) => { for (const value of Array.isArray(message) ? message : [message]) { if (typeof value === "object" && value !== null && "cause" in value) { loggedErrors.push(Cause.squash(value.cause as Cause.Cause)); } } }); const deliveryError = new ElectronWindow.ElectronWindowOperationError({ operation: "send-window-message", platform: "darwin", windowId: 42, channel: "preview:state-change", cause: new Error("renderer unavailable"), }); const delivered = vi.fn(); return withManager((manager) => Effect.gen(function* () { yield* manager.subscribeStateChanges(() => Effect.die(deliveryError)); yield* manager.subscribeStateChanges((tabId, state) => Effect.sync(() => { delivered(tabId, state); }), ); const state = yield* manager.createTab("tab_listener_failure"); expect(delivered).toHaveBeenCalledOnce(); expect(delivered).toHaveBeenCalledWith("tab_listener_failure", state); expect(loggedErrors).toHaveLength(1); expect(loggedErrors[0]).toBeInstanceOf(ElectronWindow.ElectronWindowOperationError); expect(loggedErrors[0]).toMatchObject({ operation: "send-window-message", windowId: 42, channel: "preview:state-change", }); }), ).pipe( Effect.provide( Logger.layer([logger], { mergeWithExisting: false, }), ), ); }); effectIt.effect("does not swallow state listener interruption", () => withManager((manager) => Effect.gen(function* () { const exit = yield* Effect.scoped( Effect.gen(function* () { yield* manager.subscribeStateChanges(() => Effect.interrupt); return yield* Effect.exit(manager.createTab("tab_interrupted_listener")); }), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Cause.hasInterrupts(exit.cause)).toBe(true); } }), ), ); effectIt.effect("queues navigation until the webview registers", () => withManager((manager) => Effect.gen(function* () { const loadURL = vi.fn(async () => undefined); const listeners = new Map void>(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "about:blank", getTitle: () => "", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, loadURL, on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.navigate("tab_pending", "localhost:3200"); expect(yield* manager.automationStatus("tab_pending")).toEqual({ available: false, visible: true, tabId: "tab_pending", url: "http://localhost:3200/", title: "", loading: true, }); yield* manager.registerWebview("tab_pending", 42); yield* Effect.yieldNow; expect(loadURL).toHaveBeenCalledOnce(); expect(loadURL).toHaveBeenCalledWith("http://localhost:3200/"); }), ), ); effectIt.effect("detaches a destroyed webview instead of navigating it", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_destroyed_navigation"); yield* manager.registerWebview("tab_destroyed_navigation", 42); yield* manager.setColorScheme("tab_destroyed_navigation", "dark"); preview.setDestroyed(true); yield* manager.navigate("tab_destroyed_navigation", "https://example.com/"); expect(preview.loadURL).not.toHaveBeenCalled(); expect(preview.reload).not.toHaveBeenCalled(); expect(preview.off).toHaveBeenCalled(); expect(preview.debuggerOff).toHaveBeenCalled(); expect(states.at(-1)).toMatchObject({ webContentsId: null, navStatus: { kind: "Loading", url: "https://example.com/" }, }); }), ), ); effectIt.effect("detaches through the pinned debugger after the webview is destroyed", () => withManager((manager) => Effect.gen(function* () { // Real Electron throws on any `wc.debugger` access once the // WebContents is destroyed, so cleanup must go through the debugger // reference captured at attach time (electron/electron#53376). let destroyed = false; let attached = false; const debuggerOff = vi.fn(); const debuggerDetach = vi.fn(() => { attached = false; }); const wcDebugger = { isAttached: () => attached, attach: vi.fn(() => { attached = true; }), detach: debuggerDetach, sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: debuggerOff, }; fromId.mockReturnValue({ id: 42, isDestroyed: () => destroyed, getType: () => "webview", getURL: () => "http://localhost:3200/", getTitle: () => "Preview", isLoading: () => false, isDevToolsOpened: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, reload: vi.fn(), loadURL: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), get debugger() { if (destroyed) throw new Error("Object has been destroyed"); return wcDebugger; }, } as never); yield* manager.createTab("tab_pinned_debugger"); yield* manager.registerWebview("tab_pinned_debugger", 42); yield* manager.setColorScheme("tab_pinned_debugger", "dark"); expect(attached).toBe(true); destroyed = true; yield* manager.navigate("tab_pinned_debugger", "https://example.com/"); expect(debuggerOff).toHaveBeenCalledWith("message", expect.any(Function)); expect(debuggerDetach).toHaveBeenCalledOnce(); }), ), ); effectIt.effect("does not let destroyed-webview cleanup detach a same-id replacement", () => withManager((manager) => Effect.gen(function* () { const previous = makeFaviconWebContents(); const replacement = makeFaviconWebContents({ url: "https://example.com/" }); let current = previous.webContents; let startReplacementRegistration: () => void = () => void 0; const replacementReady = new Promise((resolve) => { startReplacementRegistration = resolve; }); fromId.mockImplementation(() => current); yield* manager.createTab("tab_destroyed_replacement_race"); yield* manager.registerWebview("tab_destroyed_replacement_race", 42); yield* manager.setColorScheme("tab_destroyed_replacement_race", "dark"); const replacementRegistration = yield* Effect.promise(() => replacementReady).pipe( Effect.flatMap(() => manager.registerWebview("tab_destroyed_replacement_race", 42)), Effect.forkChild({ startImmediately: true }), ); previous.setDestroyed(true); previous.debuggerOff.mockImplementationOnce(() => { current = replacement.webContents; startReplacementRegistration(); }); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.navigate("tab_destroyed_replacement_race", "https://example.com/"); const registrationExit = yield* Fiber.await(replacementRegistration); expect(Exit.isSuccess(registrationExit)).toBe(true); expect(previous.off).toHaveBeenCalled(); expect(replacement.off).not.toHaveBeenCalled(); expect(states.at(-1)).toMatchObject({ webContentsId: 42, navStatus: { kind: "Loading", url: "https://example.com/" }, }); }), ), ); effectIt.effect("publishes a canonical favicon origin while the page is loading", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents({ url: `http://localhost:3200/${"x".repeat(3_000)}`, }); preview.setLoading(true); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_loading"); yield* manager.registerWebview("tab_favicon_loading", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); expect(states.at(-1)?.favicon).toMatchObject({ dataUrl: TEST_FAVICON, pageUrl: "http://localhost:3200", }); expect(states.at(-1)?.favicon?.capturedAt).toEqual(expect.any(Number)); }), ), ); effectIt.effect("shares an identical in-flight event and lets a changed event win", () => withManager((manager) => Effect.gen(function* () { let resolveFirst!: (response: Response) => void; const firstResponse = new Promise((resolve) => { resolveFirst = resolve; }); const preview = makeFaviconWebContents({ fetch: (url) => url.endsWith("first.png") ? firstResponse : Promise.resolve( new Response(new Uint8Array(makeSourcePng()), { headers: { "content-type": "image/png" }, }), ), }); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_latest"); yield* manager.registerWebview("tab_favicon_latest", 42); const faviconUpdated = preview.listeners.get("page-favicon-updated")!; faviconUpdated({} as never, ["http://localhost:3200/first.png"] as never); faviconUpdated({} as never, ["http://localhost:3200/first.png"] as never); yield* settle(() => preview.fetch.mock.calls.length === 1); faviconUpdated({} as never, ["http://localhost:3200/second.png"] as never); yield* settle(() => states.at(-1)?.favicon !== undefined); resolveFirst( new Response(new Uint8Array(makeSourcePng()), { headers: { "content-type": "image/png" }, }), ); yield* settle(() => false); expect(preview.fetch).toHaveBeenCalledTimes(2); expect(states.filter((state) => state.favicon !== undefined)).toHaveLength(1); }), ), ); effectIt.effect("allows an identical retry after an undecodable capture", () => withManager((manager) => Effect.gen(function* () { let rasterizations = 0; const preview = makeFaviconWebContents({ rasterize: async () => (++rasterizations === 1 ? null : TEST_FAVICON), }); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_retry"); yield* manager.registerWebview("tab_favicon_retry", 42); const faviconUpdated = preview.listeners.get("page-favicon-updated")!; faviconUpdated({} as never, ["http://localhost:3200/favicon.png"] as never); yield* settle(() => rasterizations === 1); yield* settle(() => false); faviconUpdated({} as never, ["http://localhost:3200/favicon.png"] as never); yield* settle(() => states.at(-1)?.favicon !== undefined); expect(rasterizations).toBe(2); expect(states.at(-1)?.favicon?.dataUrl).toBe(TEST_FAVICON); }), ), ); effectIt.effect("does not publish a capture invalidated by navigation", () => withManager((manager) => Effect.gen(function* () { let resolveFetch!: (response: Response) => void; const preview = makeFaviconWebContents({ fetch: () => new Promise((resolve) => { resolveFetch = resolve; }), }); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_navigation"); yield* manager.registerWebview("tab_favicon_navigation", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => preview.fetch.mock.calls.length === 1); preview.listeners.get("did-start-navigation")?.({ isMainFrame: true, isSameDocument: false, } as never); preview.setUrl("https://example.com/"); resolveFetch( new Response(new Uint8Array(makeSourcePng()), { headers: { "content-type": "image/png" }, }), ); yield* settle(() => false); expect(states.some((state) => state.favicon !== undefined)).toBe(false); }), ), ); effectIt.effect("retains a favicon when reloading the current URL without a new event", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_reload"); yield* manager.registerWebview("tab_favicon_reload", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); yield* manager.navigate("tab_favicon_reload", "http://localhost:3200/"); expect(preview.reload).toHaveBeenCalledOnce(); expect(states.at(-1)?.favicon?.dataUrl).toBe(TEST_FAVICON); }), ), ); effectIt.effect("clears a published favicon after a confirmed cross-origin navigation", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_origin"); yield* manager.registerWebview("tab_favicon_origin", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); preview.setUrl("https://example.com/"); preview.listeners.get("did-navigate")?.({} as never); yield* settle(() => states.at(-1)?.navStatus.kind === "Success"); expect(states.at(-1)?.favicon).toBeUndefined(); }), ), ); effectIt.effect( "retains the previous document icon across a failed cross-origin navigation", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_failed_origin"); yield* manager.registerWebview("tab_favicon_failed_origin", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); preview.listeners.get("did-fail-load")?.( {} as never, -105 as never, "Name not resolved" as never, "https://unreachable.example/" as never, true as never, ); yield* settle(() => states.at(-1)?.navStatus.kind === "LoadFailed"); expect(states.at(-1)?.favicon?.dataUrl).toBe(TEST_FAVICON); preview.listeners.get("did-navigate")?.({} as never); yield* settle(() => states.at(-1)?.navStatus.kind === "Success"); expect(states.at(-1)?.favicon?.dataUrl).toBe(TEST_FAVICON); }), ), ); effectIt.effect("does not resurrect an icon after a confirmed about:blank document", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_blank"); yield* manager.registerWebview("tab_favicon_blank", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); preview.setUrl("about:blank"); preview.listeners.get("did-navigate")?.({} as never); yield* settle(() => states.at(-1)?.navStatus.kind === "Idle"); expect(states.at(-1)?.favicon).toBeUndefined(); preview.setUrl("http://localhost:3200/"); preview.listeners.get("did-navigate")?.({} as never); yield* settle(() => states.at(-1)?.navStatus.kind === "Success"); expect(states.at(-1)?.favicon).toBeUndefined(); }), ), ); effectIt.effect("clears a published favicon when a replacement webview attaches", () => withManager((manager) => Effect.gen(function* () { const initial = makeFaviconWebContents({ id: 42 }); const replacement = makeFaviconWebContents({ id: 43 }); fromId.mockImplementation((id?: number) => { if (id === 42) return initial.webContents; if (id === 43) return replacement.webContents; return null; }); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_replace"); yield* manager.registerWebview("tab_favicon_replace", 42); initial.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); yield* manager.registerWebview("tab_favicon_replace", 43); expect(states.at(-1)?.webContentsId).toBe(43); expect(states.at(-1)?.favicon).toBeUndefined(); }), ), ); effectIt.effect("ignores an old capture that completes after webview replacement", () => withManager((manager) => Effect.gen(function* () { let resolveFetch!: (response: Response) => void; const initial = makeFaviconWebContents({ id: 42, fetch: () => new Promise((resolve) => { resolveFetch = resolve; }), }); const replacement = makeFaviconWebContents({ id: 43 }); fromId.mockImplementation((id?: number) => id === 42 ? initial.webContents : id === 43 ? replacement.webContents : null, ); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_late_replace"); yield* manager.registerWebview("tab_favicon_late_replace", 42); initial.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => initial.fetch.mock.calls.length === 1); yield* manager.registerWebview("tab_favicon_late_replace", 43); resolveFetch( new Response(new Uint8Array(makeSourcePng()), { headers: { "content-type": "image/png" }, }), ); yield* settle(() => false); expect(states.at(-1)?.webContentsId).toBe(43); expect( states.some((state) => state.webContentsId === 43 && state.favicon !== undefined), ).toBe(false); }), ), ); effectIt.effect("treats a reused WebContents id as a new attachment", () => withManager((manager) => Effect.gen(function* () { const initial = makeFaviconWebContents({ id: 42 }); const replacement = makeFaviconWebContents({ id: 42 }); let active = initial.webContents; fromId.mockImplementation(() => active); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_reused_id"); yield* manager.registerWebview("tab_favicon_reused_id", 42); initial.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); active = replacement.webContents; yield* manager.registerWebview("tab_favicon_reused_id", 42); expect(states.at(-1)?.favicon).toBeUndefined(); expect(initial.off).toHaveBeenCalled(); expect(replacement.listeners.has("page-favicon-updated")).toBe(true); }), ), ); effectIt.effect("preserves a favicon when the active attachment registers again", () => withManager((manager) => Effect.gen(function* () { const preview = makeFaviconWebContents(); fromId.mockReturnValue(preview.webContents); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_favicon_reregister"); yield* manager.registerWebview("tab_favicon_reregister", 42); preview.listeners.get("page-favicon-updated")?.( {} as never, ["http://localhost:3200/favicon.png"] as never, ); yield* settle(() => states.at(-1)?.favicon !== undefined); yield* manager.registerWebview("tab_favicon_reregister", 42); expect(states.at(-1)?.favicon?.dataUrl).toBe(TEST_FAVICON); }), ), ); // The guest reports whatever zoom level Chromium handed it from the app // window, so the tab's own zoom is the source of truth in both directions: // asserted onto every guest, never read back off one. effectIt.effect("keeps the tab's own zoom instead of the guest's reported zoom", () => withManager((manager) => Effect.gen(function* () { let effectiveZoom = 0.9; let zoomReadable = true; let url = "https://example.com"; const listeners = new Map void>(); const setZoomFactor = vi.fn(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => url, getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => { if (!zoomReadable) throw new Error("zoom unavailable"); return effectiveZoom; }, setZoomFactor, setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_zoom"); yield* manager.registerWebview("tab_zoom", 42); expect(states.at(-1)?.zoomFactor).toBe(1); expect(setZoomFactor).toHaveBeenCalledWith(1); // An app zoom leaves the guest reporting the inherited level. Navigating // must not adopt it as the preview's zoom. effectiveZoom = 0.8; url = "https://example.com/after-app-zoom"; listeners.get("did-navigate")?.(); yield* Effect.yieldNow; expect(states.at(-1)?.navStatus).toEqual({ kind: "Success", url, title: "Example", }); expect(states.at(-1)?.zoomFactor).toBe(1); // Only the preview's own zoom controls move it. yield* manager.zoomIn("tab_zoom"); expect(setZoomFactor).toHaveBeenCalledWith(1.1); expect(states.at(-1)?.zoomFactor).toBe(1.1); zoomReadable = false; listeners.get("did-navigate")?.(); yield* Effect.yieldNow; expect(states.at(-1)?.zoomFactor).toBe(1.1); const replacementSetZoomFactor = vi.fn(); fromId.mockReturnValue({ id: 43, isDestroyed: () => false, getType: () => "webview", getURL: () => url, getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: replacementSetZoomFactor, setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.registerWebview("tab_zoom", 43); expect(replacementSetZoomFactor).toHaveBeenCalledWith(1.1); expect(states.at(-1)?.zoomFactor).toBe(1.1); }), ), ); // Zooming the app UI pushes the window's zoom level onto every guest, so the // preview has to be put back at the zoom the user gave it. effectIt.effect("re-applies each tab's own zoom when the app window zooms", () => withManager((manager) => Effect.gen(function* () { const setZoomFactor = vi.fn(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor, setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_reapply"); yield* manager.registerWebview("tab_reapply", 42); yield* manager.zoomIn("tab_reapply"); setZoomFactor.mockClear(); yield* manager.reapplyZoom(); expect(setZoomFactor).toHaveBeenCalledTimes(1); expect(setZoomFactor).toHaveBeenCalledWith(1.1); }), ), ); // did-attach and dom-ready both re-register the guest that is already // attached, and a guest that just inherited the app window's zoom needs its // own back — without that round trip republishing tab state. effectIt.effect("re-asserts the tab's zoom when the active guest registers again", () => withManager((manager) => Effect.gen(function* () { const setZoomFactor = vi.fn(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor, setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_reregister_zoom"); yield* manager.registerWebview("tab_reregister_zoom", 42); yield* manager.zoomIn("tab_reregister_zoom"); setZoomFactor.mockClear(); const publishedBefore = states.length; yield* manager.registerWebview("tab_reregister_zoom", 42); expect(setZoomFactor).toHaveBeenCalledWith(1.1); expect(states.length).toBe(publishedBefore); expect(states.at(-1)?.zoomFactor).toBe(1.1); }), ), ); effectIt.effect("emulates prefers-color-scheme and re-applies it across webview swaps", () => withManager((manager) => Effect.gen(function* () { const makeWebContents = (id: number) => { const sendCommand = vi.fn(async () => undefined); return { sendCommand, wc: { id, isDestroyed: () => false, isDevToolsOpened: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, } as never, }; }; const first = makeWebContents(42); fromId.mockReturnValue(first.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_scheme"); yield* manager.registerWebview("tab_scheme", 42); yield* Effect.yieldNow; yield* manager.setColorScheme("tab_scheme", "dark"); expect(first.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", { features: [{ name: "prefers-color-scheme", value: "dark" }], }); expect(states.at(-1)?.colorScheme).toBe("dark"); const replacement = makeWebContents(43); fromId.mockReturnValue(replacement.wc); yield* manager.registerWebview("tab_scheme", 43); yield* Effect.yieldNow; expect(replacement.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", { features: [{ name: "prefers-color-scheme", value: "dark" }], }); expect(states.at(-1)?.colorScheme).toBe("dark"); yield* manager.setColorScheme("tab_scheme", "system"); expect(replacement.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", { features: [{ name: "prefers-color-scheme", value: "" }], }); expect(states.at(-1)?.colorScheme).toBe("system"); }), ), ); const makeAudioWebContents = (id: number) => { const listeners = new Map void>(); const setAudioMuted = vi.fn(); let audible = false; let audibleAfterFirstRead = false; let audibleReads = 0; return { setAudioMuted, emitAudioState: (next: boolean) => { audible = next; listeners.get("audio-state-changed")?.({ audible: next } as never); }, /** * Starts playing between the attach-time read and the post-attach * reconcile, without a delivered event — the window in which * audio-state-changed fires against a guest the tab does not own yet. */ startPlayingAfterFirstRead: () => { audibleAfterFirstRead = true; }, wc: { id, isDestroyed: () => false, isDevToolsOpened: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted, isCurrentlyAudible: () => { audibleReads += 1; if (audibleAfterFirstRead && audibleReads > 1) return true; return audible; }, loadURL: vi.fn(async () => undefined), on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), off: vi.fn((event: string) => { listeners.delete(event); }), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never, }; }; effectIt.effect("mutes the guest and re-applies the mute across webview swaps", () => withManager((manager) => Effect.gen(function* () { const first = makeAudioWebContents(42); fromId.mockReturnValue(first.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audio"); yield* manager.registerWebview("tab_audio", 42); yield* Effect.yieldNow; expect(states.at(-1)?.audioMuted).toBe(false); yield* manager.setAudioMuted("tab_audio", true); expect(first.setAudioMuted).toHaveBeenCalledWith(true); expect(states.at(-1)?.audioMuted).toBe(true); const replacement = makeAudioWebContents(43); fromId.mockReturnValue(replacement.wc); yield* manager.registerWebview("tab_audio", 43); yield* Effect.yieldNow; expect(replacement.setAudioMuted).toHaveBeenCalledWith(true); expect(states.at(-1)?.audioMuted).toBe(true); yield* manager.setAudioMuted("tab_audio", false); expect(replacement.setAudioMuted).toHaveBeenLastCalledWith(false); expect(states.at(-1)?.audioMuted).toBe(false); }), ), ); effectIt.effect("fails and rolls back when the guest refuses a mute", () => withManager((manager) => Effect.gen(function* () { const guest = makeAudioWebContents(42); fromId.mockReturnValue(guest.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audio_fail"); yield* manager.registerWebview("tab_audio_fail", 42); yield* Effect.yieldNow; guest.setAudioMuted.mockImplementationOnce(() => { throw new Error("guest refused"); }); const exit = yield* manager.setAudioMuted("tab_audio_fail", true).pipe(Effect.exit); // Reporting success would draw the tab as muted while it keeps playing. expect(Exit.isFailure(exit)).toBe(true); expect(states.at(-1)?.audioMuted).toBe(false); }), ), ); effectIt.effect("still registers a guest that refuses the mute reassert", () => withManager((manager) => Effect.gen(function* () { const first = makeAudioWebContents(42); fromId.mockReturnValue(first.wc); yield* manager.createTab("tab_audio_attach_fail"); yield* manager.registerWebview("tab_audio_attach_fail", 42); yield* Effect.yieldNow; yield* manager.setAudioMuted("tab_audio_attach_fail", true); const replacement = makeAudioWebContents(43); // Fails the post-attach settle, not the pre-publish apply. replacement.setAudioMuted.mockImplementationOnce(() => undefined); replacement.setAudioMuted.mockImplementationOnce(() => { throw new Error("guest went away"); }); fromId.mockReturnValue(replacement.wc); // Reconciliation is best-effort: a guest dying mid-attach must not fail // the registration it was attaching for. const exit = yield* manager.registerWebview("tab_audio_attach_fail", 43).pipe(Effect.exit); expect(Exit.isSuccess(exit)).toBe(true); }), ), ); effectIt.effect("reconciles audibility that changed while the guest attached", () => withManager((manager) => Effect.gen(function* () { const guest = makeAudioWebContents(42); guest.startPlayingAfterFirstRead(); fromId.mockReturnValue(guest.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audio_window"); yield* manager.registerWebview("tab_audio_window", 42); yield* Effect.yieldNow; // audio-state-changed for this transition was dropped: it fired before // the tab owned the guest. Without a post-attach reconcile the icon // stays wrong until the next real transition, which may never come. expect(states.at(-1)?.audible).toBe(true); }), ), ); effectIt.effect("publishes audibility transitions and drops repeats", () => withManager((manager) => Effect.gen(function* () { const guest = makeAudioWebContents(42); fromId.mockReturnValue(guest.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audible"); yield* manager.registerWebview("tab_audible", 42); yield* Effect.yieldNow; expect(states.at(-1)?.audible).toBe(false); guest.emitAudioState(true); yield* Effect.yieldNow; expect(states.at(-1)?.audible).toBe(true); // Chromium re-emits per media element; only real transitions publish. const publishedAfterFirst = states.length; guest.emitAudioState(true); yield* Effect.yieldNow; expect(states.length).toBe(publishedAfterFirst); guest.emitAudioState(false); yield* Effect.yieldNow; expect(states.at(-1)?.audible).toBe(false); expect(states.length).toBeGreaterThan(publishedAfterFirst); }), ), ); effectIt.effect("ignores audio state from a replaced guest", () => withManager((manager) => Effect.gen(function* () { const first = makeAudioWebContents(42); fromId.mockReturnValue(first.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audio_stale"); yield* manager.registerWebview("tab_audio_stale", 42); yield* Effect.yieldNow; const replacement = makeAudioWebContents(43); fromId.mockReturnValue(replacement.wc); yield* manager.registerWebview("tab_audio_stale", 43); yield* Effect.yieldNow; const publishedBefore = states.length; first.emitAudioState(true); yield* Effect.yieldNow; expect(states.length).toBe(publishedBefore); expect(states.at(-1)?.audible).toBe(false); }), ), ); effectIt.effect("carries mute and audibility across navigation", () => withManager((manager) => Effect.gen(function* () { const guest = makeAudioWebContents(42); fromId.mockReturnValue(guest.wc); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_audio_nav"); yield* manager.registerWebview("tab_audio_nav", 42); yield* Effect.yieldNow; yield* manager.setAudioMuted("tab_audio_nav", true); guest.emitAudioState(true); yield* Effect.yieldNow; expect(states.at(-1)?.audible).toBe(true); yield* manager.navigate("tab_audio_nav", "https://example.com/next"); yield* Effect.yieldNow; // navigate runs before loadURL swaps the document, so the old page can // still be playing. Dropping audibility here would lose the speaker // with no transition left to bring it back. expect(states.at(-1)?.audioMuted).toBe(true); expect(states.at(-1)?.audible).toBe(true); // Chromium reports the real stop once the new document takes over. guest.emitAudioState(false); yield* Effect.yieldNow; expect(states.at(-1)?.audible).toBe(false); }), ), ); effectIt.effect("blocks late webview and capture starts during tab close", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("close-race-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const firstWebContents = makeTestPreviewWebContents(capturePage, 42); const replacementWebContents = makeTestPreviewWebContents(capturePage, 43); const replacementListenerSpies = replacementWebContents as unknown as { readonly on: ReturnType; readonly off: ReturnType; readonly ipc: { readonly off: ReturnType }; }; fromId.mockImplementation((id) => { if (id === 42) return firstWebContents; if (id === 43) return replacementWebContents; return null; }); const { pictureInPictureWindow } = makeTestPictureInPictureWindow(); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); yield* manager.createTab("tab_close_register_race"); yield* manager.registerWebview("tab_close_register_race", 42); yield* manager.openPictureInPicture("tab_close_register_race"); const closeCleanupPaused = yield* Deferred.make(); const continueCloseCleanup = yield* Deferred.make(); yield* manager.subscribeStateChanges((_tabId, state) => !state.pictureInPicture && state.webContentsId === 42 ? Deferred.succeed(closeCleanupPaused, undefined).pipe( Effect.andThen(Deferred.await(continueCloseCleanup)), ) : Effect.void, ); const closeFiber = yield* manager .closeTab("tab_close_register_race") .pipe(Effect.forkChild({ startImmediately: true })); yield* Deferred.await(closeCleanupPaused); const recreateFiber = yield* manager .createTab("tab_close_register_race") .pipe(Effect.forkChild({ startImmediately: true })); const registrationFiber = yield* manager .registerWebview("tab_close_register_race", 43) .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.yieldNow; expect(replacementListenerSpies.on).not.toHaveBeenCalled(); yield* manager.closeTab("tab_close_register_race"); const recordingExit = yield* Effect.exit(manager.startRecording("tab_close_register_race")); yield* Deferred.succeed(continueCloseCleanup, undefined); yield* Fiber.join(closeFiber); const recreated = yield* Fiber.join(recreateFiber); const registrationExit = yield* Fiber.await(registrationFiber); for (const exit of [registrationExit, recordingExit] as ReadonlyArray< Exit.Exit >) { expect(Exit.isFailure(exit)).toBe(true); if (Exit.isSuccess(exit)) continue; expect(Option.getOrThrow(Cause.findErrorOption(exit.cause))).toMatchObject({ _tag: "PreviewTabNotFoundError", tabId: "tab_close_register_race", }); } expect(replacementListenerSpies.on).not.toHaveBeenCalled(); expect(replacementListenerSpies.off).not.toHaveBeenCalled(); expect(replacementListenerSpies.ipc.off).not.toHaveBeenCalled(); expect(capturePage).toHaveBeenCalledOnce(); expect(recreated.webContentsId).toBeNull(); }), ), ); effectIt.effect("keeps a main-frame load failure visible until a retry starts", () => withManager((manager) => Effect.gen(function* () { const url = "http://localhost:5733/"; let loading = false; const listeners = new Map void>(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => url, getTitle: () => "localhost:5733", isLoading: () => loading, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); const statuses: PreviewManager.PreviewNavStatus[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { statuses.push(state.navStatus); }), ); yield* manager.createTab("tab_failed"); yield* manager.registerWebview("tab_failed", 42); listeners.get("did-fail-load")?.( {}, -105, "ERR_NAME_NOT_RESOLVED", "https://missing-frame.example/", false, ); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("Success"); loading = true; listeners.get("did-start-loading")?.(); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("Loading"); loading = false; listeners.get("did-fail-load")?.({}, -102, "ERR_CONNECTION_REFUSED", url, true); listeners.get("did-stop-loading")?.(); listeners.get("page-title-updated")?.(); yield* Effect.yieldNow; expect(statuses.at(-1)).toEqual({ kind: "LoadFailed", url, title: "localhost:5733", code: -102, description: "ERR_CONNECTION_REFUSED", }); loading = true; listeners.get("did-start-loading")?.(); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("Loading"); loading = false; listeners.get("did-stop-loading")?.(); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("Success"); listeners.get("did-fail-load")?.({}, -102, "ERR_CONNECTION_REFUSED", url, true); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("LoadFailed"); listeners.get("did-navigate")?.(); yield* Effect.yieldNow; expect(statuses.at(-1)?.kind).toBe("Success"); }), ), ); effectIt.effect("captures a PNG screenshot into browser artifacts", () => withManager((manager) => Effect.gen(function* () { const png = Buffer.from("preview-png"); const capturePage = vi.fn(async () => ({ toPNG: () => png })); const listeners = new Map void>(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com:8443/path?query=value", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: never[]) => void) => { listeners.set(event, listener); }), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, capturePage, } as never); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); expect(webviewSend).toHaveBeenCalledWith( "preview:annotation-theme", expect.objectContaining({ colorScheme: "light", primary: "oklch(0.488 0.217 264)", }), ); const artifact = yield* manager.captureScreenshot("tab_1"); expect(capturePage).toHaveBeenCalledOnce(); expect(mkdir).toHaveBeenCalledWith("/tmp/t3/dev/browser-artifacts"); expect(writeFile).toHaveBeenCalledWith(artifact.path, png); expect(artifact).toMatchObject({ tabId: "tab_1", mimeType: "image/png", sizeBytes: png.byteLength, }); expect(artifact.path).toMatch( /\/browser-artifacts\/browser-screenshot-example-com-[^.]+\.png$/, ); // Chromium reports UnknownVizError while a hidden guest warms its // first compositor frame, so transient failures are retried. capturePage.mockClear(); capturePage.mockRejectedValueOnce(new Error("UnknownVizError")); capturePage.mockRejectedValueOnce(new Error("UnknownVizError")); const retriedFiber = yield* Effect.exit(manager.captureScreenshot("tab_1")).pipe( Effect.forkChild({ startImmediately: true }), ); yield* TestClock.adjust(1_000); const retriedExit = yield* Fiber.join(retriedFiber); expect(Exit.isSuccess(retriedExit)).toBe(true); expect(capturePage).toHaveBeenCalledTimes(3); // A persistent failure still surfaces once the retries are spent. capturePage.mockClear(); const captureCause = new Error("capture failed"); capturePage.mockRejectedValue(captureCause); const failingFiber = yield* Effect.exit(manager.captureScreenshot("tab_1")).pipe( Effect.forkChild({ startImmediately: true }), ); yield* TestClock.adjust(1_000); const exit = yield* Fiber.join(failingFiber); expect(Exit.isFailure(exit)).toBe(true); expect(capturePage).toHaveBeenCalledTimes(3); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewOperationError", operation: "captureScreenshot.capturePage", tabId: "tab_1", webContentsId: 42, cause: captureCause, }); }), ), ); effectIt.effect("keeps every recorded guest unthrottled until its frame capture stops", () => withManager((manager) => Effect.gen(function* () { const setBackgroundThrottling = vi.fn(); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const host = makeTestHostWebContents(); const firstWebContents = makeTestPreviewWebContents(capturePage, 41, host); const secondWebContents = makeTestPreviewWebContents(capturePage, 42, host); const webContentsById = new Map([ [41, firstWebContents], [42, secondWebContents], ]); fromId.mockImplementation((id) => id === undefined ? null : (webContentsById.get(id) ?? null), ); yield* manager.createTab("tab_capture_throttling_1"); yield* manager.createTab("tab_capture_throttling_2"); yield* manager.registerWebview("tab_capture_throttling_1", 41); yield* manager.registerWebview("tab_capture_throttling_2", 42); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling }, } as never); yield* manager.startRecording("tab_capture_throttling_1"); // The first renderer takes its grant, freeing the arm slot for the second tab. host.displayMediaHandler()?.({ frame: host.mainFrame }, () => {}); yield* manager.startRecording("tab_capture_throttling_2"); expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); expect(firstWebContents.setBackgroundThrottling.mock.calls).toEqual([[false]]); expect(secondWebContents.setBackgroundThrottling.mock.calls).toEqual([[false]]); yield* manager.stopRecording("tab_capture_throttling_1"); expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); expect(firstWebContents.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]); expect(secondWebContents.setBackgroundThrottling.mock.calls).toEqual([[false]]); yield* manager.stopRecording("tab_capture_throttling_2"); expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true]]); expect(secondWebContents.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]); }), ), ); effectIt.effect("does not commit failed starts and retries throttle restoration", () => withManager((manager) => Effect.gen(function* () { const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>(); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); yield* manager.createTab("tab_capture_throttling_failure"); yield* manager.registerWebview("tab_capture_throttling_failure", 42); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling }, } as never); setBackgroundThrottling.mockImplementationOnce(() => { throw new Error("start throttling update failed"); }); const failedStart = yield* Effect.exit( manager.startRecording("tab_capture_throttling_failure"), ); expect(Exit.isFailure(failedStart)).toBe(true); yield* manager.startRecording("tab_capture_throttling_failure"); expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false]]); setBackgroundThrottling.mockImplementationOnce(() => { throw new Error("stop throttling update failed"); }); yield* manager.stopRecording("tab_capture_throttling_failure"); expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false], [true], [true]]); yield* manager.startRecording("tab_capture_throttling_failure"); yield* manager.stopRecording("tab_capture_throttling_failure"); expect(setBackgroundThrottling.mock.calls).toEqual([ [false], [false], [true], [true], [false], [true], ]); }), ), ); effectIt.effect("rolls back window throttling when a recorded guest cannot be unthrottled", () => withManager((manager) => Effect.gen(function* () { const setWindowBackgroundThrottling = vi.fn(); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const wc = makeTestPreviewWebContents(capturePage); fromId.mockReturnValue(wc); yield* manager.createTab("tab_guest_throttling_failure"); yield* manager.registerWebview("tab_guest_throttling_failure", 42); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling: setWindowBackgroundThrottling }, } as never); wc.setBackgroundThrottling.mockImplementationOnce(() => { throw new Error("guest throttling update failed"); }); const failedStart = yield* Effect.exit( manager.startRecording("tab_guest_throttling_failure"), ); expect(Exit.isFailure(failedStart)).toBe(true); expect(setWindowBackgroundThrottling.mock.calls).toEqual([[false], [true]]); expect(wc.setBackgroundThrottling.mock.calls).toEqual([[false]]); yield* manager.startRecording("tab_guest_throttling_failure"); yield* manager.stopRecording("tab_guest_throttling_failure"); expect(setWindowBackgroundThrottling.mock.calls).toEqual([ [false], [true], [false], [true], ]); expect(wc.setBackgroundThrottling.mock.calls).toEqual([[false], [false], [true]]); }), ), ); effectIt.effect("does not publish a replacement window when capture reconciliation fails", () => withManager((manager) => Effect.gen(function* () { const setBackgroundThrottling = vi.fn(() => { throw new Error("replacement throttling update failed"); }); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); yield* manager.createTab("tab_capture_replacement_failure"); yield* manager.registerWebview("tab_capture_replacement_failure", 42); yield* manager.startRecording("tab_capture_replacement_failure"); const failedReplacement = yield* Effect.exit( manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling }, } as never), ); expect(Exit.isFailure(failedReplacement)).toBe(true); yield* manager.stopRecording("tab_capture_replacement_failure"); expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); }), ), ); effectIt.effect("ignores close events from replaced main windows", () => withManager((manager) => Effect.gen(function* () { let closeFirstWindow: (() => void) | undefined; const firstWindowThrottling = vi.fn(); const replacementWindowThrottling = vi.fn(); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); yield* manager.createTab("tab_replaced_window_close"); yield* manager.registerWebview("tab_replaced_window_close", 42); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn((event: string, listener: () => void) => { if (event === "closed") closeFirstWindow = listener; }), webContents: { setBackgroundThrottling: firstWindowThrottling }, } as never); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling: replacementWindowThrottling }, } as never); closeFirstWindow?.(); yield* manager.startRecording("tab_replaced_window_close"); expect(firstWindowThrottling).not.toHaveBeenCalled(); expect(replacementWindowThrottling.mock.calls).toEqual([[false]]); yield* manager.stopRecording("tab_replaced_window_close"); expect(replacementWindowThrottling.mock.calls).toEqual([[false], [true]]); }), ), ); effectIt.effect("releases frame capture when the main window closes", () => withManager((manager) => Effect.gen(function* () { let closeMainWindow: (() => void) | undefined; const firstWindowThrottling = vi.fn(); const replacementWindowThrottling = vi.fn(); const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const host = makeTestHostWebContents(); const webContentsById = new Map([ [42, makeTestPreviewWebContents(capturePage, 42, host)], [43, makeTestPreviewWebContents(capturePage, 43, host)], ]); fromId.mockImplementation((id) => id === undefined ? null : (webContentsById.get(id) ?? null), ); yield* manager.createTab("tab_window_close_recording"); yield* manager.createTab("tab_window_close_race"); yield* manager.registerWebview("tab_window_close_recording", 42); yield* manager.registerWebview("tab_window_close_race", 43); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn((event: string, listener: () => void) => { if (event === "closed") closeMainWindow = listener; }), webContents: { setBackgroundThrottling: firstWindowThrottling }, } as never); yield* manager.startRecording("tab_window_close_recording"); expect(firstWindowThrottling.mock.calls).toEqual([[false]]); closeMainWindow?.(); const racedStart = yield* Effect.exit(manager.startRecording("tab_window_close_race")); expect(Exit.isFailure(racedStart)).toBe(true); if (Exit.isFailure(racedStart)) { expect(Option.getOrThrow(Cause.findErrorOption(racedStart.cause))).toMatchObject({ _tag: "PreviewMainWindowClosedError", tabId: "tab_window_close_race", }); } yield* Effect.yieldNow; yield* Effect.yieldNow; const grants: Array<{ video?: unknown }> = []; host.displayMediaHandler()?.({ frame: host.mainFrame }, (value) => grants.push(value)); expect(grants).toEqual([{}]); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: { setBackgroundThrottling: replacementWindowThrottling }, } as never); expect(replacementWindowThrottling).not.toHaveBeenCalled(); }), ), ); effectIt.effect("does not arm recording after the main window closes during warmup", () => withManager((manager) => Effect.gen(function* () { let closeMainWindow: (() => void) | undefined; let finishWarmup!: (image: TestCapturedPreviewImage) => void; let markWarmupStarted!: () => void; const warmupStarted = new Promise((resolve) => { markWarmupStarted = resolve; }); const capturedImage = { toJPEG: () => Buffer.from("recording-frame"), getSize: () => ({ width: 1280, height: 720 }), }; const capturePage = vi.fn( () => new Promise((resolve) => { markWarmupStarted(); finishWarmup = resolve; }), ); const host = makeTestHostWebContents(); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage, 42, host)); yield* manager.createTab("tab_window_close_warmup"); yield* manager.registerWebview("tab_window_close_warmup", 42); yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn((event: string, listener: () => void) => { if (event === "closed") closeMainWindow = listener; }), webContents: { setBackgroundThrottling: vi.fn() }, } as never); const start = yield* manager .startRecording("tab_window_close_warmup") .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.promise(() => warmupStarted); closeMainWindow?.(); finishWarmup(capturedImage); const exit = yield* Fiber.await(start); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isFailure(exit)) { expect(Option.getOrThrow(Cause.findErrorOption(exit.cause))).toMatchObject({ _tag: "PreviewMainWindowClosedError", tabId: "tab_window_close_warmup", }); } expect(host.session.setDisplayMediaRequestHandler).not.toHaveBeenCalled(); }), ), ); effectIt.effect("stops capture retries when the tab swaps during the retry delay", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toPNG: () => Buffer.from("png"), toJPEG: () => Buffer.from("jpeg"), getSize: () => ({ width: 100, height: 80 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage, 42)); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); capturePage.mockRejectedValueOnce(new Error("UnknownVizError")); const fiber = yield* Effect.exit(manager.captureScreenshot("tab_1")).pipe( Effect.forkChild({ startImmediately: true }), ); // Let the rejection schedule its retry before replacing the guest. yield* TestClock.adjust(60); expect(capturePage).toHaveBeenCalledTimes(1); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage, 43)); yield* manager.registerWebview("tab_1", 43); yield* TestClock.adjust(1_000); const exit = yield* Fiber.join(fiber); expect(Exit.isFailure(exit)).toBe(true); expect(capturePage).toHaveBeenCalledTimes(1); expect(writeFile).not.toHaveBeenCalled(); }), ), ); effectIt.effect("discards a screenshot that resolves after its guest is replaced", () => withManager((manager) => Effect.gen(function* () { const image = { toPNG: () => Buffer.from("stale-png"), toJPEG: () => Buffer.from("stale-jpeg"), getSize: () => ({ width: 100, height: 80 }), }; const pending = Promise.withResolvers(); const capturePage = vi.fn(() => pending.promise); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage, 42)); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const fiber = yield* Effect.exit(manager.captureScreenshot("tab_1")).pipe( Effect.forkChild({ startImmediately: true }), ); yield* TestClock.adjust(0); expect(capturePage).toHaveBeenCalledOnce(); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage, 43)); yield* manager.registerWebview("tab_1", 43); pending.resolve(image); const exit = yield* Fiber.join(fiber); expect(Exit.isFailure(exit)).toBe(true); expect(capturePage).toHaveBeenCalledOnce(); expect(writeFile).not.toHaveBeenCalled(); }), ), ); effectIt.effect("releases snapshot control when every capture attempt stalls", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(() => new Promise(() => {})); const wc = makeTestPreviewWebContents(capturePage); Object.assign(wc, { isDevToolsOpened: () => false }); Object.assign(wc.debugger, { sendCommand: vi.fn(async (method: string, params?: Record) => { if (method === "Runtime.evaluate") { return { result: { value: params?.["expression"] === "42" ? 42 : { url: "https://example.com", title: "Example", loading: false, visibleText: "Example", interactiveElements: [], }, }, }; } return method === "Accessibility.getFullAXTree" ? { nodes: [] } : undefined; }), }); fromId.mockReturnValue(wc); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const snapshot = yield* Effect.exit(manager.automationSnapshot("tab_1")).pipe( Effect.forkChild({ startImmediately: true }), ); yield* TestClock.adjust(100); expect(capturePage).toHaveBeenCalledOnce(); const evaluate = yield* manager .automationEvaluate("tab_1", { expression: "42" }) .pipe(Effect.forkChild({ startImmediately: true })); expect(evaluate.pollUnsafe()).toBeUndefined(); yield* TestClock.adjust(4_000); const exit = yield* Fiber.join(snapshot); expect(Exit.isFailure(exit)).toBe(true); expect(capturePage).toHaveBeenCalledTimes(3); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewOperationError", operation: "automationSnapshot.capturePage", tabId: "tab_1", webContentsId: 42, cause: { _tag: "TimeoutError" }, }); expect(yield* Fiber.join(evaluate)).toBe(42); }), ), ); effectIt.effect("grants each concurrent preview recording its own tab frame", () => withManager((manager) => Effect.gen(function* () { const firstJpeg = Buffer.from("first-recording-frame"); const secondJpeg = Buffer.from("second-recording-frame"); const firstCapturePage = vi.fn(async () => ({ toJPEG: () => firstJpeg, getSize: () => ({ width: 800, height: 600 }), })); const secondCapturePage = vi.fn(async () => ({ toJPEG: () => secondJpeg, getSize: () => ({ width: 390, height: 844 }), })); const firstSendCommand = vi.fn(async () => undefined); const secondSendCommand = vi.fn(async () => undefined); // Both webviews live in the same window, so they share one display-media handler. const host = makeTestHostWebContents(); const makeWebContents = ( id: number, capturePage: typeof firstCapturePage, sendCommand: typeof firstSendCommand, ) => ({ id, mainFrame: { routingId: id }, hostWebContents: host, executeJavaScript: vi.fn(async () => id === 41 ? { width: 800, height: 600 } : { width: 390, height: 844 }, ), isDestroyed: () => false, getType: () => "webview", getURL: () => `https://example.com/${id}`, getTitle: () => `Example ${id}`, isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), setBackgroundThrottling: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, capturePage, }) as never; const webContentsById = new Map([ [41, makeWebContents(41, firstCapturePage, firstSendCommand)], [42, makeWebContents(42, secondCapturePage, secondSendCommand)], ]); fromId.mockImplementation((id) => id === undefined ? null : (webContentsById.get(id) ?? null), ); yield* manager.createTab("tab_1"); yield* manager.createTab("tab_2"); yield* manager.registerWebview("tab_1", 41); yield* manager.registerWebview("tab_2", 42); const grants: Array<{ video?: unknown }> = []; const takeGrant = () => host.displayMediaHandler()?.({ frame: host.mainFrame }, (value) => { grants.push(value); }); yield* manager.startRecording("tab_1"); takeGrant(); yield* manager.startRecording("tab_2"); takeGrant(); expect(grants).toEqual([{ video: { routingId: 41 } }, { video: { routingId: 42 } }]); expect(firstCapturePage).toHaveBeenCalledOnce(); expect(secondCapturePage).toHaveBeenCalledOnce(); expect(firstSendCommand).not.toHaveBeenCalledWith( "Page.startScreencast", expect.anything(), ); expect(secondSendCommand).not.toHaveBeenCalledWith( "Page.startScreencast", expect.anything(), ); yield* Effect.all([manager.stopRecording("tab_1"), manager.stopRecording("tab_2")], { concurrency: 2, discard: true, }); }), ), ); effectIt.effect("requests display media with a fresh renderer gesture", () => withManager((manager) => Effect.gen(function* () { const { host, takeGrant } = yield* setupRecordingRaceTabs(manager); yield* manager.startRecording("tab_race_a"); expect(host.executeJavaScript).toHaveBeenCalledWith( expect.stringContaining(DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER), true, ); expect(host.executeJavaScript).toHaveBeenCalledWith( expect.stringContaining("tab_race_a"), true, ); takeGrant(); yield* manager.stopRecording("tab_race_a"); }), ), ); // Runs on the real clock: an earlier queueing design only settled under TestClock and stalled the // losing start forever in the desktop app. effectIt.live("settles both starts when two tabs race for the capture stream", () => withManager((manager) => Effect.gen(function* () { const { host, grants, takeGrant } = yield* setupRecordingRaceTabs(manager); const exits = yield* Effect.all( [ Effect.exit(manager.startRecording("tab_race_a")), Effect.exit(manager.startRecording("tab_race_b")), ], { concurrency: 2 }, ); const [exitA, exitB] = exits; // Exactly one start owns the stream; the other fails fast instead of hanging. expect(exits.filter(Exit.isSuccess)).toHaveLength(1); const loserExit = Exit.isSuccess(exitA) ? exitB : exitA; if (Exit.isSuccess(loserExit)) return; expect(Option.getOrThrow(Cause.findErrorOption(loserExit.cause))).toMatchObject({ _tag: "PreviewRecordingArmConflictError", }); // The single grant goes to the tab that actually won the slot, never the other one. takeGrant(); expect(grants).toEqual([{ video: { routingId: Exit.isSuccess(exitA) ? 41 : 42 } }]); expect(host.session.setDisplayMediaRequestHandler).toHaveBeenCalledOnce(); }), ), ); effectIt.effect("releases an armed slot that the renderer never redeemed", () => withManager((manager) => Effect.gen(function* () { const { grants, takeGrant } = yield* setupRecordingRaceTabs(manager); yield* manager.startRecording("tab_race_a"); const blocked = yield* Effect.exit(manager.startRecording("tab_race_b")); if (Exit.isSuccess(blocked)) throw new Error("expected the second tab to be refused"); expect(Option.getOrThrow(Cause.findErrorOption(blocked.cause))).toMatchObject({ _tag: "PreviewRecordingArmConflictError", tabId: "tab_race_b", armedTabId: "tab_race_a", }); // Nothing ever captured the armed tab, so the slot goes stale and stops blocking starts. yield* TestClock.adjust(10_000); yield* manager.startRecording("tab_race_b"); takeGrant(); expect(grants).toEqual([{ video: { routingId: 42 } }]); yield* manager.stopRecording("tab_race_a"); yield* manager.stopRecording("tab_race_b"); }), ), ); effectIt.effect("denies a display-media request that arrives after the arm went stale", () => withManager((manager) => Effect.gen(function* () { const { grants, takeGrant } = yield* setupRecordingRaceTabs(manager); yield* manager.startRecording("tab_race_a"); yield* TestClock.adjust(10_000); // The handler cannot read a clock, so the expiry fiber must have dropped the frame. takeGrant(); expect(grants).toEqual([{}]); yield* manager.stopRecording("tab_race_a"); }), ), ); effectIt.effect("only lets the host frame that armed a recording claim its stream", () => withManager((manager) => Effect.gen(function* () { const { grants, takeGrant } = yield* setupRecordingRaceTabs(manager); yield* manager.startRecording("tab_race_a"); takeGrant({ frameTreeNodeId: 999 }); takeGrant(); expect(grants).toEqual([{}, { video: { routingId: 41 } }]); yield* manager.stopRecording("tab_race_a"); }), ), ); effectIt.effect("reclaims the arm slot from a destroyed webContents", () => withManager((manager) => Effect.gen(function* () { const { grants, takeGrant, destroy } = yield* setupRecordingRaceTabs(manager); yield* manager.startRecording("tab_race_a"); destroy(41); yield* manager.startRecording("tab_race_b"); takeGrant(); expect(grants).toEqual([{ video: { routingId: 42 } }]); yield* manager.stopRecording("tab_race_b"); }), ), ); effectIt.effect("continues native recording when the source warmup fails", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => { throw new Error("source is not ready"); }); const host = makeTestHostWebContents(); const webContents = Object.assign(makeTestPreviewWebContents(capturePage, 42, host), { executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), }); fromId.mockReturnValue(webContents); yield* manager.createTab("tab_recording_warmup_failure"); yield* manager.registerWebview("tab_recording_warmup_failure", 42); yield* manager.startRecording("tab_recording_warmup_failure"); expect(capturePage).toHaveBeenCalledTimes(2); // The armed tab answers exactly one display-media request, then further requests are denied. const handler = host.displayMediaHandler(); const streams: Array<{ video?: unknown }> = []; handler?.({ frame: host.mainFrame }, (value) => streams.push(value)); handler?.({ frame: host.mainFrame }, (value) => streams.push(value)); expect(streams).toEqual([{ video: { routingId: 42 } }, {}]); yield* manager.stopRecording("tab_recording_warmup_failure"); }), ), ); effectIt.effect("serializes recording source acquisition with webview replacement", () => withManager((manager) => Effect.gen(function* () { const capturedImage = { toJPEG: () => Buffer.from("unused-recording-frame"), getSize: () => ({ width: 1280, height: 720 }), }; let markWarmupStarted!: () => void; const warmupStarted = new Promise((resolve) => { markWarmupStarted = resolve; }); let finishWarmup!: (image: TestCapturedPreviewImage) => void; const capturePage = vi .fn<() => Promise>() .mockImplementationOnce( () => new Promise((resolve) => { markWarmupStarted(); finishWarmup = resolve; }), ) .mockResolvedValue(capturedImage); const initialWebContents = makeTestPreviewWebContents(capturePage, 42); const replacementOn = vi.fn(); const replacementWebContents = Object.assign(makeTestPreviewWebContents(capturePage, 43), { on: replacementOn, }); fromId.mockImplementation((id) => { if (id === 42) return initialWebContents; if (id === 43) return replacementWebContents; return null; }); yield* manager.createTab("tab_recording_replacement_race"); yield* manager.registerWebview("tab_recording_replacement_race", 42); const start = yield* manager .startRecording("tab_recording_replacement_race") .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.promise(() => warmupStarted); const replacement = yield* manager .registerWebview("tab_recording_replacement_race", 43) .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.yieldNow; expect(replacementOn).not.toHaveBeenCalled(); finishWarmup(capturedImage); yield* Fiber.join(start); yield* Fiber.join(replacement); expect(replacementOn).toHaveBeenCalled(); expect(initialWebContents.setBackgroundThrottling.mock.calls).toEqual([[false]]); expect(replacementWebContents.setBackgroundThrottling.mock.calls).toEqual([[false]]); yield* manager.stopRecording("tab_recording_replacement_race"); expect(initialWebContents.setBackgroundThrottling.mock.calls).toEqual([[false], [true]]); expect(replacementWebContents.setBackgroundThrottling.mock.calls).toEqual([ [false], [true], ]); }), ), ); effectIt.effect("shares background frame capture between recording and picture-in-picture", () => withManager((manager) => Effect.gen(function* () { const setBackgroundThrottling = vi.fn(); const mainWindowWebContents = Object.assign(makeTestHostWebContents(), { setBackgroundThrottling, }); const jpeg = Buffer.from("shared-preview-frame"); const capturePage = vi.fn(async () => ({ toJPEG: () => jpeg, getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue({ id: 42, mainFrame: { routingId: 42 }, hostWebContents: mainWindowWebContents, executeJavaScript: vi.fn(async () => ({ width: 1280, height: 720 })), isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), setBackgroundThrottling: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, capturePage, } as never); const pictureInPictureListeners = new Map void>(); const pictureInPictureSend = vi.fn(); const pictureInPictureWindow = { isDestroyed: vi.fn(() => false), once: vi.fn((event: string, listener: () => void) => { pictureInPictureListeners.set(event, listener); }), setAlwaysOnTop: vi.fn(), setVisibleOnAllWorkspaces: vi.fn(), setAspectRatio: vi.fn(), getContentSize: vi.fn(() => [480, 320] as [number, number]), setContentSize: vi.fn(), loadURL: vi.fn(async () => undefined), showInactive: vi.fn(), close: vi.fn(() => { pictureInPictureListeners.get("closed")?.(); }), webContents: { on: vi.fn(), off: vi.fn(), send: pictureInPictureSend, }, }; browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); const states: PreviewManager.PreviewTabState[] = []; const recordingFrames: DesktopPreviewRecordingFrame[] = []; yield* manager.setMainWindow({ isDestroyed: () => false, once: vi.fn(), webContents: mainWindowWebContents, } as never); yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.subscribeRecordingFrames((frame) => Effect.sync(() => { recordingFrames.push(frame); }), ); yield* manager.createTab("tab_pip"); yield* manager.registerWebview("tab_pip", 42); yield* manager.openPictureInPicture("tab_pip"); expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); expect(browserWindowConstructor).toHaveBeenCalledWith( expect.objectContaining({ alwaysOnTop: true, show: false, skipTaskbar: true, webPreferences: expect.objectContaining({ preload: "/tmp/t3/desktop/preview-pip-preload.cjs", backgroundThrottling: false, }), }), ); expect(pictureInPictureWindow.showInactive).toHaveBeenCalledOnce(); expect(pictureInPictureWindow.setVisibleOnAllWorkspaces).toHaveBeenCalledWith(true, { visibleOnFullScreen: true, skipTransformProcessType: true, }); expect(pictureInPictureWindow.setAspectRatio.mock.calls).toEqual([[0], [1280 / 720]]); expect(pictureInPictureWindow.setContentSize).toHaveBeenCalledWith(523, 294, false); expect(pictureInPictureWindow.setAspectRatio.mock.invocationCallOrder[0]).toBeLessThan( pictureInPictureWindow.setContentSize.mock.invocationCallOrder[0] ?? 0, ); expect(pictureInPictureWindow.setContentSize.mock.invocationCallOrder[0]).toBeLessThan( pictureInPictureWindow.setAspectRatio.mock.invocationCallOrder[1] ?? 0, ); expect(pictureInPictureSend).toHaveBeenCalledWith( "desktop:preview-pip-frame", expect.objectContaining({ tabId: "tab_pip", data: jpeg.toString("base64"), width: 1280, height: 720, }), ); expect(states.at(-1)?.pictureInPicture).toBe(true); expect(capturePage).toHaveBeenCalledOnce(); const pictureInPictureFramesBeforeRecording = pictureInPictureSend.mock.calls.length; yield* manager.startRecording("tab_pip"); expect(capturePage).toHaveBeenCalledTimes(2); expect(recordingFrames).toHaveLength(0); yield* TestClock.adjust(100); expect(capturePage).toHaveBeenCalledTimes(3); expect(pictureInPictureSend).toHaveBeenCalledTimes(pictureInPictureFramesBeforeRecording); expect(recordingFrames).toHaveLength(0); yield* manager.stopRecording("tab_pip"); expect(setBackgroundThrottling.mock.calls).toEqual([[false]]); const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length; yield* TestClock.adjust(100); expect(capturePage).toHaveBeenCalledTimes(4); expect(pictureInPictureSend.mock.calls.length).toBe(framesBeforePictureInPictureOnlyTick); expect(recordingFrames).toHaveLength(0); setBackgroundThrottling.mockImplementationOnce(() => { throw new Error("picture-in-picture throttling restore failed"); }); yield* manager.closePictureInPicture("tab_pip"); expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true], [true]]); expect(pictureInPictureWindow.close).toHaveBeenCalledOnce(); expect(states.at(-1)?.pictureInPicture).toBe(false); const capturesAfterClose = capturePage.mock.calls.length; yield* TestClock.adjust(200); expect(capturePage).toHaveBeenCalledTimes(capturesAfterClose); }), ), ); effectIt.effect("keeps picture-in-picture capture separate from recording warmup", () => withManager((manager) => Effect.gen(function* () { const jpeg = Buffer.from("shared-preview-frame"); const capturePage = vi.fn(async () => ({ toJPEG: () => jpeg, getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow, send } = makeTestPictureInPictureWindow(); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); const recordingFrames: DesktopPreviewRecordingFrame[] = []; yield* manager.subscribeRecordingFrames((frame) => Effect.sync(() => { recordingFrames.push(frame); }), ); yield* manager.createTab("tab_recording_then_pip"); yield* manager.registerWebview("tab_recording_then_pip", 42); yield* manager.startRecording("tab_recording_then_pip"); expect(recordingFrames).toHaveLength(0); expect(capturePage).toHaveBeenCalledOnce(); yield* manager.openPictureInPicture("tab_recording_then_pip"); expect(capturePage).toHaveBeenCalledTimes(2); expect(send).toHaveBeenCalledOnce(); yield* TestClock.adjust(100); expect(capturePage).toHaveBeenCalledTimes(3); expect(recordingFrames).toHaveLength(0); expect(send).toHaveBeenCalledOnce(); yield* manager.closePictureInPicture("tab_recording_then_pip"); yield* manager.stopRecording("tab_recording_then_pip"); }), ), ); effectIt.effect("stops frame capture when the native picture-in-picture window closes", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("native-close-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow } = makeTestPictureInPictureWindow(); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_native_pip_close"); yield* manager.registerWebview("tab_native_pip_close", 42); yield* manager.openPictureInPicture("tab_native_pip_close"); pictureInPictureWindow.close(); yield* settle(() => states.at(-1)?.pictureInPicture === false); expect(states.at(-1)?.pictureInPicture).toBe(false); const capturesAfterClose = capturePage.mock.calls.length; yield* TestClock.adjust(200); expect(capturePage).toHaveBeenCalledTimes(capturesAfterClose); }), ), ); effectIt.effect("retries an unchanged picture-in-picture frame after delivery fails", () => withManager((manager) => Effect.gen(function* () { const jpeg = Buffer.from("retry-preview-frame"); const capturePage = vi.fn(async () => ({ toJPEG: () => jpeg, getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow, send } = makeTestPictureInPictureWindow(); send.mockImplementationOnce(() => { throw new Error("picture-in-picture delivery failed"); }); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); yield* manager.createTab("tab_pip_delivery_retry"); yield* manager.registerWebview("tab_pip_delivery_retry", 42); yield* manager.openPictureInPicture("tab_pip_delivery_retry"); expect(send).toHaveBeenCalledOnce(); yield* TestClock.adjust(100); expect(capturePage).toHaveBeenCalledTimes(2); expect(send).toHaveBeenCalledTimes(2); yield* manager.closePictureInPicture("tab_pip_delivery_retry"); }), ), ); effectIt.effect("replays an unchanged picture-in-picture frame after its renderer reloads", () => withManager((manager) => Effect.gen(function* () { const jpeg = Buffer.from("reloaded-preview-frame"); const capturePage = vi.fn(async () => ({ toJPEG: () => jpeg, getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow, send, webContentsListeners } = makeTestPictureInPictureWindow(); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); yield* manager.createTab("tab_pip_reload"); yield* manager.registerWebview("tab_pip_reload", 42); yield* manager.openPictureInPicture("tab_pip_reload"); expect(send).toHaveBeenCalledOnce(); webContentsListeners.get("did-finish-load")?.(); yield* TestClock.adjust(100); expect(send).toHaveBeenCalledTimes(2); yield* manager.closePictureInPicture("tab_pip_reload"); }), ), ); effectIt.effect("drops empty frames before picture-in-picture delivery", () => withManager((manager) => Effect.gen(function* () { const validImage: TestCapturedPreviewImage = { toJPEG: () => Buffer.from("valid-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), }; const capturePage = vi.fn(async () => validImage); capturePage.mockResolvedValueOnce({ toJPEG: () => Buffer.from("empty-preview-frame"), getSize: () => ({ width: 0, height: 0 }), }); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow, send } = makeTestPictureInPictureWindow(); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); yield* manager.createTab("tab_empty_frame"); yield* manager.registerWebview("tab_empty_frame", 42); yield* manager.openPictureInPicture("tab_empty_frame"); expect(capturePage).toHaveBeenCalledOnce(); expect(pictureInPictureWindow.setAspectRatio).not.toHaveBeenCalled(); expect(send).not.toHaveBeenCalled(); yield* TestClock.adjust(100); expect(pictureInPictureWindow.setAspectRatio.mock.calls).toEqual([[0], [1280 / 720]]); expect(send).toHaveBeenCalledOnce(); yield* manager.closePictureInPicture("tab_empty_frame"); }), ), ); effectIt.effect("does not publish picture-in-picture readiness after window teardown", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("closing-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow } = makeTestPictureInPictureWindow(); pictureInPictureWindow.showInactive.mockImplementationOnce(() => { pictureInPictureWindow.close(); }); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_pip_teardown"); yield* manager.registerWebview("tab_pip_teardown", 42); const openExit = yield* Effect.exit(manager.openPictureInPicture("tab_pip_teardown")); expect(Exit.hasInterrupts(openExit)).toBe(true); expect(pictureInPictureWindow.close).toHaveBeenCalledOnce(); expect(states.some((state) => state.pictureInPicture)).toBe(false); expect(states.at(-1)?.pictureInPicture).toBe(false); }), ), ); effectIt.effect("closes an initializing picture-in-picture without blocking later opens", () => withManager((manager) => Effect.gen(function* () { const capturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("serialized-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), })); fromId.mockReturnValue(makeTestPreviewWebContents(capturePage)); const { pictureInPictureWindow: initializingWindow } = makeTestPictureInPictureWindow( () => new Promise(() => { // Simulate a renderer load that never settles. }), ); const { pictureInPictureWindow: reopenedWindow } = makeTestPictureInPictureWindow(); browserWindowConstructor .mockImplementationOnce(function () { return initializingWindow; }) .mockImplementationOnce(function () { return reopenedWindow; }); const states: PreviewManager.PreviewTabState[] = []; yield* manager.subscribeStateChanges((_tabId, state) => Effect.sync(() => { states.push(state); }), ); yield* manager.createTab("tab_concurrent_pip"); yield* manager.registerWebview("tab_concurrent_pip", 42); const firstOpen = yield* manager .openPictureInPicture("tab_concurrent_pip") .pipe(Effect.forkChild); yield* Effect.yieldNow; const secondOpen = yield* manager .openPictureInPicture("tab_concurrent_pip") .pipe(Effect.forkChild); yield* Effect.yieldNow; const close = yield* manager .closePictureInPicture("tab_concurrent_pip") .pipe(Effect.forkChild); yield* Effect.yieldNow; expect(browserWindowConstructor).toHaveBeenCalledOnce(); expect(initializingWindow.loadURL).toHaveBeenCalledOnce(); expect(initializingWindow.close).toHaveBeenCalledOnce(); const [firstOpenExit, secondOpenExit] = yield* Effect.all([ Fiber.await(firstOpen), Fiber.await(secondOpen), ]); yield* Fiber.join(close); expect(Exit.hasInterrupts(firstOpenExit)).toBe(true); expect(Exit.hasInterrupts(secondOpenExit)).toBe(true); expect(initializingWindow.showInactive).not.toHaveBeenCalled(); expect(capturePage).not.toHaveBeenCalled(); expect(states.at(-1)?.pictureInPicture).toBe(false); yield* manager.openPictureInPicture("tab_concurrent_pip"); expect(browserWindowConstructor).toHaveBeenCalledTimes(2); expect(reopenedWindow.showInactive).toHaveBeenCalledOnce(); expect(capturePage).toHaveBeenCalledOnce(); expect(states.at(-1)?.pictureInPicture).toBe(true); yield* manager.closePictureInPicture("tab_concurrent_pip"); expect(browserWindowConstructor).toHaveBeenCalledTimes(2); expect(reopenedWindow.close).toHaveBeenCalledOnce(); expect(states.at(-1)?.pictureInPicture).toBe(false); const capturesAfterClose = capturePage.mock.calls.length; yield* TestClock.adjust(200); expect(capturePage).toHaveBeenCalledTimes(capturesAfterClose); }), ), ); effectIt.effect("rejects picture-in-picture when its webview changes during initialization", () => withManager((manager) => Effect.gen(function* () { const initialCapturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("stale-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const replacementCapturePage = vi.fn(async () => ({ toJPEG: () => Buffer.from("replacement-preview-frame"), getSize: () => ({ width: 1280, height: 720 }), })); const initialWebContents = makeTestPreviewWebContents(initialCapturePage, 42); const replacementWebContents = makeTestPreviewWebContents(replacementCapturePage, 43); fromId.mockImplementation((webContentsId?: number) => { if (webContentsId === 42) return initialWebContents; if (webContentsId === 43) return replacementWebContents; return null; }); let resolveLoad: (() => void) | undefined; const { pictureInPictureWindow } = makeTestPictureInPictureWindow( () => new Promise((resolve) => { resolveLoad = resolve; }), ); browserWindowConstructor.mockImplementation(function () { return pictureInPictureWindow; }); yield* manager.createTab("tab_replaced_webview"); yield* manager.registerWebview("tab_replaced_webview", 42); const open = yield* manager .openPictureInPicture("tab_replaced_webview") .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.yieldNow; expect(pictureInPictureWindow.loadURL).toHaveBeenCalledOnce(); expect(resolveLoad).toBeDefined(); const concurrentOpen = yield* manager .openPictureInPicture("tab_replaced_webview") .pipe(Effect.forkChild({ startImmediately: true })); yield* Effect.yieldNow; yield* manager.registerWebview("tab_replaced_webview", 43); resolveLoad?.(); const openExits = yield* Effect.all([Fiber.await(open), Fiber.await(concurrentOpen)]); for (const openExit of openExits) { expect(Exit.isFailure(openExit)).toBe(true); if (Exit.isSuccess(openExit)) continue; const error = Option.getOrThrow(Cause.findErrorOption(openExit.cause)); expect(error).toMatchObject({ _tag: "PreviewOperationError", operation: "pictureInPicture.validateWebContents", tabId: "tab_replaced_webview", webContentsId: 42, }); } expect(browserWindowConstructor).toHaveBeenCalledOnce(); expect(pictureInPictureWindow.close).toHaveBeenCalledOnce(); expect(pictureInPictureWindow.showInactive).not.toHaveBeenCalled(); expect(initialCapturePage).not.toHaveBeenCalled(); expect(replacementCapturePage).not.toHaveBeenCalled(); }), ), ); effectIt.effect("keeps element picking active during subframe navigation", () => withManager((manager) => Effect.gen(function* () { const listeners = new Map void>(); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isFocused: () => true, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), once: vi.fn((event: string, listener: (...args: unknown[]) => void) => { listeners.set(event, listener); }), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn(), removeListener: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const pick = yield* manager.pickElement("tab_1").pipe(Effect.forkChild); yield* Effect.yieldNow; listeners.get("did-start-navigation")?.({}, "about:blank", false, false); yield* Effect.yieldNow; expect(pick.pollUnsafe()).toBeUndefined(); listeners.get("did-start-navigation")?.({}, "https://example.com/next", false, true); expect(yield* Fiber.join(pick)).toBeNull(); }), ), ); effectIt.effect("settles the pick when the annotation screenshot never arrives", () => withManager((manager) => Effect.gen(function* () { let onPicked: ((event: unknown, ...args: unknown[]) => void) | undefined; fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isFocused: () => true, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), once: vi.fn(), off: vi.fn(), // A wedged compositor leaves `capturePage` pending forever. capturePage: vi.fn(() => new Promise(() => {})), ipc: { on: vi.fn((channel: string, listener: typeof onPicked) => { if (channel === "preview:element-picked") onPicked = listener; }), off: vi.fn(), removeListener: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const pick = yield* manager.pickElement("tab_1").pipe(Effect.forkChild); yield* Effect.yieldNow; onPicked?.( {}, { id: "annotation_1", pageUrl: "https://example.com", pageTitle: "Example", comment: "Tighten this spacing", elements: [], regions: [{ id: "region_1", rect: { x: 5, y: 6, width: 20, height: 30 } }], strokes: [], styleChanges: [], screenshot: null, createdAt: "2026-06-11T00:00:00.000Z", }, null, "send", ); yield* Effect.yieldNow; expect(pick.pollUnsafe()).toBeUndefined(); yield* TestClock.adjust("6 seconds"); // The pick has to give up on the crop rather than strand the renderer, // which would leave the composer stuck on "Capturing…". const result = yield* Fiber.join(pick); expect(result?.annotation.screenshot).toBeNull(); expect(result?.screenshotFailed).toBe(true); expect(result?.submission).toBe("send"); expect(webviewSend).toHaveBeenCalledWith("preview:annotation-captured"); }), ), ); effectIt.effect("a stale capture from a replaced pick never touches the next pick", () => withManager((manager) => Effect.gen(function* () { let onPicked: ((event: unknown, ...args: unknown[]) => void) | undefined; fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isFocused: () => true, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), once: vi.fn(), off: vi.fn(), capturePage: vi.fn(() => new Promise(() => {})), ipc: { on: vi.fn((channel: string, listener: typeof onPicked) => { if (channel === "preview:element-picked") onPicked = listener; }), off: vi.fn(), removeListener: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); const annotation = { id: "annotation_1", pageUrl: "https://example.com", pageTitle: "Example", comment: "Tighten this spacing", elements: [], regions: [{ id: "region_1", rect: { x: 5, y: 6, width: 20, height: 30 } }], strokes: [], styleChanges: [], screenshot: null, createdAt: "2026-06-11T00:00:00.000Z", }; yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const firstPick = yield* manager.pickElement("tab_1").pipe(Effect.forkChild); yield* Effect.yieldNow; // The first pick submits and its crop hangs. onPicked?.({}, annotation, null, "send"); yield* Effect.yieldNow; // A second pick on the same tab replaces the first, which resumes null. const secondPick = yield* manager.pickElement("tab_1").pipe(Effect.forkChild); yield* Effect.yieldNow; expect(yield* Fiber.join(firstPick)).toBeNull(); webviewSend.mockClear(); // The first pick's crop times out while the second pick is live. It // must not signal the overlay, which would tear down the second pick. yield* TestClock.adjust("6 seconds"); yield* Effect.yieldNow; expect(webviewSend).not.toHaveBeenCalledWith("preview:annotation-captured"); expect(secondPick.pollUnsafe()).toBeUndefined(); onPicked?.({}, { ...annotation, id: "annotation_2" }, null, "attach"); yield* TestClock.adjust("6 seconds"); const result = yield* Fiber.join(secondPick); expect(result?.annotation.id).toBe("annotation_2"); expect(result?.submission).toBe("attach"); }), ), ); effectIt.effect("navigates the guest history when the thumb-button ipc fires", () => withManager((manager) => Effect.gen(function* () { let mouseNavigate: ((event: unknown, payload: unknown) => void) | undefined; const goBack = vi.fn(); const goForward = vi.fn(); let canGoBack = true; fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof mouseNavigate) => { if (channel === "preview:mouse-navigate") mouseNavigate = listener; }), off: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => canGoBack, canGoForward: () => true, goBack, goForward, }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand: vi.fn(async () => undefined), on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_nav"); yield* manager.registerWebview("tab_nav", 42); expect(mouseNavigate).toBeDefined(); mouseNavigate?.({}, { direction: "back" }); yield* Effect.yieldNow; expect(goBack).toHaveBeenCalledOnce(); mouseNavigate?.({}, { direction: "forward" }); yield* Effect.yieldNow; expect(goForward).toHaveBeenCalledOnce(); // Ignores unknown payloads and never navigates when history is exhausted. mouseNavigate?.({}, { direction: "sideways" }); canGoBack = false; mouseNavigate?.({}, { direction: "back" }); yield* Effect.yieldNow; expect(goBack).toHaveBeenCalledOnce(); }), ), ); effectIt.effect("reveals only files inside the configured browser artifact directory", () => withManager((manager) => Effect.gen(function* () { yield* manager.revealArtifact("/tmp/t3/dev/browser-artifacts/browser-screenshot-test.png"); expect(showItemInFolder).toHaveBeenCalledWith( "/tmp/t3/dev/browser-artifacts/browser-screenshot-test.png", ); const exit = yield* Effect.exit(manager.revealArtifact("/tmp/t3/dev/settings.json")); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewArtifactPathOutsideDirectoryError", artifactPath: "/tmp/t3/dev/settings.json", artifactDirectory: "/tmp/t3/dev/browser-artifacts", }); expect("cause" in error).toBe(false); }), ), ); effectIt.effect("copies screenshot artifacts to the system clipboard", () => withManager((manager) => Effect.gen(function* () { const artifactPath = "/tmp/t3/dev/browser-artifacts/browser-screenshot-test.png"; yield* manager.copyArtifactToClipboard(artifactPath); expect(createFromPath).toHaveBeenCalledWith(artifactPath); expect(clipboardItemConstructor).toHaveBeenCalledWith({ "image/png": expect.any(Blob), }); expect(writeClipboard).toHaveBeenCalledOnce(); const exit = yield* Effect.exit( manager.copyArtifactToClipboard("/tmp/t3/dev/settings.json"), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewArtifactPathOutsideDirectoryError", artifactPath: "/tmp/t3/dev/settings.json", artifactDirectory: "/tmp/t3/dev/browser-artifacts", }); expect("cause" in error).toBe(false); createFromPath.mockReturnValueOnce({ isEmpty: () => true, toPNG: () => Buffer.from("invalid"), }); const invalidImageExit = yield* Effect.exit(manager.copyArtifactToClipboard(artifactPath)); expect(Exit.isFailure(invalidImageExit)).toBe(true); if (Exit.isSuccess(invalidImageExit)) return; expect(Option.getOrThrow(Cause.findErrorOption(invalidImageExit.cause))).toMatchObject({ _tag: "PreviewArtifactImageLoadError", artifactPath, }); const writeCause = new Error("clipboard write failed"); writeClipboard.mockRejectedValueOnce(writeCause); const writeExit = yield* Effect.exit(manager.copyArtifactToClipboard(artifactPath)); expect(Exit.isFailure(writeExit)).toBe(true); if (Exit.isSuccess(writeExit)) return; expect(Option.getOrThrow(Cause.findErrorOption(writeExit.cause))).toMatchObject({ _tag: "PreviewOperationError", operation: "copyArtifactToClipboard.write", artifactPath, cause: writeCause, }); }), ), ); effectIt.effect("emits the resolved pointer target before dispatching an automation click", () => withManager((manager) => Effect.gen(function* () { let humanInput: ((_event: unknown, signal: unknown) => void) | undefined; const activity: string[] = []; const sendCommand = vi.fn(async (method: string, params?: Record) => { if (method === "Runtime.evaluate") { return { result: { value: { width: 800, height: 600 }, }, }; } if (method === "Input.dispatchMouseEvent" && params?.type === "mousePressed") { activity.push("mousePressed"); humanInput?.({}, { kind: "pointer", x: params.x, y: params.y, button: 0 }); } return undefined; }); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isDevToolsOpened: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.subscribePointerEvents((event) => Effect.sync(() => { activity.push(event.phase); }), ); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const click = yield* manager .automationClick("tab_1", { x: 120, y: 80 }) .pipe(Effect.forkChild({ startImmediately: true })); yield* TestClock.adjust(200); yield* Fiber.join(click); expect(activity).toEqual(["move", "click", "mousePressed"]); expect(sendCommand).toHaveBeenCalledWith("Input.dispatchMouseEvent", { type: "mousePressed", x: 120, y: 80, button: "left", clickCount: 1, }); expect(sendCommand).toHaveBeenCalledWith("Input.dispatchMouseEvent", { type: "mouseReleased", x: 120, y: 80, button: "left", clickCount: 1, }); }), ), ); effectIt.effect("types in background webviews and enables native key input", () => withManager((manager) => Effect.gen(function* () { let failKeyDown = false; let humanInput: ((_event: unknown, signal: unknown) => void) | undefined; const sendCommand = vi.fn(async (method: string, params?: Record) => { if ( failKeyDown && method === "Input.dispatchKeyEvent" && (params?.["type"] === "keyDown" || params?.["type"] === "rawKeyDown") ) { throw new Error("key dispatch failed"); } if ( method === "Input.dispatchKeyEvent" && (params?.["type"] === "keyDown" || params?.["type"] === "rawKeyDown") ) { humanInput?.( {}, { kind: "key", key: params["key"], code: params["code"] ?? "Digit1", }, ); } return method === "Runtime.evaluate" ? { result: { value: { ok: true } } } : undefined; }); const restoreFocus = vi.fn(); const focus = vi.fn(); getFocusedWebContents.mockReturnValue({ id: 7, isDestroyed: () => false, focus: restoreFocus, } as never); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isDevToolsOpened: () => false, focus, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_input"); yield* manager.registerWebview("tab_input", 42); yield* manager.automationType("tab_input", { text: "hello", clear: true }); yield* manager.automationType("tab_input", { text: "", clear: true }); yield* manager.automationPress("tab_input", { key: "x" }); const calls = sendCommand.mock.calls; const methods = calls.map(([method]) => method); const enableIndex = methods.indexOf("Input.setIgnoreInputEvents"); const focusOnIndex = calls.findIndex( ([method, params]) => method === "Emulation.setFocusEmulationEnabled" && params?.["enabled"] === true, ); const keyDownIndex = calls.findIndex( ([method, params]) => method === "Input.dispatchKeyEvent" && params?.["type"] === "keyDown", ); const keyUpIndex = calls.findIndex( ([method, params]) => method === "Input.dispatchKeyEvent" && params?.["type"] === "keyUp", ); const focusOffIndex = calls.findIndex( ([method, params]) => method === "Emulation.setFocusEmulationEnabled" && params?.["enabled"] === false, ); const typeEvaluation = sendCommand.mock.calls.find( ([method, params]) => method === "Runtime.evaluate" && typeof params === "object" && params !== null && "expression" in params && typeof params.expression === "string" && params.expression.includes('document.execCommand("insertText"'), ); expect(typeEvaluation).toBeDefined(); const clearOnlyEvaluation = sendCommand.mock.calls.find( ([method, params]) => method === "Runtime.evaluate" && typeof params === "object" && params !== null && "expression" in params && typeof params.expression === "string" && params.expression.includes('const text = ""') && params.expression.includes("Object.getOwnPropertyDescriptor"), ); expect(clearOnlyEvaluation).toBeDefined(); expect(methods).not.toContain("Input.insertText"); expect(enableIndex).toBeGreaterThanOrEqual(0); expect(focus).toHaveBeenCalledOnce(); expect(restoreFocus).toHaveBeenCalledOnce(); expect(methods).toContain("Page.bringToFront"); expect(enableIndex).toBeLessThan(focusOnIndex); expect(focusOnIndex).toBeLessThan(keyDownIndex); expect(keyDownIndex).toBeLessThan(keyUpIndex); expect(keyUpIndex).toBeLessThan(focusOffIndex); expect( calls.filter( ([method, params]) => method === "Input.dispatchKeyEvent" && params?.["type"] === "keyUp", ), ).toHaveLength(1); expect(sendCommand).toHaveBeenCalledWith("Input.setIgnoreInputEvents", { ignore: false }); sendCommand.mockClear(); failKeyDown = true; const failedPress = yield* Effect.exit(manager.automationPress("tab_input", { key: "y" })); expect(Exit.isFailure(failedPress)).toBe(true); expect(sendCommand).toHaveBeenCalledWith("Input.dispatchKeyEvent", { type: "keyUp", key: "y", code: "KeyY", modifiers: 0, windowsVirtualKeyCode: 89, location: 0, isKeypad: false, }); expect(sendCommand).toHaveBeenCalledWith("Emulation.setFocusEmulationEnabled", { enabled: false, }); expect(restoreFocus).toHaveBeenCalledTimes(2); expect( sendCommand.mock.calls.filter( ([method, params]) => method === "Input.dispatchKeyEvent" && params?.["type"] === "keyUp", ), ).toHaveLength(1); sendCommand.mockClear(); failKeyDown = false; yield* manager.automationPress("tab_input", { key: "!" }); expect(sendCommand).toHaveBeenCalledWith("Input.dispatchKeyEvent", { type: "keyDown", key: "!", code: "Digit1", modifiers: 0, windowsVirtualKeyCode: 49, location: 0, isKeypad: false, text: "!", unmodifiedText: "!", }); expect(restoreFocus).toHaveBeenCalledTimes(3); }), ), ); effectIt.effect("still interrupts agent control for a different human pointer event", () => withManager((manager) => Effect.gen(function* () { let humanInput: ((_event: unknown, signal: unknown) => void) | undefined; const sendCommand = vi.fn(async (method: string) => { if (method === "Runtime.evaluate") { return { result: { value: { width: 800, height: 600 }, }, }; } if (method === "Input.dispatchMouseEvent") { humanInput?.({}, { kind: "pointer", x: 400, y: 300, button: 0 }); } return undefined; }); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isDevToolsOpened: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn((channel: string, listener: typeof humanInput) => { if (channel === "preview:human-input") humanInput = listener; }), off: vi.fn(), }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const click = yield* manager .automationClick("tab_1", { x: 120, y: 80 }) .pipe(Effect.forkChild({ startImmediately: true })); yield* TestClock.adjust(200); const exit = yield* Fiber.await(click); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewAutomationControlInterruptedError", operation: "click", tabId: "tab_1", webContentsId: 42, }); expect(error).toBeInstanceOf(Error); if (error instanceof Error) { expect(error.name).toBe("PreviewAutomationControlInterruptedError"); } expect("cause" in error).toBe(false); }), ), ); effectIt.effect("derives evaluation detail kind and length from the same non-empty source", () => withManager((manager) => Effect.gen(function* () { const text = "ReferenceError: fallbackDetail is not defined"; const exceptionDetails = { text, exception: { description: "" }, }; const sendCommand = vi.fn(async (method: string) => method === "Runtime.evaluate" ? { exceptionDetails } : undefined, ); fromId.mockReturnValue({ id: 42, isDestroyed: () => false, getType: () => "webview", getURL: () => "https://example.com", getTitle: () => "Example", isLoading: () => false, isDevToolsOpened: () => false, getZoomFactor: () => 1, setZoomFactor: vi.fn(), setAudioMuted: vi.fn(), isCurrentlyAudible: () => false, on: vi.fn(), off: vi.fn(), ipc: { on: vi.fn(), off: vi.fn() }, send: webviewSend, navigationHistory: { canGoBack: () => false, canGoForward: () => false }, setIgnoreMenuShortcuts: vi.fn(), setWindowOpenHandler: vi.fn(), debugger: { isAttached: () => false, attach: vi.fn(), sendCommand, on: vi.fn(), off: vi.fn(), }, } as never); yield* manager.createTab("tab_1"); yield* manager.registerWebview("tab_1", 42); const exit = yield* Effect.exit( manager.automationEvaluate("tab_1", { expression: "fallbackDetail" }), ); expect(Exit.isFailure(exit)).toBe(true); if (Exit.isSuccess(exit)) return; const error = Option.getOrThrow(Cause.findErrorOption(exit.cause)); expect(error).toMatchObject({ _tag: "PreviewAutomationEvaluationError", detailKind: "exception-text", detailLength: text.length, cause: exceptionDetails, }); }), ), ); }); describe("PreviewOperationError", () => { it("keeps timeline detail separate from its structured message", () => { const cause = new Error("CDP command failed with an invalid node id"); const error = new PreviewManager.PreviewOperationError({ operation: "click.DOM.resolveNode", tabId: "tab_1", webContentsId: 42, cause, }); expect(error.message).not.toContain(cause.message); expect(PreviewManager.PreviewOperationError.toTimelineMessage(error)).toBe(cause.message); }); }); describe("Preview automation diagnostics", () => { it("keeps browser exception detail out of structural diagnostics", () => { const secret = "unrelated-browser-payload-secret"; const detail = "ReferenceError: missingValue is not defined"; const cause = { text: "Uncaught Error", exception: { description: detail }, unsafePayload: secret, }; const error = new PreviewManager.PreviewAutomationEvaluationError({ tabId: "tab_1", detailKind: "exception-description", detailLength: detail.length, cause, }); const encoded = encodePreviewManagerError(error); const { cause: encodedCause, ...encodedDiagnostics } = encoded as typeof encoded & { readonly cause?: unknown; }; expect(error.cause).toBe(cause); expect(encodedCause).toStrictEqual(cause); expect(error.message).toBe("Preview JavaScript evaluation failed in tab tab_1"); expect(error.message).not.toContain(secret); expect(JSON.stringify(encodedDiagnostics)).not.toContain(secret); expect("detail" in error).toBe(false); expect(PreviewManager.PreviewAutomationEvaluationError.toTimelineMessage(error)).toBe(detail); expect(PreviewManager.PreviewAutomationEvaluationError.toTimelineMessage(error)).not.toContain( secret, ); }); it("retains bounded selector diagnostics without exposing selector or reason text", () => { const selector = "role=button[name='selector-secret']"; const reason = "Unexpected token near reason-secret"; const cause = { invalidSelector: true as const, message: reason }; const error = new PreviewManager.PreviewAutomationInvalidSelectorError({ operation: "click", tabId: "tab_1", selectorKind: "locator", selectorLength: selector.length, reasonLength: reason.length, cause, }); const encoded = encodePreviewManagerError(error); const { cause: encodedCause, ...encodedDiagnostics } = encoded as typeof encoded & { readonly cause?: unknown; }; expect(error.cause).toBe(cause); expect(encodedCause).toStrictEqual(cause); expect(error).toMatchObject({ selectorKind: "locator", selectorLength: selector.length, reasonLength: reason.length, }); expect(error.detail).toEqual({ selectorKind: "locator", selectorLength: selector.length, }); expect(error.message).not.toContain("secret"); expect(JSON.stringify(encodedDiagnostics)).not.toContain("secret"); expect("selector" in error).toBe(false); expect("reason" in error).toBe(false); expect(PreviewManager.PreviewAutomationInvalidSelectorError.toTimelineMessage(error)).toBe( reason, ); }); it("does not retain a missing target locator", () => { const selector = "[data-token='target-secret']"; const error = new PreviewManager.PreviewAutomationTargetNotFoundError({ operation: "scroll", tabId: "tab_1", selectorKind: "selector", selectorLength: selector.length, }); expect(error.message).not.toContain(selector); expect(JSON.stringify(error)).not.toContain(selector); expect("locator" in error).toBe(false); }); });