| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; |
| 2 | import { |
| 3 | EnvironmentId, |
| 4 | ProjectId, |
| 5 | ThreadId, |
| 6 | type ScopedThreadRef, |
| 7 | type PullRequestDetailView, |
| 8 | type ThreadPullRequestLink, |
| 9 | } from "@t3tools/contracts"; |
| 10 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; |
| 11 | import { act, type ReactNode, type ReactElement, type ComponentProps } from "react"; |
| 12 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 13 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 14 | import { DraftId, useComposerDraftStore } from "~/composerDraftStore"; |
| 15 | |
| 16 | const { newThread, prepareThread, refresh, browserState, Wrapper, Trigger } = vi.hoisted(() => ({ |
| 17 | newThread: vi.fn(), |
| 18 | prepareThread: vi.fn(), |
| 19 | refresh: vi.fn(), |
| 20 | browserState: { configured: false }, |
| 21 | Wrapper: ({ children }: { children?: ReactNode }) => children, |
| 22 | Trigger: ({ children, render }: { children?: ReactNode; render?: ReactElement }) => ( |
| 23 | <> |
| 24 | {render} |
| 25 | {children} |
| 26 | </> |
| 27 | ), |
| 28 | })); |
| 29 | vi.mock("~/state/session", async (original) => ({ |
| 30 | ...(await original<typeof import("~/state/session")>()), |
| 31 | useEnvironmentScope: () => true, |
| 32 | readEnvironmentScope: () => true, |
| 33 | })); |
| 34 | vi.mock("@effect/atom-react", () => ({ useAtomValue: () => [] })); |
| 35 | vi.mock("~/state/server", () => ({ primaryServerKeybindingsAtom: {} })); |
| 36 | vi.mock("~/state/entities", () => ({ |
| 37 | useProjects: () => [], |
| 38 | useServerConfigs: () => |
| 39 | browserState.configured |
| 40 | ? new Map([ |
| 41 | [ |
| 42 | "env-1", |
| 43 | { mergebot: { origin: "https://mergebot.example", repositories: ["Owner/Repo"] } }, |
| 44 | ], |
| 45 | ]) |
| 46 | : new Map(), |
| 47 | })); |
| 48 | vi.mock("./MergebotBrowserPanel", () => ({ |
| 49 | MergebotBrowserPanel: ({ threadRef, url }: { threadRef: ScopedThreadRef; url: string }) => ( |
| 50 | <div data-mergebot-owner={threadRef.threadId} data-mergebot-url={url} /> |
| 51 | ), |
| 52 | })); |
| 53 | vi.mock("~/state/environments", () => ({ |
| 54 | useEnvironment: () => null, |
| 55 | useEnvironments: () => ({ environments: [] }), |
| 56 | usePrimaryEnvironmentId: () => EnvironmentId.make("env-1"), |
| 57 | })); |
| 58 | vi.mock("~/hooks/useSettings", () => ({ |
| 59 | useEnvironmentSettings: () => undefined, |
| 60 | useClientSettings: (select: (settings: typeof DEFAULT_CLIENT_SETTINGS) => unknown) => |
| 61 | select(DEFAULT_CLIENT_SETTINGS), |
| 62 | })); |
| 63 | vi.mock("~/hooks/useLiveRefresh", () => ({ useLiveRefresh: () => {} })); |
| 64 | vi.mock("~/hooks/useHandleNewThread", () => ({ useNewThreadHandler: () => newThread })); |
| 65 | vi.mock("~/lib/sourceControlActions", () => ({ |
| 66 | usePreparePullRequestThreadAction: () => ({ run: prepareThread, isAllowed: true, error: null }), |
| 67 | })); |
| 68 | vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() })); |
| 69 | vi.mock("~/state/pullRequests", async (importOriginal) => ({ |
| 70 | ...(await importOriginal<typeof import("~/state/pullRequests")>()), |
| 71 | pullRequestEnvironment: { |
| 72 | detail: () => "detail", |
| 73 | activity: () => "activity", |
| 74 | // The header menu reads the stored guide to label "Write guide" or "Rewrite guide". |
| 75 | reviewGuide: () => "review-guide", |
| 76 | generateReviewGuide: { permissionAtom: () => "generate-review-guide" }, |
| 77 | }, |
| 78 | usePullRequestTurnRefresh: () => 0, |
| 79 | useSharedPullRequestSummary: () => null, |
| 80 | })); |
| 81 | vi.mock("~/state/vcs", () => ({ vcsEnvironment: { listRefs: () => null } })); |
| 82 | vi.mock("~/state/query", () => ({ |
| 83 | useEnvironmentQuery: (query: string) => ({ |
| 84 | data: query === "detail" ? detail : null, |
| 85 | isPending: false, |
| 86 | isSuccess: true, |
| 87 | error: null, |
| 88 | refresh, |
| 89 | }), |
| 90 | })); |
| 91 | vi.mock("~/state/usePullRequestStack", () => ({ |
| 92 | usePullRequestStack: () => ({ |
| 93 | data: null, |
| 94 | isSuccess: true, |
| 95 | isPending: false, |
| 96 | error: null, |
| 97 | refresh, |
| 98 | }), |
| 99 | })); |
| 100 | vi.mock("../ui/toast", () => ({ toastManager: { add: vi.fn(), update: vi.fn() } })); |
| 101 | vi.mock("../ui/tooltip", () => ({ |
| 102 | TooltipProvider: Wrapper, |
| 103 | Tooltip: Wrapper, |
| 104 | TooltipTrigger: Trigger, |
| 105 | TooltipPopup: () => null, |
| 106 | })); |
| 107 | vi.mock("../ui/menu", () => ({ |
| 108 | Menu: Wrapper, |
| 109 | MenuPopup: Wrapper, |
| 110 | MenuTrigger: Trigger, |
| 111 | MenuItem: "button", |
| 112 | MenuRadioGroup: Wrapper, |
| 113 | MenuRadioItem: "button", |
| 114 | MenuSeparator: () => null, |
| 115 | MenuShortcut: () => null, |
| 116 | })); |
| 117 | vi.mock("../ui/alert-dialog", () => ({ |
| 118 | AlertDialog: () => null, |
| 119 | AlertDialogPopup: Wrapper, |
| 120 | AlertDialogHeader: Wrapper, |
| 121 | AlertDialogTitle: Wrapper, |
| 122 | AlertDialogDescription: Wrapper, |
| 123 | AlertDialogFooter: Wrapper, |
| 124 | AlertDialogClose: Wrapper, |
| 125 | })); |
| 126 | vi.mock("./PullRequestMarkdown", () => ({ |
| 127 | PullRequestMarkdownContext: Wrapper, |
| 128 | PullRequestMarkdown: () => null, |
| 129 | })); |
| 130 | vi.mock("~/browser/useOpenLink", () => ({ useOpenLink: () => vi.fn() })); |
| 131 | vi.mock("./PullRequestThreadLinks", () => ({ PullRequestThreadLinks: () => null })); |
| 132 | vi.mock("./PullRequestSummaryTab", () => ({ |
| 133 | PullRequestSummaryTab: ({ |
| 134 | onFixFinding, |
| 135 | scrollerRef, |
| 136 | }: ComponentProps<typeof import("./PullRequestSummaryTab").PullRequestSummaryTab>) => ( |
| 137 | <div ref={scrollerRef} data-test-scroll="summary"> |
| 138 | <button |
| 139 | onClick={() => |
| 140 | onFixFinding?.({ |
| 141 | kind: "check", |
| 142 | check: { name: "Unit tests", status: "failure", description: "Test failed", url: null }, |
| 143 | }) |
| 144 | } |
| 145 | > |
| 146 | Fix check |
| 147 | </button> |
| 148 | </div> |
| 149 | ), |
| 150 | })); |
| 151 | // The guide renders diffs, which start the diff worker; this suite never opens it. |
| 152 | vi.mock("./PullRequestGuideTab", () => ({ PullRequestGuideTab: () => null })); |
| 153 | vi.mock("./PullRequestCodeTab", () => ({ |
| 154 | default: ({ |
| 155 | onAddToAgentSelection, |
| 156 | scrollerRef, |
| 157 | }: ComponentProps<typeof import("./PullRequestCodeTab").default>) => ( |
| 158 | <div ref={scrollerRef} data-test-scroll="code"> |
| 159 | <button |
| 160 | onClick={() => |
| 161 | onAddToAgentSelection?.({ |
| 162 | request: "Fix this line", |
| 163 | comment: { |
| 164 | id: "note-1", |
| 165 | sectionId: "file:a.ts", |
| 166 | sectionTitle: "a.ts", |
| 167 | filePath: "a.ts", |
| 168 | startIndex: 0, |
| 169 | endIndex: 0, |
| 170 | rangeLabel: "L1", |
| 171 | text: "Please fix", |
| 172 | diff: "+broken()", |
| 173 | }, |
| 174 | }) |
| 175 | } |
| 176 | > |
| 177 | Add to agent |
| 178 | </button> |
| 179 | </div> |
| 180 | ), |
| 181 | })); |
| 182 | |
| 183 | import { PullRequestDetailPanel } from "./PullRequestDetailPanel"; |
| 184 | import { pullRequestPanelContext } from "./pullRequestDetail.logic"; |
| 185 | |
| 186 | const detail: PullRequestDetailView = { |
| 187 | provider: SourceControlProviderKind.make("github"), |
| 188 | projectId: ProjectId.make("project"), |
| 189 | projectTitle: "Project", |
| 190 | workspaceRoot: "/workspace", |
| 191 | repository: "owner/repo", |
| 192 | number: 1, |
| 193 | title: "Test pull request", |
| 194 | body: "Original description", |
| 195 | url: "https://github.com/owner/repo/pull/1", |
| 196 | author: { login: "author", name: null, avatarUrl: null }, |
| 197 | viewer: "author", |
| 198 | state: "open", |
| 199 | isDraft: false, |
| 200 | mergeability: "conflicting", |
| 201 | additions: 1, |
| 202 | deletions: 0, |
| 203 | changedFiles: 1, |
| 204 | headBranch: "feature", |
| 205 | baseBranch: "main", |
| 206 | createdAt: "2026-09-01T00:00:00Z", |
| 207 | updatedAt: "2026-09-01T00:00:00Z", |
| 208 | mergedAt: null, |
| 209 | closedAt: null, |
| 210 | reviewers: [], |
| 211 | labels: [], |
| 212 | checks: [{ name: "Unit tests", status: "success", description: null, url: null }], |
| 213 | comments: [], |
| 214 | commentCount: 0, |
| 215 | commentsTruncated: false, |
| 216 | reviewThreads: [], |
| 217 | commits: [], |
| 218 | mergeCapabilities: { merge: false, squash: false, rebase: false }, |
| 219 | capabilities: { |
| 220 | diff: true, |
| 221 | comment: false, |
| 222 | search: true, |
| 223 | actions: [], |
| 224 | mergeMethods: [], |
| 225 | review: { inlineComment: false, reply: false, resolve: false, verdicts: [] }, |
| 226 | reviewers: { request: false, listCandidates: false }, |
| 227 | edit: { changeRequest: true, comment: false }, |
| 228 | }, |
| 229 | viewerPermissions: { |
| 230 | actions: [], |
| 231 | comment: false, |
| 232 | resolve: false, |
| 233 | verdicts: [], |
| 234 | requestReviewers: false, |
| 235 | }, |
| 236 | }; |
| 237 | |
| 238 | const threadRef: ScopedThreadRef = { |
| 239 | environmentId: EnvironmentId.make("env-1"), |
| 240 | threadId: ThreadId.make("thread-1"), |
| 241 | }; |
| 242 | const draftId = DraftId.make("draft-1"); |
| 243 | const newDraftId = DraftId.make("new-draft"); |
| 244 | let renderer: ReactTestRenderer; |
| 245 | |
| 246 | beforeEach(() => { |
| 247 | browserState.configured = false; |
| 248 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 249 | vi.stubGlobal("window", { addEventListener: vi.fn(), removeEventListener: vi.fn() }); |
| 250 | useComposerDraftStore.setState({ draftsByThreadKey: {} }); |
| 251 | newThread |
| 252 | .mockReset() |
| 253 | .mockResolvedValue({ draftId: newDraftId, threadId: ThreadId.make("new-thread") }); |
| 254 | prepareThread.mockReset().mockResolvedValue({ |
| 255 | _tag: "Success", |
| 256 | value: { branch: "feature", worktreePath: "/workspace/pr" }, |
| 257 | }); |
| 258 | }); |
| 259 | afterEach(() => { |
| 260 | act(() => renderer?.unmount()); |
| 261 | vi.unstubAllGlobals(); |
| 262 | }); |
| 263 | |
| 264 | async function click(label: string) { |
| 265 | const button = renderer.root |
| 266 | .findAllByType("button") |
| 267 | .find( |
| 268 | (node) => |
| 269 | node.props["aria-label"] === label || |
| 270 | node.findAll((child) => child.children.includes(label)).length > 0, |
| 271 | ); |
| 272 | expect(button, label).toBeDefined(); |
| 273 | await act(async () => |
| 274 | button!.props.onClick({ |
| 275 | nativeEvent: new Event("click"), |
| 276 | preventDefault() {}, |
| 277 | stopPropagation() {}, |
| 278 | }), |
| 279 | ); |
| 280 | } |
| 281 | |
| 282 | const actions = [ |
| 283 | "Resolve conflicts", |
| 284 | "Ask a question", |
| 285 | "Explain this PR", |
| 286 | "Fix findings in this thread", |
| 287 | "Fix check", |
| 288 | "Add to agent", |
| 289 | ]; |
| 290 | |
| 291 | // The surface ChatView opens for `detail`, and the thread states it can be opened beside. The |
| 292 | // context prop is derived here the way ChatView derives it, so a wrong answer from the thread's |
| 293 | // link list fails these cases rather than only a hand-picked prop. |
| 294 | const surface = { projectId: detail.projectId, repository: detail.repository, number: 1 }; |
| 295 | const link = (number: number, source: ThreadPullRequestLink["source"]): ThreadPullRequestLink => ({ |
| 296 | host: "github.com", |
| 297 | repository: detail.repository, |
| 298 | number, |
| 299 | url: `https://github.com/${detail.repository}/pull/${number}`, |
| 300 | source, |
| 301 | linkedAt: "2026-09-01T00:00:00Z", |
| 302 | snapshot: null, |
| 303 | stack: null, |
| 304 | }); |
| 305 | const stackThread = { |
| 306 | projectId: detail.projectId, |
| 307 | pullRequests: [link(3, "manual"), link(2, "stack"), link(1, "stack")], |
| 308 | // The server's one-slot field names the top layer; the panel shows the bottom one. |
| 309 | linkedPullRequest: { ...surface, number: 3, url: link(3, "manual").url }, |
| 310 | }; |
| 311 | const unrelatedThread = { |
| 312 | projectId: detail.projectId, |
| 313 | pullRequests: [link(9, "created")], |
| 314 | linkedPullRequest: { ...surface, number: 9, url: link(9, "created").url }, |
| 315 | }; |
| 316 | |
| 317 | describe.each([ |
| 318 | ["own PR, a lower layer of the thread's stack", stackThread, threadRef], |
| 319 | ["another PR beside the current thread", unrelatedThread, threadRef], |
| 320 | ["PR beside an unsent draft", null, draftId], |
| 321 | ["standalone PR page", null, undefined], |
| 322 | ] as const)("%s", (_name, thread, target) => { |
| 323 | const context = thread ? pullRequestPanelContext(thread, surface) : "page"; |
| 324 | |
| 325 | function render() { |
| 326 | renderer = create( |
| 327 | <PullRequestDetailPanel |
| 328 | environmentId={threadRef.environmentId} |
| 329 | reference={detail} |
| 330 | context={context} |
| 331 | {...(target ? { composerDraftTarget: target, threadRef } : {})} |
| 332 | shortcutsEnabled={false} |
| 333 | getShortcutContext={() => ({ |
| 334 | terminalFocus: false, |
| 335 | terminalOpen: false, |
| 336 | previewFocus: false, |
| 337 | previewOpen: false, |
| 338 | isWeb: true, |
| 339 | isDesktop: false, |
| 340 | })} |
| 341 | />, |
| 342 | ); |
| 343 | } |
| 344 | |
| 345 | it(`${thread === stackThread ? "hides" : "offers"} the checkout`, async () => { |
| 346 | await act(async () => render()); |
| 347 | const checkout = renderer.root |
| 348 | .findAllByType("button") |
| 349 | .filter((node) => node.props["aria-label"] === "Check out"); |
| 350 | expect(checkout).toHaveLength(thread === stackThread ? 0 : 1); |
| 351 | }); |
| 352 | |
| 353 | it.each(actions)("%s writes to the correct composer", async (action) => { |
| 354 | if (target) useComposerDraftStore.getState().setPrompt(target, "Keep my draft"); |
| 355 | await act(async () => render()); |
| 356 | if (action === "Add to agent") await click("Code"); |
| 357 | await click(target ? action : action.replace("in this thread", "in a thread")); |
| 358 | const draft = useComposerDraftStore.getState().getComposerDraft(target ?? newDraftId); |
| 359 | if (action === "Resolve conflicts") expect(draft?.prompt).toContain("resolve every conflict"); |
| 360 | else if (action === "Fix check") expect(draft?.prompt).toContain("Fix the failing check"); |
| 361 | else if (action.startsWith("Fix findings")) |
| 362 | expect(draft?.prompt).toContain("Fix the actionable findings"); |
| 363 | else expect(draft?.reviewComments?.length).toBeGreaterThan(0); |
| 364 | if (action === "Add to agent") { |
| 365 | expect(draft?.prompt).toContain("Fix this line"); |
| 366 | expect(draft?.reviewComments).toEqual( |
| 367 | expect.arrayContaining([expect.objectContaining({ id: "note-1", diff: "+broken()" })]), |
| 368 | ); |
| 369 | } |
| 370 | if (target) { |
| 371 | expect(draft?.prompt).toContain("Keep my draft"); |
| 372 | expect(newThread).not.toHaveBeenCalled(); |
| 373 | expect(prepareThread).not.toHaveBeenCalled(); |
| 374 | } else { |
| 375 | expect(newThread).toHaveBeenCalled(); |
| 376 | } |
| 377 | }); |
| 378 | }); |
| 379 | |
| 380 | it("changes the title only for the active tab's main scroller", async () => { |
| 381 | const scrollers = new Map<string, { scrollTop: number; scrollHeight: number }>(); |
| 382 | await act(async () => { |
| 383 | renderer = create( |
| 384 | <PullRequestDetailPanel |
| 385 | environmentId={threadRef.environmentId} |
| 386 | reference={detail} |
| 387 | context="page" |
| 388 | shortcutsEnabled={false} |
| 389 | getShortcutContext={() => ({ |
| 390 | terminalFocus: false, |
| 391 | terminalOpen: false, |
| 392 | previewFocus: false, |
| 393 | previewOpen: false, |
| 394 | isWeb: true, |
| 395 | isDesktop: false, |
| 396 | })} |
| 397 | />, |
| 398 | { |
| 399 | createNodeMock: (element) => { |
| 400 | const props = element.props as Record<string, unknown>; |
| 401 | const name = props["data-test-scroll"] as string | undefined; |
| 402 | if (name) { |
| 403 | const scroller = { scrollTop: 0, scrollHeight: 300 }; |
| 404 | scrollers.set(name, scroller); |
| 405 | return scroller; |
| 406 | } |
| 407 | return { scrollHeight: props.inert === false ? 100 : 20 }; |
| 408 | }, |
| 409 | }, |
| 410 | ); |
| 411 | }); |
| 412 | |
| 413 | const mainScroll = renderer.root.findByProps({ |
| 414 | className: "relative flex min-h-0 flex-1 flex-col overflow-hidden", |
| 415 | }); |
| 416 | const titleFold = () => { |
| 417 | let node = renderer.root.findByType("h1"); |
| 418 | while (typeof node.props.inert !== "boolean") node = node.parent!; |
| 419 | return node; |
| 420 | }; |
| 421 | const summary = scrollers.get("summary")!; |
| 422 | const nested = { scrollTop: 200 }; |
| 423 | await act(async () => mainScroll.props.onScrollCapture({ target: nested })); |
| 424 | expect(titleFold().props.inert).toBe(false); |
| 425 | summary.scrollTop = 180; |
| 426 | nested.scrollTop = 0; |
| 427 | await act(async () => { |
| 428 | mainScroll.props.onScrollCapture({ target: summary }); |
| 429 | mainScroll.props.onScrollCapture({ target: nested }); |
| 430 | }); |
| 431 | expect(titleFold().props.inert).toBe(true); |
| 432 | expect(summary.scrollTop).toBe(100); |
| 433 | expect(nested.scrollTop).toBe(0); |
| 434 | |
| 435 | await act(async () => mainScroll.props.onScrollCapture({ target: { scrollTop: 0 } })); |
| 436 | expect(titleFold().props.inert).toBe(true); |
| 437 | summary.scrollTop = 0; |
| 438 | await act(async () => mainScroll.props.onScrollCapture({ target: summary })); |
| 439 | expect(titleFold().props.inert).toBe(false); |
| 440 | |
| 441 | await click("Code"); |
| 442 | const code = scrollers.get("code")!; |
| 443 | summary.scrollTop = 180; |
| 444 | await act(async () => mainScroll.props.onScrollCapture({ target: summary })); |
| 445 | expect(titleFold().props.inert).toBe(false); |
| 446 | code.scrollTop = 180; |
| 447 | await act(async () => mainScroll.props.onScrollCapture({ target: code })); |
| 448 | expect(titleFold().props.inert).toBe(true); |
| 449 | expect(code.scrollTop).toBe(100); |
| 450 | await click("Summary"); |
| 451 | expect(titleFold().props.inert).toBe(false); |
| 452 | await click("Code"); |
| 453 | expect(titleFold().props.inert).toBe(true); |
| 454 | code.scrollTop = 0; |
| 455 | await act(async () => mainScroll.props.onScrollCapture({ target: code })); |
| 456 | expect(titleFold().props.inert).toBe(false); |
| 457 | }); |
| 458 | |
| 459 | it.each([true, false])( |
| 460 | "opens the actual Mergebot UI before native detail hooks (chat owner: %s)", |
| 461 | async (owned) => { |
| 462 | browserState.configured = true; |
| 463 | await act(async () => { |
| 464 | renderer = create( |
| 465 | <PullRequestDetailPanel |
| 466 | environmentId={threadRef.environmentId} |
| 467 | reference={detail} |
| 468 | threadRef={owned ? threadRef : null} |
| 469 | shortcutsEnabled={false} |
| 470 | getShortcutContext={() => ({ |
| 471 | terminalFocus: false, |
| 472 | terminalOpen: false, |
| 473 | previewFocus: false, |
| 474 | previewOpen: false, |
| 475 | isWeb: true, |
| 476 | isDesktop: false, |
| 477 | })} |
| 478 | />, |
| 479 | ); |
| 480 | }); |
| 481 | expect( |
| 482 | renderer.root.findByProps({ |
| 483 | "data-mergebot-url": "https://mergebot.example/Owner/Repo/pull/1", |
| 484 | }).props["data-mergebot-owner"], |
| 485 | ).toBe(owned ? "thread-1" : "mergebot-browser:owner/repo:1"); |
| 486 | expect(renderer.root.findAllByType("h1")).toHaveLength(0); |
| 487 | }, |
| 488 | ); |
| 489 | |
| 490 | it.each(["back", "close", "both"])( |
| 491 | "preserves provided %s app navigation above Mergebot", |
| 492 | async (controls) => { |
| 493 | browserState.configured = true; |
| 494 | const back = vi.fn(); |
| 495 | const close = vi.fn(); |
| 496 | await act(async () => { |
| 497 | renderer = create( |
| 498 | <PullRequestDetailPanel |
| 499 | environmentId={threadRef.environmentId} |
| 500 | reference={detail} |
| 501 | threadRef={threadRef} |
| 502 | {...(controls !== "close" ? { onBack: back } : {})} |
| 503 | {...(controls !== "back" ? { onClose: close } : {})} |
| 504 | shortcutsEnabled={false} |
| 505 | getShortcutContext={() => ({ |
| 506 | terminalFocus: false, |
| 507 | terminalOpen: false, |
| 508 | previewFocus: false, |
| 509 | previewOpen: false, |
| 510 | isWeb: true, |
| 511 | isDesktop: false, |
| 512 | })} |
| 513 | />, |
| 514 | ); |
| 515 | }); |
| 516 | if (controls !== "close") { |
| 517 | await click("Back to this thread's pull requests"); |
| 518 | expect(back).toHaveBeenCalledOnce(); |
| 519 | } else |
| 520 | expect( |
| 521 | renderer.root.findAllByProps({ "aria-label": "Back to this thread's pull requests" }), |
| 522 | ).toHaveLength(0); |
| 523 | if (controls !== "back") { |
| 524 | await click("Collapse pull request panel"); |
| 525 | expect(close).toHaveBeenCalledOnce(); |
| 526 | } else |
| 527 | expect( |
| 528 | renderer.root.findAllByProps({ "aria-label": "Collapse pull request panel" }), |
| 529 | ).toHaveLength(0); |
| 530 | expect( |
| 531 | renderer.root.findByProps({ |
| 532 | "data-mergebot-url": "https://mergebot.example/Owner/Repo/pull/1", |
| 533 | }), |
| 534 | ).toBeDefined(); |
| 535 | }, |
| 536 | ); |