1import { describe, expect, it } from "vite-plus/test";
2import {
3 allowedMergeMethods,
4 allowedUpdateMethods,
5 canPerformPullRequestAction,
6 mergeSummary,
7 mergeUnavailableReason,
8 ALL_PR_ACTIONS,
9} from "./pullRequestActions";
10
11type Detail = Parameters<typeof allowedMergeMethods>[0];
12
13function detail(overrides: Partial<Detail> = {}): Detail {
14 return {
15 state: "open",
16 isDraft: false,
17 mergeability: "mergeable",
18 autoMergeEnabled: false,
19 capabilities: {
20 actions: ALL_PR_ACTIONS,
21 mergeMethods: ["merge", "squash", "rebase"],
22 updateMethods: ["merge", "rebase"],
23 },
24 viewerPermissions: { actions: ALL_PR_ACTIONS, updateMethods: ["merge"] },
25 mergeCapabilities: { merge: true, squash: true, rebase: false },
26 ...overrides,
27 };
28}
29
30describe("mobile pull request actions", () => {
31 it("offers branch updates only through strategies supported by the provider and account", () => {
32 expect(allowedUpdateMethods(detail())).toEqual(["merge"]);
33 expect(canPerformPullRequestAction(detail(), "update-branch")).toBe(true);
34 const unsupported = detail({ viewerPermissions: { actions: ALL_PR_ACTIONS } });
35 expect(allowedUpdateMethods(unsupported)).toEqual([]);
36 expect(canPerformPullRequestAction(unsupported, "update-branch")).toBe(false);
37 const disallowed = detail({
38 viewerPermissions: { actions: ALL_PR_ACTIONS, updateMethods: ["rebase"] },
39 capabilities: { actions: ALL_PR_ACTIONS, mergeMethods: ["squash"], updateMethods: ["merge"] },
40 });
41 expect(canPerformPullRequestAction(disallowed, "update-branch")).toBe(false);
42 });
43 it("offers workflow approval only for reported pending approvals", () => {
44 expect(canPerformPullRequestAction(detail(), "approve-workflows")).toBe(false);
45 expect(
46 canPerformPullRequestAction(detail({ workflowApprovalsRequired: 2 }), "approve-workflows"),
47 ).toBe(true);
48 expect(
49 canPerformPullRequestAction(detail({ workflowApprovalsRequired: 0 }), "approve-workflows"),
50 ).toBe(false);
51 });
52 it("offers only merge strategies allowed by both the provider and repository", () => {
53 expect(allowedMergeMethods(detail())).toEqual(["merge", "squash"]);
54 const pr = detail({ capabilities: { actions: ALL_PR_ACTIONS, mergeMethods: ["merge"] } });
55 expect(allowedMergeMethods(pr)).toEqual(["merge"]);
56 expect(mergeSummary(pr, true)).toBe("Merge · auto-merge off");
57 });
58
59 it("requires both provider support and account permission", () => {
60 expect(
61 canPerformPullRequestAction(detail({ viewerPermissions: { actions: [] } }), "merge"),
62 ).toBe(false);
63 expect(
64 canPerformPullRequestAction(
65 detail({ capabilities: { actions: [], mergeMethods: ["merge"] } }),
66 "merge",
67 ),
68 ).toBe(false);
69 expect(mergeUnavailableReason(detail(), false)).toContain("read-only");
70 });
71
72 it("turns drafts into ready PRs before allowing a merge", () => {
73 const draft = detail({ isDraft: true });
74 expect(canPerformPullRequestAction(draft, "merge")).toBe(false);
75 expect(canPerformPullRequestAction(draft, "ready")).toBe(true);
76 expect(canPerformPullRequestAction(draft, "draft")).toBe(false);
77 expect(canPerformPullRequestAction(detail(), "ready")).toBe(false);
78 expect(canPerformPullRequestAction(detail(), "draft")).toBe(true);
79 });
80
81 it("blocks a conflicting PR's immediate merge and explains why", () => {
82 const pr = detail({ mergeability: "conflicting" });
83 expect(canPerformPullRequestAction(pr, "merge")).toBe(false);
84 expect(mergeUnavailableReason(pr, true)).toContain("conflicts");
85 expect(mergeSummary(pr, true)).toBe("Has conflicts");
86 });
87
88 it("offers the reverse lifecycle action after a close", () => {
89 const closed = detail({ state: "closed" });
90 expect(canPerformPullRequestAction(closed, "close")).toBe(false);
91 expect(canPerformPullRequestAction(closed, "merge")).toBe(false);
92 expect(canPerformPullRequestAction(closed, "reopen")).toBe(true);
93 expect(canPerformPullRequestAction(detail(), "reopen")).toBe(false);
94 });
95
96 it("does not reopen a merged PR, but can offer a revert", () => {
97 const merged = detail({ state: "merged" });
98 expect(canPerformPullRequestAction(merged, "reopen")).toBe(false);
99 expect(canPerformPullRequestAction(merged, "merge")).toBe(false);
100 expect(canPerformPullRequestAction(merged, "revert")).toBe(true);
101 expect(mergeSummary(merged, true)).toBe("Merged");
102 });
103
104 it("offers disable auto-merge only when armed, and enable only when explicitly off", () => {
105 const armed = detail({ autoMergeEnabled: true });
106 expect(canPerformPullRequestAction(armed, "enable-auto-merge")).toBe(false);
107 expect(canPerformPullRequestAction(armed, "disable-auto-merge")).toBe(true);
108 expect(canPerformPullRequestAction(detail(), "enable-auto-merge")).toBe(true);
109 expect(canPerformPullRequestAction(detail(), "disable-auto-merge")).toBe(false);
110 const unknown = detail({ autoMergeEnabled: undefined });
111 expect(canPerformPullRequestAction(unknown, "enable-auto-merge")).toBe(false);
112 expect(mergeSummary(unknown, true)).toBe("Squash");
113 });
114
115 it("offers deleting the source branch only once merged or closed on a host that supports it", () => {
116 const capabilities = { ...detail().capabilities, deleteBranch: true };
117 expect(canPerformPullRequestAction(detail({ capabilities }), "delete-branch")).toBe(false);
118 for (const state of ["merged", "closed"] as const) {
119 expect(canPerformPullRequestAction(detail({ state, capabilities }), "delete-branch")).toBe(
120 true,
121 );
122 expect(canPerformPullRequestAction(detail({ state }), "delete-branch")).toBe(false);
123 }
124 const refused = detail({ state: "merged", capabilities, viewerPermissions: { actions: [] } });
125 expect(canPerformPullRequestAction(refused, "delete-branch")).toBe(false);
126 });
127
128 it("cannot merge when the repository has disabled every offered strategy", () => {
129 const pr = detail({ mergeCapabilities: { merge: false, squash: false, rebase: false } });
130 expect(canPerformPullRequestAction(pr, "merge")).toBe(false);
131 expect(canPerformPullRequestAction(pr, "enable-auto-merge")).toBe(false);
132 expect(mergeUnavailableReason(pr, true)).toContain("no available merge methods");
133 });
134});