| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { |
| 3 | allowedMergeMethods, |
| 4 | allowedUpdateMethods, |
| 5 | canPerformPullRequestAction, |
| 6 | mergeSummary, |
| 7 | mergeUnavailableReason, |
| 8 | ALL_PR_ACTIONS, |
| 9 | } from "./pullRequestActions"; |
| 10 | |
| 11 | type Detail = Parameters<typeof allowedMergeMethods>[0]; |
| 12 | |
| 13 | function 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 | |
| 30 | describe("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 | }); |