Skip to content
Source
Sign inSign up

useRightPanelSurfaceClose.browser.test.tsx

137 lines5.6 KB

1import { EnvironmentId, ThreadId, type ScopedThreadRef } from "@t3tools/contracts";
2import { scopedThreadKey } from "@t3tools/client-runtime/environment";
3import { expect, it, vi } from "vite-plus/test";
4import { act, render } from "../../test/render";
5import { selectThreadRightPanelState, useRightPanelStore } from "../rightPanelStore";
6import { useTerminalUiStateStore } from "../terminalUiStateStore";
7import { useRightPanelSurfaceClose } from "./useRightPanelSurfaceClose";
8​
9const fixture = vi.hoisted(() => ({
10 confirm: (_labels: readonly string[]): Promise<boolean> => Promise.resolve(false),
11 allowed: new Set<string>(),
12 sessions: new Map<string, string>(),
13 failClose: false,
14}));
15vi.mock("../lib/terminalCloseConfirm", () => ({
16 confirmTerminalClose: (labels: readonly string[]) => fixture.confirm(labels),
17}));
18vi.mock("../components/ChatView.logic", () => ({
19 agentControlledBrowserCloseConfirmation: () => null,
20}));
21vi.mock("../previewStateStore", () => ({
22 useThreadPreviewState: () => ({ sessions: {}, desktopByTabId: {} }),
23 setActivePreviewTab: () => {},
24 beginPreviewSessionClose: () => {},
25 cancelPreviewSessionClose: () => {},
26}));
27vi.mock("../state/session", () => ({
28 useEnvironmentScope: (id: string) => fixture.allowed.has(id),
29 readEnvironmentScope: (id: string) => fixture.allowed.has(id),
30}));
31vi.mock("../state/entities", () => ({
32 useQuietThreadShell: () => null,
33 useProject: () => null,
34 readThreadShell: () => null,
35}));
36vi.mock("../state/terminalSessions", () => ({ useKnownTerminalSessions: () => null }));
37vi.mock("../composerDraftStore", async () => {
38 const Schema = await import("effect/Schema");
39 return {
40 useComposerDraftStore: () => null,
41 DraftId: Schema.String.pipe(Schema.brand("DraftId")),
42 };
43});
44vi.mock("../state/use-atom-command", () => ({
45 useAtomCommand:
46 (_command: unknown, label: string) =>
47 async (request: {
48 environmentId: string;
49 input: { threadId: string; terminalId: string; data?: string };
50 }) => {
51 const key = `${request.environmentId}/${request.input.threadId}/${request.input.terminalId}`;
52 if (label === "terminal close" && fixture.failClose) return { _tag: "Failure" };
53 if (label === "terminal close" || label === "terminal write") fixture.sessions.delete(key);
54 return { _tag: "Success" };
55 },
56}));
57// The commands are replaced above; importing their runtime would connect a client.
58vi.mock("../state/terminal", () => ({ terminalEnvironment: { open: {}, close: {}, write: {} } }));
59vi.mock("../state/preview", () => ({ previewEnvironment: { close: {} } }));
60vi.mock("../state/threads", () => ({ threadEnvironment: { delete: {} } }));
61vi.mock("../state/shell", () => ({ environmentShell: { stateValueAtom: () => null } }));
62vi.mock("@t3tools/client-runtime/state/runtime", () => ({
63 isAtomCommandInterrupted: () => false,
64}));
65​
66function setup() {
67 fixture.allowed = new Set(["environment-a", "environment-b"]);
68 fixture.sessions = new Map();
69 fixture.failClose = false;
70 useRightPanelStore.setState({ byThreadKey: {} });
71 useTerminalUiStateStore.setState({ terminalUiStateByThreadKey: {} });
72 const original: ScopedThreadRef = {
73 environmentId: EnvironmentId.make("environment-a"),
74 threadId: ThreadId.make("thread-a"),
75 };
76 const other: ScopedThreadRef = {
77 environmentId: EnvironmentId.make("environment-b"),
78 threadId: ThreadId.make("thread-b"),
79 };
80 for (const ref of [original, other]) {
81 fixture.sessions.set(`${ref.environmentId}/${ref.threadId}/term-1`, "history to preserve");
82 useRightPanelStore.getState().openTerminal(ref, "term-1");
83 }
84 const surface = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, original)
85 .surfaces[0]!;
86 let resolve!: (confirmed: boolean) => void;
87 const confirmation = new Promise<boolean>((done) => {
88 resolve = done;
89 });
90 fixture.confirm = () => confirmation;
91 function Harness({ threadRef }: { threadRef: ScopedThreadRef }) {
92 const { closeSurface } = useRightPanelSurfaceClose({
93 threadRef,
94 terminalLabelsById: new Map(),
95 });
96 return <button onClick={() => closeSurface(surface)}>Close original terminal</button>;
97 }
98 return { original, other, Harness, resolve };
100​
101it.each([false, true])(
102 "keeps a pending close on its original environment and thread, fallback=%s",
103 async (failClose) => {
104 const { original, other, Harness, resolve } = setup();
105 fixture.failClose = failClose;
106 await using screen = await render(<Harness threadRef={original} />);
107 await screen.getByRole("button").click();
108 await screen.rerender(<Harness threadRef={other} />);
109 await act(async () => {
110 resolve(true);
111 });
112 expect([...fixture.sessions]).toEqual([
113 ["environment-b/thread-b/term-1", "history to preserve"],
114 ]);
115 const panels = useRightPanelStore.getState().byThreadKey;
116 expect(selectThreadRightPanelState(panels, original).surfaces).toEqual([]);
117 expect(selectThreadRightPanelState(panels, other).surfaces).toHaveLength(1);
118 },
119);
120​
121it.each(["cancel", "revoked"])(
122 "leaves both sessions intact when confirmation is %s",
123 async (outcome) => {
124 const { original, other, Harness, resolve } = setup();
125 await using screen = await render(<Harness threadRef={original} />);
126 await screen.getByRole("button").click();
127 await screen.rerender(<Harness threadRef={other} />);
128 if (outcome === "revoked") fixture.allowed.delete(original.environmentId);
129 await act(async () => {
130 resolve(outcome !== "cancel");
131 });
132 expect(fixture.sessions.size).toBe(2);
133 expect(
134 useRightPanelStore.getState().byThreadKey[scopedThreadKey(other)]?.surfaces,
135 ).toHaveLength(1);
136 },
137);