feat: implement ReasonPrompt component with quick tags
- Create ReasonPrompt dialog for capturing optional reasons - Add quick tag buttons (Blocking, Urgent, Context switch, etc.) - Support keyboard navigation (Escape to cancel) - Handle text input with trimming and null for empty - Different titles for different actions (set_focus, defer, skip) - All 10 tests pass Closes bd-2p0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
158
tests/components/ReasonPrompt.test.tsx
Normal file
158
tests/components/ReasonPrompt.test.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Tests for ReasonPrompt component.
|
||||
*
|
||||
* TDD: These tests define the expected behavior before implementation.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ReasonPrompt } from "@/components/ReasonPrompt";
|
||||
|
||||
describe("ReasonPrompt", () => {
|
||||
const defaultProps = {
|
||||
action: "set_focus",
|
||||
itemTitle: "Review MR !847",
|
||||
onSubmit: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders with action context", () => {
|
||||
render(<ReasonPrompt {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Setting focus to.*Review MR !847/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("captures text input", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
await user.type(
|
||||
screen.getByRole("textbox"),
|
||||
"Sarah pinged me, she is blocked"
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: "Sarah pinged me, she is blocked",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("allows selecting quick tags", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /blocking/i }));
|
||||
await user.click(screen.getByRole("button", { name: /urgent/i }));
|
||||
await user.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: expect.arrayContaining(["blocking", "urgent"]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("toggles tag off when clicked again", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
const blockingTag = screen.getByRole("button", { name: /blocking/i });
|
||||
|
||||
// Select then deselect
|
||||
await user.click(blockingTag);
|
||||
await user.click(blockingTag);
|
||||
await user.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("allows skipping reason", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /skip reason/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
reason: null,
|
||||
tags: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("trims whitespace from reason", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
await user.type(screen.getByRole("textbox"), " spaced reason ");
|
||||
await user.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: "spaced reason",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("treats empty reason as null", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
||||
// Just click confirm without typing
|
||||
await user.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels on Escape key", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCancel = vi.fn();
|
||||
render(<ReasonPrompt {...defaultProps} onCancel={onCancel} />);
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
expect(onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("displays all quick tag options", () => {
|
||||
render(<ReasonPrompt {...defaultProps} />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /blocking/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /urgent/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /context switch/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /energy/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows different titles for different actions", () => {
|
||||
const { rerender } = render(
|
||||
<ReasonPrompt {...defaultProps} action="defer" itemTitle="Issue #42" />
|
||||
);
|
||||
expect(screen.getByText(/Deferring.*Issue #42/i)).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ReasonPrompt {...defaultProps} action="skip" itemTitle="MR !100" />
|
||||
);
|
||||
expect(screen.getByText(/Skipping.*MR !100/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user