import { DEFAULT_CLIENT_SETTINGS, EnvironmentId, ThreadId, type ClientSettings, type PreviewAutomationResponse, type PreviewAutomationStreamEvent, type PreviewOpenInput, type PreviewSessionSnapshot,} from "@t3tools/contracts";import { AsyncResult, Atom } from "effect/unstable/reactivity";import { act } from "react";import { create, type ReactTestRenderer } from "react-test-renderer";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { __resetClientSettingsPersistenceForTests } from "~/hooks/useSettings";import { readThreadPreviewState, resetPreviewStateForTests } from "~/previewStateStore";import { appAtomRegistry, AppAtomRegistryProvider } from "~/rpc/atomRegistry"; import { PreviewAutomationHosts } from "./PreviewAutomationHosts"; const mocks = vi.hoisted(() => ({ getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(), setClientSettings: vi.fn(), open: vi.fn(async (_target: { environmentId: EnvironmentId; input: PreviewOpenInput }) => AsyncResult.success(snapshot), ), list: vi.fn(async () => AsyncResult.success(emptyList)), resize: vi.fn(), respond: vi.fn< (target: { environmentId: EnvironmentId; input: PreviewAutomationResponse }) => Promise<void> >(), focus: vi.fn(async () => undefined),})); vi.mock("~/localApi", () => ({ ensureLocalApi: () => ({ persistence: mocks }),}));vi.mock("~/env", () => ({ isElectron: true }));vi.mock("~/state/environments", () => ({ useEnvironments: () => ({ environments: [{ environmentId }] }),}));vi.mock("~/state/preview", () => ({ previewEnvironment: { automationRequests: () => requestsAtom, list: () => listAtom, open: mocks.open, resize: mocks.resize, respondToAutomation: mocks.respond, focusAutomationHost: mocks.focus, },}));vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: (command: unknown) => command,}));vi.mock("~/state/use-atom-query-runner", () => ({ useAtomQueryRunner: () => mocks.list,}));vi.mock("./previewBridge", () => ({ previewBridge: { automation: {} } })); const environmentId = EnvironmentId.make("automation-environment");const threadId = ThreadId.make("automation-thread");const threadRef = { environmentId, threadId };const viewport = { _tag: "freeform", width: 1440, height: 900 } as const;const savedSettings: ClientSettings = { ...DEFAULT_CLIENT_SETTINGS, browserDefaultViewport: viewport, browserDefaultProfileId: "work", browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }],};const snapshot: PreviewSessionSnapshot = { threadId, tabId: "automation-tab", navStatus: { _tag: "Idle" }, canGoBack: false, canGoForward: false, viewport, profileId: "work", updatedAt: "2026-09-05T00:00:00.000Z",};const emptyList = { sessions: [], serverEpoch: "test-server", revision: 0 };const listAtom = Atom.make(AsyncResult.success(emptyList));const requestsAtom = Atom.make<AsyncResult.AsyncResult<PreviewAutomationStreamEvent, Error>>( AsyncResult.initial(false),);const requestEvent: PreviewAutomationStreamEvent = { type: "request", connectionId: "automation-connection", request: { requestId: "open-request", threadId, operation: "open", input: { open: false, reuseExistingTab: false }, timeoutMs: 15_000, },}; function deferred<A>() { let resolve!: (value: A) => void; const promise = new Promise<A>((complete) => { resolve = complete; }); return { promise, resolve };} let renderer: ReactTestRenderer | null = null; beforeEach(async () => { vi.clearAllMocks(); mocks.getClientSettings.mockReset().mockResolvedValue(savedSettings); mocks.respond.mockReset(); __resetClientSettingsPersistenceForTests(); resetPreviewStateForTests(); appAtomRegistry.set(requestsAtom, AsyncResult.initial(false)); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.stubGlobal("window", { addEventListener: vi.fn(), removeEventListener: vi.fn() }); vi.stubGlobal("document", { hasFocus: () => false, querySelectorAll: () => [] }); await act(() => { renderer = create( <AppAtomRegistryProvider> <PreviewAutomationHosts /> </AppAtomRegistryProvider>, ); });}); afterEach(async () => { await act(() => renderer?.unmount()); renderer = null; resetPreviewStateForTests(); __resetClientSettingsPersistenceForTests(); vi.unstubAllGlobals(); vi.restoreAllMocks();}); describe("PreviewAutomationHosts open", () => { it("waits for saved settings before opening a tab with the configured profile and viewport", async () => { const readStarted = deferred<void>(); const read = deferred<ClientSettings>(); const response = deferred<PreviewAutomationResponse>(); mocks.getClientSettings.mockImplementationOnce(() => { readStarted.resolve(); return read.promise; }); mocks.respond.mockImplementationOnce(async ({ input }) => response.resolve(input)); await act(async () => { appAtomRegistry.set(requestsAtom, AsyncResult.success(requestEvent)); await readStarted.promise; }); expect(mocks.open).not.toHaveBeenCalled(); await act(async () => { read.resolve(savedSettings); await response.promise; }); expect(mocks.open).toHaveBeenCalledExactlyOnceWith({ environmentId, input: { threadId, viewport, profileId: "work" }, }); expect(mocks.getClientSettings).toHaveBeenCalledOnce(); await expect(response.promise).resolves.toMatchObject({ requestId: "open-request", ok: true }); expect(readThreadPreviewState(threadRef).snapshot).toEqual(snapshot); expect(mocks.setClientSettings).not.toHaveBeenCalled(); }); it("reports a settings read failure without opening a tab", async () => { vi.spyOn(console, "error").mockImplementation(() => undefined); mocks.getClientSettings.mockRejectedValueOnce(new Error("Settings read failed")); const response = deferred<PreviewAutomationResponse>(); mocks.respond.mockImplementationOnce(async ({ input }) => response.resolve(input)); await act(async () => { appAtomRegistry.set(requestsAtom, AsyncResult.success(requestEvent)); await response.promise; }); await expect(response.promise).resolves.toMatchObject({ requestId: "open-request", ok: false, error: { _tag: "PreviewAutomationExecutionError" }, }); expect(mocks.getClientSettings).toHaveBeenCalledOnce(); expect(mocks.open).not.toHaveBeenCalled(); expect(readThreadPreviewState(threadRef).snapshot).toBeNull(); expect(mocks.setClientSettings).not.toHaveBeenCalled(); });});
import {
DEFAULT_CLIENT_SETTINGS,
EnvironmentId,
ThreadId,
type ClientSettings,
type PreviewAutomationResponse,
type PreviewAutomationStreamEvent,
type PreviewOpenInput,
type PreviewSessionSnapshot,
} from "@t3tools/contracts";
import { AsyncResult, Atom } from "effect/unstable/reactivity";
import { act } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { __resetClientSettingsPersistenceForTests } from "~/hooks/useSettings";
import { readThreadPreviewState, resetPreviewStateForTests } from "~/previewStateStore";
import { appAtomRegistry, AppAtomRegistryProvider } from "~/rpc/atomRegistry";
import { PreviewAutomationHosts } from "./PreviewAutomationHosts";
const mocks = vi.hoisted(() => ({
getClientSettings: vi.fn<() => Promise<ClientSettings | null>>(),
setClientSettings: vi.fn(),
open: vi.fn(async (_target: { environmentId: EnvironmentId; input: PreviewOpenInput }) =>
AsyncResult.success(snapshot),
),
list: vi.fn(async () => AsyncResult.success(emptyList)),
resize: vi.fn(),
respond:
vi.fn<
(target: { environmentId: EnvironmentId; input: PreviewAutomationResponse }) => Promise<void>
>(),
focus: vi.fn(async () => undefined),
}));
vi.mock("~/localApi", () => ({
ensureLocalApi: () => ({ persistence: mocks }),
}));
vi.mock("~/env", () => ({ isElectron: true }));
vi.mock("~/state/environments", () => ({
useEnvironments: () => ({ environments: [{ environmentId }] }),
}));
vi.mock("~/state/preview", () => ({
previewEnvironment: {
automationRequests: () => requestsAtom,
list: () => listAtom,
open: mocks.open,
resize: mocks.resize,
respondToAutomation: mocks.respond,
focusAutomationHost: mocks.focus,
},
}));
vi.mock("~/state/use-atom-command", () => ({
useAtomCommand: (command: unknown) => command,
}));
vi.mock("~/state/use-atom-query-runner", () => ({
useAtomQueryRunner: () => mocks.list,
}));
vi.mock("./previewBridge", () => ({ previewBridge: { automation: {} } }));
const environmentId = EnvironmentId.make("automation-environment");
const threadId = ThreadId.make("automation-thread");
const threadRef = { environmentId, threadId };
const viewport = { _tag: "freeform", width: 1440, height: 900 } as const;
const savedSettings: ClientSettings = {
...DEFAULT_CLIENT_SETTINGS,
browserDefaultViewport: viewport,
browserDefaultProfileId: "work",
browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }],
};
const snapshot: PreviewSessionSnapshot = {
threadId,
tabId: "automation-tab",
navStatus: { _tag: "Idle" },
canGoBack: false,
canGoForward: false,
viewport,
profileId: "work",
updatedAt: "2026-09-05T00:00:00.000Z",
};
const emptyList = { sessions: [], serverEpoch: "test-server", revision: 0 };
const listAtom = Atom.make(AsyncResult.success(emptyList));
const requestsAtom = Atom.make<AsyncResult.AsyncResult<PreviewAutomationStreamEvent, Error>>(
AsyncResult.initial(false),
);
const requestEvent: PreviewAutomationStreamEvent = {
type: "request",
connectionId: "automation-connection",
request: {
requestId: "open-request",
threadId,
operation: "open",
input: { open: false, reuseExistingTab: false },
timeoutMs: 15_000,
},
};
function deferred<A>() {
let resolve!: (value: A) => void;
const promise = new Promise<A>((complete) => {
resolve = complete;
});
return { promise, resolve };
}
let renderer: ReactTestRenderer | null = null;
beforeEach(async () => {
vi.clearAllMocks();
mocks.getClientSettings.mockReset().mockResolvedValue(savedSettings);
mocks.respond.mockReset();
__resetClientSettingsPersistenceForTests();
resetPreviewStateForTests();
appAtomRegistry.set(requestsAtom, AsyncResult.initial(false));
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("window", { addEventListener: vi.fn(), removeEventListener: vi.fn() });
vi.stubGlobal("document", { hasFocus: () => false, querySelectorAll: () => [] });
await act(() => {
renderer = create(
<AppAtomRegistryProvider>
<PreviewAutomationHosts />
</AppAtomRegistryProvider>,
);
});
});
afterEach(async () => {
await act(() => renderer?.unmount());
renderer = null;
resetPreviewStateForTests();
__resetClientSettingsPersistenceForTests();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("PreviewAutomationHosts open", () => {
it("waits for saved settings before opening a tab with the configured profile and viewport", async () => {
const readStarted = deferred<void>();
const read = deferred<ClientSettings>();
const response = deferred<PreviewAutomationResponse>();
mocks.getClientSettings.mockImplementationOnce(() => {
readStarted.resolve();
return read.promise;
});
mocks.respond.mockImplementationOnce(async ({ input }) => response.resolve(input));
await act(async () => {
appAtomRegistry.set(requestsAtom, AsyncResult.success(requestEvent));
await readStarted.promise;
});
expect(mocks.open).not.toHaveBeenCalled();
await act(async () => {
read.resolve(savedSettings);
await response.promise;
});
expect(mocks.open).toHaveBeenCalledExactlyOnceWith({
environmentId,
input: { threadId, viewport, profileId: "work" },
});
expect(mocks.getClientSettings).toHaveBeenCalledOnce();
await expect(response.promise).resolves.toMatchObject({ requestId: "open-request", ok: true });
expect(readThreadPreviewState(threadRef).snapshot).toEqual(snapshot);
expect(mocks.setClientSettings).not.toHaveBeenCalled();
});
it("reports a settings read failure without opening a tab", async () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
mocks.getClientSettings.mockRejectedValueOnce(new Error("Settings read failed"));
const response = deferred<PreviewAutomationResponse>();
mocks.respond.mockImplementationOnce(async ({ input }) => response.resolve(input));
await act(async () => {
appAtomRegistry.set(requestsAtom, AsyncResult.success(requestEvent));
await response.promise;
});
await expect(response.promise).resolves.toMatchObject({
requestId: "open-request",
ok: false,
error: { _tag: "PreviewAutomationExecutionError" },
});
expect(mocks.getClientSettings).toHaveBeenCalledOnce();
expect(mocks.open).not.toHaveBeenCalled();
expect(readThreadPreviewState(threadRef).snapshot).toBeNull();
expect(mocks.setClientSettings).not.toHaveBeenCalled();
});
});