import { it as effectIt } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Cause from "effect/Cause";
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";
const {
createFromPath,
fromId,
getFocusedWebContents,
mkdir,
showItemInFolder,
webviewSend,
writeFile,
writeImage,
} = vi.hoisted(() => ({
createFromPath: vi.fn((): { readonly isEmpty: () => boolean } => ({ isEmpty: () => false })),
fromId: vi.fn(() => 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),
writeImage: vi.fn(),
}));
vi.mock("electron", () => ({
clipboard: {
writeImage,
},
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",
} 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, "linux")),
);
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);
describe("PreviewManager", () => {
beforeEach(() => {
fromId.mockClear();
getFocusedWebContents.mockReset();
getFocusedWebContents.mockReturnValue(null);
mkdir.mockClear();
writeFile.mockClear();
showItemInFolder.mockClear();
writeImage.mockClear();
createFromPath.mockClear();
webviewSend.mockClear();
});
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("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(),
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 },
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("mirrors Electron's effective zoom across registration and navigation", () =>
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,
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 },
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(0.9);
expect(setZoomFactor).not.toHaveBeenCalled();
effectiveZoom = 1.25;
listeners.get("did-navigate")?.();
yield* Effect.yieldNow;
expect(states.at(-1)?.zoomFactor).toBe(1.25);
expect(setZoomFactor).not.toHaveBeenCalled();
zoomReadable = false;
url = "https://example.com/after-zoom-read-failed";
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.25);
const replacementSetZoomFactor = vi.fn();
fromId.mockReturnValue({
id: 43,
isDestroyed: () => false,
getType: () => "webview",
getURL: () => url,
getTitle: () => "Example",
isLoading: () => false,
getZoomFactor: () => 1,
setZoomFactor: replacementSetZoomFactor,
on: vi.fn(),
off: vi.fn(),
ipc: { on: vi.fn(), off: vi.fn() },
send: webviewSend,
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
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.25);
expect(states.at(-1)?.zoomFactor).toBe(1.25);
}),
),
);
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(),
on: vi.fn(),
off: vi.fn(),
ipc: { on: vi.fn(), off: vi.fn() },
send: webviewSend,
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
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");
}),
),
);
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(),
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 },
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(),
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 },
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$/,
);
const captureCause = new Error("capture failed");
capturePage.mockRejectedValueOnce(captureCause);
const exit = yield* Effect.exit(manager.captureScreenshot("tab_1"));
expect(Exit.isFailure(exit)).toBe(true);
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 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(),
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 },
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("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(writeImage).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 });
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,
});
}),
),
);
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(),
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 },
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(),
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 },
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(),
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 },
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(),
on: vi.fn(),
off: vi.fn(),
ipc: { on: vi.fn(), off: vi.fn() },
send: webviewSend,
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
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);
});
});