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>(), 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 >(), 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.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() { let resolve!: (value: A) => void; const promise = new Promise((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( , ); }); }); 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(); const read = deferred(); const response = deferred(); 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(); 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(); }); });