Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/ui/src/features/task-detail/BranchMismatchDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -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<Parameters<typeof BranchMismatchDialog>[0]>,
) {
const handlers = {
onSwitch: vi.fn(),
onContinue: vi.fn(),
onCancel: vi.fn(),
};
render(
<Theme>
<BranchMismatchDialog
open
linkedBranch="feat/foo"
currentBranch="main"
hasUncommittedChanges={false}
switchError={null}
{...handlers}
{...overrides}
/>
</Theme>,
);
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();
});
});
26 changes: 15 additions & 11 deletions packages/ui/src/features/task-detail/BranchMismatchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export function BranchMismatchDialog({
<AlertDialog.Root
open={open}
onOpenChange={(isOpen) => {
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();
}}
>
<AlertDialog.Content maxWidth="420px" size="2">
Expand Down Expand Up @@ -116,16 +118,18 @@ export function BranchMismatchDialog({
Continue anyway
</Button>

<AlertDialog.Action>
<Button
variant="solid"
size="1"
onClick={onSwitch}
loading={isSwitching}
>
Switch branch
</Button>
</AlertDialog.Action>
{/* 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. */}
<Button
variant="solid"
size="1"
onClick={onSwitch}
loading={isSwitching}
>
Switch branch
</Button>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
Expand Down
Loading