feat(FN-1438): enable description expansion in task creation forms
- Fix textbox role selector to avoid matching the expand button in TaskForm - Enable description expansion in create mode for better task input experience - Update tests in NewTaskModal and TaskForm to cover the new behavior
This commit is contained in:
@@ -575,7 +575,7 @@ export function TaskForm({
|
|||||||
rows={mode === "edit" ? 8 : 5}
|
rows={mode === "edit" ? 8 : 5}
|
||||||
disabled={disabled || isRefining}
|
disabled={disabled || isRefining}
|
||||||
/>
|
/>
|
||||||
{mode === "edit" && !disabled && !isDescriptionExpanded && (
|
{!disabled && !isDescriptionExpanded && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm description-expand-btn"
|
className="btn btn-sm description-expand-btn"
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ vi.mock("lucide-react", () => ({
|
|||||||
ChevronDown: () => null,
|
ChevronDown: () => null,
|
||||||
X: () => null,
|
X: () => null,
|
||||||
Bot: () => null,
|
Bot: () => null,
|
||||||
|
Maximize2: () => null,
|
||||||
|
Minimize2: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the api module
|
// Mock the api module
|
||||||
@@ -72,7 +74,7 @@ describe("NewTaskModal", () => {
|
|||||||
renderNewTaskModal();
|
renderNewTaskModal();
|
||||||
|
|
||||||
expect(screen.getByText("New Task")).toBeTruthy();
|
expect(screen.getByText("New Task")).toBeTruthy();
|
||||||
expect(screen.getByLabelText(/Description/i)).toBeTruthy();
|
expect(screen.getByRole('textbox')).toBeTruthy();
|
||||||
expect(screen.getByRole("button", { name: "Plan" })).toBeTruthy();
|
expect(screen.getByRole("button", { name: "Plan" })).toBeTruthy();
|
||||||
expect(screen.getByRole("button", { name: "Subtask" })).toBeTruthy();
|
expect(screen.getByRole("button", { name: "Subtask" })).toBeTruthy();
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("focuses description textarea when modal opens", async () => {
|
it("focuses description textarea when modal opens", async () => {
|
||||||
renderNewTaskModal();
|
renderNewTaskModal();
|
||||||
|
|
||||||
const textarea = screen.getByLabelText(/Description/i);
|
const textarea = screen.getByRole('textbox');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(document.activeElement).toBe(textarea);
|
expect(document.activeElement).toBe(textarea);
|
||||||
});
|
});
|
||||||
@@ -123,7 +125,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("creates task with description when submitted", async () => {
|
it("creates task with description when submitted", async () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -140,7 +142,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("closes modal after successful creation", async () => {
|
it("closes modal after successful creation", async () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Test" } });
|
fireEvent.change(descTextarea, { target: { value: "Test" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -164,7 +166,7 @@ describe("NewTaskModal", () => {
|
|||||||
onCreateTask: vi.fn().mockResolvedValue({ id: "FN-042" }),
|
onCreateTask: vi.fn().mockResolvedValue({ id: "FN-042" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -177,7 +179,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("confirms before closing with dirty state", () => {
|
it("confirms before closing with dirty state", () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
fireEvent.change(descTextarea, { target: { value: "Test description" } });
|
||||||
|
|
||||||
// Mock confirm to return false (cancel)
|
// Mock confirm to return false (cancel)
|
||||||
@@ -202,7 +204,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("creates task with title undefined by default", async () => {
|
it("creates task with title undefined by default", async () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Only description" } });
|
fireEvent.change(descTextarea, { target: { value: "Only description" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -222,7 +224,7 @@ describe("NewTaskModal", () => {
|
|||||||
const onPlanningMode = vi.fn();
|
const onPlanningMode = vi.fn();
|
||||||
const { props } = renderNewTaskModal({ onPlanningMode });
|
const { props } = renderNewTaskModal({ onPlanningMode });
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Build a login system" } });
|
fireEvent.change(descTextarea, { target: { value: "Build a login system" } });
|
||||||
|
|
||||||
// Wait for models to load
|
// Wait for models to load
|
||||||
@@ -241,7 +243,7 @@ describe("NewTaskModal", () => {
|
|||||||
const onPlanningMode = vi.fn();
|
const onPlanningMode = vi.fn();
|
||||||
const { props } = renderNewTaskModal({ onPlanningMode });
|
const { props } = renderNewTaskModal({ onPlanningMode });
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Normal task" } });
|
fireEvent.change(descTextarea, { target: { value: "Normal task" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -269,7 +271,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("enables Create Task when description has content", () => {
|
it("enables Create Task when description has content", () => {
|
||||||
renderNewTaskModal();
|
renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Some text" } });
|
fireEvent.change(descTextarea, { target: { value: "Some text" } });
|
||||||
|
|
||||||
const createButton = screen.getByRole("button", { name: "Create Task" });
|
const createButton = screen.getByRole("button", { name: "Create Task" });
|
||||||
@@ -281,7 +283,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("omits modelPresetId from payload when in default mode", async () => {
|
it("omits modelPresetId from payload when in default mode", async () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
const descTextarea = screen.getByLabelText(/Description/i);
|
const descTextarea = screen.getByRole('textbox');
|
||||||
fireEvent.change(descTextarea, { target: { value: "Default mode task" } });
|
fireEvent.change(descTextarea, { target: { value: "Default mode task" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
@@ -315,7 +317,7 @@ describe("NewTaskModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Type a description
|
// Type a description
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Preset task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Preset task" } });
|
||||||
|
|
||||||
// Select the preset
|
// Select the preset
|
||||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||||
@@ -356,7 +358,7 @@ describe("NewTaskModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Type a description
|
// Type a description
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Custom task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Custom task" } });
|
||||||
|
|
||||||
// Select a preset first
|
// Select a preset first
|
||||||
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
const select = document.getElementById("model-preset") as HTMLSelectElement;
|
||||||
@@ -395,7 +397,7 @@ describe("NewTaskModal", () => {
|
|||||||
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Task with workflow step" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Task with workflow step" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -428,7 +430,7 @@ describe("NewTaskModal", () => {
|
|||||||
fireEvent.click(checkbox2);
|
fireEvent.click(checkbox2);
|
||||||
|
|
||||||
// Type description and submit
|
// Type description and submit
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Ordered task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Ordered task" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -467,7 +469,7 @@ describe("NewTaskModal", () => {
|
|||||||
fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002"));
|
fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002"));
|
||||||
|
|
||||||
// Type description and submit
|
// Type description and submit
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Reordered task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Reordered task" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -496,7 +498,7 @@ describe("NewTaskModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Don't interact with workflow steps at all
|
// Don't interact with workflow steps at all
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "No interaction task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "No interaction task" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -530,7 +532,7 @@ describe("NewTaskModal", () => {
|
|||||||
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Deselected task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Deselected task" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -564,7 +566,7 @@ describe("NewTaskModal", () => {
|
|||||||
// Don't modify the selection — just submit.
|
// Don't modify the selection — just submit.
|
||||||
// Since user hasn't explicitly changed steps, the explicitlySet flag is false,
|
// Since user hasn't explicitly changed steps, the explicitlySet flag is false,
|
||||||
// so the modal sends undefined (backend applies its own defaults)
|
// so the modal sends undefined (backend applies its own defaults)
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Auto-selected task" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Auto-selected task" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -648,7 +650,7 @@ describe("NewTaskModal", () => {
|
|||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
// Type description
|
// Type description
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Task with agent" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Task with agent" } });
|
||||||
|
|
||||||
// Open agent picker and select agent
|
// Open agent picker and select agent
|
||||||
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
||||||
@@ -674,7 +676,7 @@ describe("NewTaskModal", () => {
|
|||||||
it("omits assignedAgentId from payload when no agent is selected", async () => {
|
it("omits assignedAgentId from payload when no agent is selected", async () => {
|
||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Task without agent" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Task without agent" } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||||
|
|
||||||
@@ -696,7 +698,7 @@ describe("NewTaskModal", () => {
|
|||||||
const { props } = renderNewTaskModal();
|
const { props } = renderNewTaskModal();
|
||||||
|
|
||||||
// Type description
|
// Type description
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Task with agent" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Task with agent" } });
|
||||||
|
|
||||||
// Open agent picker and select agent
|
// Open agent picker and select agent
|
||||||
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
||||||
@@ -766,7 +768,7 @@ describe("NewTaskModal", () => {
|
|||||||
renderNewTaskModal();
|
renderNewTaskModal();
|
||||||
|
|
||||||
// Type description
|
// Type description
|
||||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Task with agent" } });
|
fireEvent.change(screen.getByRole('textbox'), { target: { value: "Task with agent" } });
|
||||||
|
|
||||||
// Open agent picker and select agent
|
// Open agent picker and select agent
|
||||||
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
fireEvent.click(screen.getByTestId("new-task-agent-button"));
|
||||||
|
|||||||
@@ -268,13 +268,34 @@ describe("TaskForm", () => {
|
|||||||
expect(textarea.getAttribute("rows")).toBe("5");
|
expect(textarea.getAttribute("rows")).toBe("5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render expand button in create mode", () => {
|
it("renders expand button in create mode and toggles fullscreen", () => {
|
||||||
renderTaskForm({
|
const { container } = renderTaskForm({
|
||||||
|
mode: "create",
|
||||||
|
description: "Some description to expand",
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandButton = screen.getByRole("button", { name: "Expand description" });
|
||||||
|
expect(expandButton).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(expandButton);
|
||||||
|
expect(container.querySelector(".description-with-refine.description--fullscreen")).toBeTruthy();
|
||||||
|
expect(screen.getByRole("button", { name: "Collapse description" })).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Collapse description" }));
|
||||||
|
expect(container.querySelector(".description-with-refine.description--fullscreen")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("collapses fullscreen description editor on Escape in create mode", () => {
|
||||||
|
const { container } = renderTaskForm({
|
||||||
mode: "create",
|
mode: "create",
|
||||||
description: "Some description",
|
description: "Some description",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: "Expand description" })).toBeNull();
|
fireEvent.click(screen.getByRole("button", { name: "Expand description" }));
|
||||||
|
const textarea = screen.getByRole("textbox", { name: /Description/i });
|
||||||
|
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||||
|
|
||||||
|
expect(container.querySelector(".description-with-refine.description--fullscreen")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("debounces auto-save in edit mode and calls onAutoSaveDescription after 1.5s", async () => {
|
it("debounces auto-save in edit mode and calls onAutoSaveDescription after 1.5s", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user