Skip to content
Source
Sign inSign up
Open History to see the last commit

ThreadDetailsPanel.browser.test.tsx

217 lines6.8 KB

1import {
2 EnvironmentId,
3 ProjectId,
4 ThreadId,
5 type ScopedThreadRef,
6 type ThreadPullRequestLink,
7} from "@t3tools/contracts";
8import { AsyncResult, Atom } from "effect/reactivity";
9import { expect, it, vi } from "vite-plus/test";
10import { page } from "vite-plus/test/browser";
11​
12const SCRATCH_ROOT = "/home/user/.t3/scratch";
13​
14const state = vi.hoisted(() => ({
15 workspaceRoot: "",
16 folder: "",
17 pullRequests: [] as unknown[],
18 configLoaded: true,
19}));
20​
21vi.mock(import("~/state/entities"), async (importOriginal) => ({
22 ...(await importOriginal()),
23 useProject: ((ref: unknown) =>
24 ref === null
25 ? null
26 : {
27 id: "project",
28 environmentId: "environment",
29 title: state.workspaceRoot === SCRATCH_ROOT ? "No project" : "plain",
30 workspaceRoot: state.workspaceRoot,
31 scripts: [],
32 }) as never,
33 useProjects: (() => []) as never,
34 useQuietThreadShell: ((ref: unknown) =>
35 ref === null
36 ? null
37 : {
38 id: "thread",
39 environmentId: "environment",
40 projectId: "project",
41 worktreePath: state.folder,
42 branch: null,
43 pullRequests: state.pullRequests,
44 }) as never,
45 useServerConfigs: (() =>
46 new Map(
47 state.configLoaded
48 ? [
49 [
50 EnvironmentId.make("environment"),
51 {
52 scratchWorkspaceRoot: SCRATCH_ROOT,
53 environment: { capabilities: { pullRequests: true } },
54 },
55 ],
56 ]
57 : [],
58 )) as never,
59}));
60// Neither folder is a repository, which is what the server reports for both.
61vi.mock(import("~/state/vcs"), async (importOriginal) => {
62 const actual = await importOriginal();
63 const notARepo = Atom.make(
64 AsyncResult.success({
65 isRepo: false,
66 hasPrimaryRemote: false,
67 isDefaultRef: false,
68 refName: null,
69 hasWorkingTreeChanges: false,
70 workingTree: { files: [], insertions: 0, deletions: 0 },
71 hasUpstream: false,
72 aheadCount: 0,
73 behindCount: 0,
74 aheadOfDefaultCount: 0,
75 pr: null,
76 }),
77 );
78 return {
79 ...actual,
80 vcsEnvironment: { ...actual.vcsEnvironment, status: (() => notARepo) as never },
81 };
82});
83​
84import { render } from "../../../test/render";
85import { useRightPanelStore } from "../../rightPanelStore";
86import { PopoverCreateHandle } from "../ui/popover";
87import { ChatCanvasContext } from "./ChatCanvasContext";
88import { resolveChatCanvasLayout } from "./chatCanvasLayout";
89import { ThreadDetailsPanel } from "./ThreadDetailsPanel";
90​
91const threadRef: ScopedThreadRef = {
92 environmentId: EnvironmentId.make("environment"),
93 threadId: ThreadId.make("thread"),
94};
95const container = { width: 1200, height: 800 };
96const placement = { x: 908, y: 0, right: 12, width: 280, height: 400 } as const;
97const linkedPullRequest: ThreadPullRequestLink = {
98 host: "github.com",
99 repository: "pingdotgg/t3code",
100 number: 27513,
101 url: "https://github.com/pingdotgg/t3code/pull/27513",
102 source: "manual",
103 linkedAt: "2026-10-30T00:00:00.000Z",
104 snapshot: null,
105 stack: null,
106};
107​
108function setup(input: {
109 workspaceRoot: string;
110 folder: string;
111 pullRequests?: ThreadPullRequestLink[];
112 configLoaded?: boolean;
113}) {
114 state.configLoaded = input.configLoaded ?? true;
115 state.workspaceRoot = input.workspaceRoot;
116 state.folder = input.folder;
117 state.pullRequests = input.pullRequests ?? [];
118 useRightPanelStore.setState({ threadPanelVisibilityByThreadKey: {} });
119 const anchor = document.createElement("div");
120 document.body.append(anchor);
121 return {
122 anchor: { current: anchor },
123 handle: PopoverCreateHandle(),
124 [Symbol.dispose]: () => anchor.remove(),
125 };
127​
128function Panel({ fixture }: { fixture: ReturnType<typeof setup> }) {
129 const noop = () => {};
130 const scriptResult = async () => ({ ok: true }) as never;
131 return (
132 <ChatCanvasContext
133 value={{
134 container,
135 lane: { padding: 48, minChatWidth: 640 },
136 layout: resolveChatCanvasLayout({ container, preview: null }),
137 previewKey: null,
138 registerTimeline: noop,
139 measureDetailsCard: noop,
140 detailsCardTopInset: 0,
141 preferredDetailsCard: placement,
142 detailsCardPlacement: placement,
143 }}
144 >
145 <ThreadDetailsPanel
146 anchor={fixture.anchor}
147 handle={fixture.handle}
148 environmentId={threadRef.environmentId}
149 threadId={threadRef.threadId}
150 projectId={ProjectId.make("project")}
151 composerDraftTarget={threadRef}
152 worktreePath={state.folder}
153 isServerThread
154 activeProjectName={state.workspaceRoot === SCRATCH_ROOT ? "No project" : "plain"}
155 activeProjectScripts={undefined}
156 preferredScriptId={null}
157 keybindings={[]}
158 availableEditors={[]}
159 showOpenInPicker={false}
160 openInShortcutEnabled={false}
161 gitCwd={state.folder}
162 workspaceMutationId={null}
163 isGitRepo={false}
164 envLocked
165 availableEnvironments={[]}
166 onEnvModeChange={noop}
167 envMode="local"
168 startFromOrigin={false}
169 onStartFromOriginChange={noop}
170 onComposerFocusRequest={noop}
171 onRunProjectScript={noop}
172 onAddProjectScript={scriptResult}
173 onUpdateProjectScript={scriptResult}
174 onDeleteProjectScript={scriptResult}
175 usage={{} as never}
176 />
177 </ChatCanvasContext>
178 );
180​
181it("offers no git controls in a No project thread, but shows its linked pull request", async () => {
182 using fixture = setup({
183 workspaceRoot: SCRATCH_ROOT,
184 folder: `${SCRATCH_ROOT}/thread`,
185 pullRequests: [linkedPullRequest],
186 });
187 await using _screen = await render(<Panel fixture={fixture} />);
188 await expect.element(page.getByText("#27513")).toBeVisible();
189 await expect
190 .element(page.getByRole("button", { name: "Initialize Git" }))
191 .not.toBeInTheDocument();
192});
193​
194it("shows a linked pull request in a folder that is not a git repository", async () => {
195 using fixture = setup({
196 workspaceRoot: "/work/plain",
197 folder: "/work/plain",
198 pullRequests: [linkedPullRequest],
199 });
200 await using _screen = await render(<Panel fixture={fixture} />);
201 await expect.element(page.getByRole("region", { name: "Workspace" })).toBeVisible();
202 await expect.element(page.getByText("#27513")).toBeVisible();
203 await expect.element(page.getByRole("button", { name: "Initialize Git" })).toBeVisible();
204});
205​
206it("offers no Initialize Git before the server config says where No project lives", async () => {
207 using fixture = setup({
208 workspaceRoot: SCRATCH_ROOT,
209 folder: `${SCRATCH_ROOT}/thread`,
210 configLoaded: false,
211 });
212 await using _screen = await render(<Panel fixture={fixture} />);
213 await expect.element(page.getByRole("region", { name: "Workspace" })).toBeVisible();
214 await expect
215 .element(page.getByRole("button", { name: "Initialize Git" }))
216 .not.toBeInTheDocument();
217});