1import { EnvironmentId, ThreadId, type ThreadPullRequestLink } from "@t3tools/contracts";
2import { act } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, expect, it, vi } from "vite-plus/test";
5
6const watchCommand = vi.hoisted(() => vi.fn());
7
8vi.mock("./ThreadDetailsPrRow", () => ({
9 ThreadDetailsPrRow: ({
10 number,
11 onStopWatching,
12 }: {
13 number: number;
14 onStopWatching?: () => void;
15 }) => (
16 <span
17 data-row={String(number)}
18 data-watched={onStopWatching ? "" : undefined}
19 onClick={onStopWatching}
20 />
21 ),
22}));
23vi.mock("~/state/entities", () => ({ useProjects: () => [] }));
24vi.mock("~/state/threads", () => ({ threadEnvironment: {} }));
25vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => watchCommand }));
26vi.mock("~/lib/openPullRequestLink", () => ({
27 parseChangeRequestUrl: () => null,
28 findProjectOnChangeRequestHost: () => undefined,
29}));
30
31import { ThreadDetailsPrRows } from "./ThreadDetailsPrRows";
32
33function link(
34 number: number,
35 headBranch: string,
36 baseBranch: string,
37 updatedAt: string,
38): ThreadPullRequestLink {
39 return {
40 host: "github.com",
41 repository: "pingdotgg/t3code",
42 number,
43 url: `https://github.com/pingdotgg/t3code/pull/${number}`,
44 source: "manual",
45 linkedAt: updatedAt,
46 snapshot: {
47 state: "open",
48 title: `Change ${number}`,
49 headBranch,
50 baseBranch,
51 isDraft: false,
52 updatedAt,
53 syncedAt: updatedAt,
54 },
55 stack: null,
56 };
57}
58
59const bottom = link(1, "layer-one", "main", "2026-01-01T00:00:10.000Z");
60const top = link(2, "layer-two", "layer-one", "2026-01-01T00:00:20.000Z");
61const other = link(3, "unrelated", "main", "2026-01-01T00:00:05.000Z");
62
63let renderer: ReactTestRenderer;
64afterEach(() => {
65 act(() => renderer?.unmount());
66 vi.unstubAllGlobals();
67});
68
69function render(links: ReadonlyArray<ThreadPullRequestLink>, current: ThreadPullRequestLink) {
70 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
71 act(() => {
72 renderer = create(
73 <ThreadDetailsPrRows
74 threadRef={{
75 environmentId: EnvironmentId.make("environment"),
76 threadId: ThreadId.make("thread"),
77 }}
78 links={links}
79 currentLink={current}
80 onOpenLink={vi.fn()}
81 environmentId={EnvironmentId.make("environment")}
82 pr={null}
83 number={current.number}
84 reference={current}
85 status={null}
86 project={null}
87 label={`#${current.number}`}
88 openAriaLabel="Open pull request"
89 onOpen={vi.fn()}
90 />,
91 );
92 });
93}
94
95const rows = () => renderer.root.findAllByType("span").map((node) => node.props["data-row"]);
96const toggleLabel = () =>
97 renderer.root
98 .findAllByType("button")
99 .at(-1)
100 ?.children.filter((child) => typeof child === "string")
101 .join("");
102
103function toggle() {
104 act(() => {
105 (renderer.root.findAllByType("button").at(-1)!.props as { onClick: () => void }).onClick();
106 });
107}
108
109it("shows only the current pull request until the rest are asked for", () => {
110 render([other, bottom, top], top);
111 expect(rows()).toEqual(["2"]);
112 expect(toggleLabel()).toBe("Show 2 more");
113
114 toggle();
115 expect(rows()).toEqual(["2", "1", "3"]);
116 expect(toggleLabel()).toBe("Show less");
117
118 toggle();
119 expect(rows()).toEqual(["2"]);
120 expect(toggleLabel()).toBe("Show 2 more");
121});
122
123it("keeps the single row untouched when the thread links one pull request", () => {
124 render([bottom], bottom);
125 expect(rows()).toEqual(["1"]);
126 expect(toggleLabel()).toBeUndefined();
127});
128
129it("lets only watched pull requests stop their watch", () => {
130 const watched: ThreadPullRequestLink = {
131 ...bottom,
132 watch: {
133 startedAt: "2026-01-01T00:00:30.000Z",
134 headSha: null,
135 failedChecks: [],
136 passed: false,
137 passedChecks: [],
138 remarksThrough: "2026-01-01T00:00:30.000Z",
139 remarkIds: [],
140 conflicting: false,
141 wakes: 0,
142 },
143 };
144 render([other, watched, top], top);
145 toggle();
146 const spans = renderer.root.findAllByType("span");
147 expect(
148 spans
149 .filter((node) => node.props["data-watched"] !== undefined)
150 .map((node) => node.props["data-row"]),
151 ).toEqual(["1"]);
152
153 act(() => spans.find((node) => node.props["data-row"] === "1")!.props.onClick());
154 expect(watchCommand).toHaveBeenCalledWith({
155 environmentId: EnvironmentId.make("environment"),
156 input: {
157 threadId: ThreadId.make("thread"),
158 host: "github.com",
159 repository: "pingdotgg/t3code",
160 number: 1,
161 watching: false,
162 },
163 });
164});