diff --git a/packages/ui/src/features/task-detail/BranchMismatchDialog.test.tsx b/packages/ui/src/features/task-detail/BranchMismatchDialog.test.tsx new file mode 100644 index 0000000000..6c2d317ec2 --- /dev/null +++ b/packages/ui/src/features/task-detail/BranchMismatchDialog.test.tsx @@ -0,0 +1,77 @@ +import { Theme } from "@radix-ui/themes"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { BranchMismatchDialog } from "./BranchMismatchDialog"; + +function renderDialog( + overrides?: Partial[0]>, +) { + const handlers = { + onSwitch: vi.fn(), + onContinue: vi.fn(), + onCancel: vi.fn(), + }; + render( + + + , + ); + return handlers; +} + +describe("BranchMismatchDialog", () => { + // The Switch case is the regression: the button was wrapped in + // AlertDialog.Action, whose auto-close fired onOpenChange -> onCancel, + // discarding the pending message so it was never sent after the checkout + // succeeded. + it.each([ + { button: "Switch branch", fires: "onSwitch" }, + { button: "Continue anyway", fires: "onContinue" }, + { button: "Cancel", fires: "onCancel" }, + ] as const)( + "clicking $button fires $fires only", + async ({ button, fires }) => { + const user = userEvent.setup(); + const handlers = renderDialog(); + + await user.click(screen.getByRole("button", { name: button })); + + for (const [name, handler] of Object.entries(handlers)) { + expect(handler, name).toHaveBeenCalledTimes(name === fires ? 1 : 0); + } + }, + ); + + it("Escape closes and fires onCancel when idle", async () => { + const user = userEvent.setup(); + const { onCancel } = renderDialog(); + + await user.keyboard("{Escape}"); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it("Escape does not fire onCancel while switching", async () => { + const user = userEvent.setup(); + const { onCancel } = renderDialog({ isSwitching: true }); + + await user.keyboard("{Escape}"); + + expect(onCancel).not.toHaveBeenCalled(); + }); + + it("shows the switch error", () => { + renderDialog({ switchError: "dirty worktree" }); + + expect(screen.getByText("dirty worktree")).toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/features/task-detail/BranchMismatchDialog.tsx b/packages/ui/src/features/task-detail/BranchMismatchDialog.tsx index 23c5cb5b78..04b343cae1 100644 --- a/packages/ui/src/features/task-detail/BranchMismatchDialog.tsx +++ b/packages/ui/src/features/task-detail/BranchMismatchDialog.tsx @@ -55,7 +55,9 @@ export function BranchMismatchDialog({ { - if (!isOpen) onCancel(); + // While the checkout runs the dialog must stay up: onCancel discards + // the pending message the switch handler sends on success. + if (!isOpen && !isSwitching) onCancel(); }} > @@ -116,16 +118,18 @@ export function BranchMismatchDialog({ Continue anyway - - - + {/* Not an AlertDialog.Action: Action closes the dialog on click, + which fires onOpenChange -> onCancel and discards the pending + message before the checkout finishes. The dialog closes (or + shows the error) when the mutation settles. */} +