1import { EnvironmentId, type PullRequestCheck } from "@t3tools/contracts";
2import { act, cloneElement, type ReactElement, type ReactNode } from "react";
3import { create, type ReactTestRenderer } from "react-test-renderer";
4import { afterEach, expect, it, vi } from "vite-plus/test";
5
6const state = vi.hoisted(() => ({
7 status: "success" as PullRequestCheck["status"],
8 extraStatus: null as PullRequestCheck["status"] | null,
9 perform: vi.fn(),
10}));
11
12vi.mock("~/state/entities", () => ({ useServerConfigs: () => new Map() }));
13vi.mock("~/state/pullRequests", () => ({ pullRequestEnvironment: {} }));
14vi.mock("~/hooks/useLiveRefresh", () => ({ useLiveRefresh: () => {} }));
15vi.mock("~/state/query", () => ({
16 useEnvironmentQuery: () => ({
17 data: {
18 number: 1,
19 title: "Test PR",
20 state: "open",
21 isDraft: false,
22 mergeability: "mergeable",
23 headBranch: "feature",
24 baseBranch: "main",
25 changedFiles: 1,
26 additions: 1,
27 deletions: 0,
28 checks: [state.status, ...(state.extraStatus ? [state.extraStatus] : [])].map((status) => ({
29 name: `CI-${status}`,
30 status,
31 description: null,
32 url: null,
33 })),
34 capabilities: { actions: ["merge"], mergeMethods: ["merge"] },
35 viewerPermissions: { actions: ["merge"] },
36 mergeCapabilities: { merge: true, squash: false, rebase: false },
37 },
38 dataUpdatedAt: 1,
39 isPending: false,
40 refresh: vi.fn(),
41 }),
42}));
43vi.mock("../pullRequest/usePullRequestActions", () => ({
44 usePullRequestActionRunner: () => ({ actionPending: false, perform: state.perform }),
45 usePullRequestHandoffs: () => ({ handoff: null, startHandoff: vi.fn() }),
46}));
47vi.mock("../ui/popover", () => ({
48 Popover: ({ children }: { children: ReactNode }) => children,
49 PopoverTrigger: ({ render, children }: { render: ReactElement; children: ReactNode }) =>
50 cloneElement(render, undefined, children),
51 PopoverPopup: () => null,
52}));
53vi.mock("../ui/tooltip", () => ({
54 Tooltip: ({ children }: { children: ReactNode }) => children,
55 TooltipTrigger: ({ render, children }: { render: ReactElement; children: ReactNode }) =>
56 cloneElement(render, undefined, children),
57 TooltipPopup: () => null,
58}));
59vi.mock("../ui/alert-dialog", () => ({
60 AlertDialog: ({ open, children }: { open: boolean; children: ReactNode }) =>
61 open ? <div role="alertdialog">{children}</div> : null,
62 AlertDialogPopup: ({ children }: { children: ReactNode }) => children,
63 AlertDialogHeader: ({ children }: { children: ReactNode }) => children,
64 AlertDialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
65 AlertDialogDescription: ({ children }: { children: ReactNode }) => children,
66 AlertDialogFooter: ({ children }: { children: ReactNode }) => children,
67 AlertDialogClose: () => null,
68}));
69
70import { ThreadDetailsPrRow } from "./ThreadDetailsPrRow";
71
72let renderer: ReactTestRenderer;
73afterEach(() => {
74 act(() => renderer?.unmount());
75 vi.unstubAllGlobals();
76 state.status = "success";
77 state.extraStatus = null;
78});
79
80it("requires a new merge click after passing checks become pending and pass again", () => {
81 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
82 const render = () => (
83 <ThreadDetailsPrRow
84 environmentId={EnvironmentId.make("environment")}
85 pr={null}
86 number={1}
87 status={null}
88 project={null}
89 label="Test PR"
90 openAriaLabel="Open PR"
91 onOpen={vi.fn()}
92 />
93 );
94 const clickMerge = () =>
95 act(() => {
96 renderer.root
97 .findAllByType("button")
98 .find((button) => button.children.includes("Merge"))!
99 .props.onClick();
100 });
101 const dialogs = () => renderer.root.findAllByProps({ role: "alertdialog" });
102
103 act(() => {
104 renderer = create(render());
105 });
106 clickMerge();
107 expect(dialogs()).toHaveLength(1);
108 act(() => {
109 state.status = "pending";
110 renderer.update(render());
111 });
112 expect(dialogs()).toHaveLength(0);
113 act(() => {
114 state.status = "success";
115 renderer.update(render());
116 });
117 expect(dialogs()).toHaveLength(0);
118 clickMerge();
119 expect(dialogs()).toHaveLength(1);
120 expect(state.perform).not.toHaveBeenCalled();
121});
122
123it.each<[PullRequestCheck["status"], PullRequestCheck["status"], string]>([
124 ["success", "skipped", ""],
125 ["failure", "cancelled", ""],
126 ["success", "action-required", ""],
127 ["success", "pending", "1/2"],
128 ["failure", "pending", "1/2"],
129])("shows a count only while checks run (%s, %s)", (status, extraStatus, count) => {
130 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
131 state.status = status;
132 state.extraStatus = extraStatus;
133 act(() => {
134 renderer = create(
135 <ThreadDetailsPrRow
136 environmentId={EnvironmentId.make("environment")}
137 pr={null}
138 number={1}
139 status={null}
140 project={null}
141 label="Test PR"
142 openAriaLabel="Open PR"
143 onOpen={vi.fn()}
144 />,
145 );
146 });
147 const text = renderer.root
148 .findAllByType("span")
149 .map((span) => span.children.filter((child) => typeof child === "string").join(""));
150 expect(text.filter((value) => /^\d+\/\d+$/.test(value))).toEqual(count ? [count] : []);
151});