1import { AsyncResult } from "effect/reactivity";
2import * as Cause from "effect/Cause";
3import { act } from "react";
4import { create, type ReactTestRenderer } from "react-test-renderer";
5import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
6
7const state = vi.hoisted(() => ({
8 navigate: vi.fn(),
9 params: { environmentId: "local", threadId: "active" } as Record<string, string>,
10 canPreview: true,
11 configured: true,
12 open: vi.fn(),
13 external: vi.fn(),
14 showBrowser: vi.fn(),
15 toast: vi.fn(),
16}));
17vi.mock("@tanstack/react-router", () => ({
18 useNavigate: () => state.navigate,
19 useParams: ({ select }: { select: (value: object) => unknown }) => select(state.params),
20}));
21vi.mock("../../state/environments", () => ({
22 usePrimaryEnvironmentId: () => "local",
23 useEnvironment: () => ({
24 serverConfig: {
25 mergebot: state.configured
26 ? { origin: "https://mergebot.example", repositories: ["work/app"] }
27 : undefined,
28 },
29 }),
30}));
31vi.mock("../../state/session", () => ({ useEnvironmentScope: () => state.canPreview }));
32vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));
33vi.mock("../../state/preview", () => ({ previewEnvironment: { open: {} } }));
34vi.mock("../preview/openPreviewSession", () => ({ openPreviewSession: state.open }));
35vi.mock("../../rightPanelStore", () => ({
36 useRightPanelStore: { getState: () => ({ openBrowser: state.showBrowser }) },
37}));
38vi.mock("../../previewStateStore", () => ({ readThreadPreviewState: () => ({ sessions: {} }) }));
39vi.mock("../../localApi", () => ({
40 readLocalApi: () => ({ shell: { openExternal: state.external } }),
41}));
42vi.mock("../ui/toast", () => ({ toastManager: { add: state.toast } }));
43vi.mock("../ui/sidebar", () => ({
44 useSidebar: () => ({ isMobile: false, setOpenMobile: vi.fn() }),
45 SidebarMenuItem: "div",
46 SidebarMenuButton: "button",
47}));
48vi.mock("../ui/tooltip", () => ({
49 Tooltip: "div",
50 TooltipPopup: "aside",
51 TooltipTrigger: ({ render }: { render: unknown }) => render,
52}));
53
54import { SidebarMergebotButton } from "./SidebarMergebotButton";
55let renderer: ReactTestRenderer;
56async function mount() {
57 await act(() => {
58 renderer = create(<SidebarMergebotButton />);
59 });
60}
61const click = () => renderer.root.findByType("button").props.onClick();
62beforeEach(() => {
63 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
64 state.params = { environmentId: "local", threadId: "active" };
65 state.canPreview = true;
66 state.configured = true;
67 state.open.mockReset().mockResolvedValue(AsyncResult.success({ tabId: "mergebot" }));
68 state.navigate.mockReset().mockResolvedValue(undefined);
69 state.external.mockReset().mockResolvedValue(undefined);
70 state.showBrowser.mockReset();
71 state.toast.mockReset();
72});
73afterEach(async () => {
74 await act(() => renderer?.unmount());
75 vi.unstubAllGlobals();
76});
77
78it("opens the live queue beside the active chat, and rapid clicks create one tab", async () => {
79 let finish!: (result: unknown) => void;
80 state.open.mockReturnValue(
81 new Promise((resolve) => {
82 finish = resolve;
83 }),
84 );
85 await mount();
86 await act(() => {
87 click();
88 click();
89 });
90 expect(state.open).toHaveBeenCalledTimes(1);
91 expect(state.open.mock.calls[0]![0]).toMatchObject({
92 url: "https://mergebot.example/",
93 threadRef: { environmentId: "local", threadId: "active" },
94 });
95 await act(async () => {
96 finish(AsyncResult.success({ tabId: "mergebot" }));
97 });
98 expect(state.showBrowser).toHaveBeenCalledExactlyOnceWith(
99 { environmentId: "local", threadId: "active" },
100 "mergebot",
101 );
102 expect(state.external).not.toHaveBeenCalled();
103});
104it("opens the queue inside the PR page without an active chat", async () => {
105 state.params = {};
106 await mount();
107 await act(() => click());
108 expect(state.open).not.toHaveBeenCalled();
109 expect(state.navigate).toHaveBeenCalledExactlyOnceWith({
110 to: "/pull-requests",
111 search: { involvement: "all", state: "open", mergebot: "local" },
112 });
113 expect(state.external).not.toHaveBeenCalled();
114});
115it("reports an integrated-browser failure and allows retry", async () => {
116 state.open.mockResolvedValueOnce(AsyncResult.failure(Cause.fail(new Error("offline"))));
117 await mount();
118 await act(() => click());
119 expect(state.toast).toHaveBeenCalledWith(
120 expect.objectContaining({ type: "error", description: "offline" }),
121 );
122 expect(state.external).not.toHaveBeenCalled();
123 await act(() => click());
124 expect(state.open).toHaveBeenCalledTimes(2);
125 expect(state.showBrowser).toHaveBeenCalledTimes(1);
126});
127
128it("hides the shortcut when the destination has no Mergebot configuration", async () => {
129 state.configured = false;
130 await mount();
131 expect(renderer.toJSON()).toBeNull();
132 expect(state.open).not.toHaveBeenCalled();
133 expect(state.external).not.toHaveBeenCalled();
134});