132bf61ee1
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
229 lines
7.6 KiB
TypeScript
229 lines
7.6 KiB
TypeScript
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { TaskTopBarTitle } from "./task-top-bar-title";
|
|
|
|
const mockRename = vi.hoisted(() => vi.fn(() => Promise.resolve()));
|
|
|
|
vi.mock("@kandev/ui/tooltip", () => ({
|
|
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
TooltipContent: ({ children }: { children: React.ReactNode }) => (
|
|
<div role="tooltip">{children}</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/hooks/use-task-actions", () => ({
|
|
useTaskActions: () => ({ renameTaskById: mockRename }),
|
|
}));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
mockRename.mockClear();
|
|
});
|
|
|
|
function getTitle() {
|
|
return screen.getByTestId("task-topbar-title");
|
|
}
|
|
|
|
const RENAME_INPUT_TEST_ID = "task-title-rename-input";
|
|
|
|
function queryInput() {
|
|
return screen.queryByTestId(RENAME_INPUT_TEST_ID);
|
|
}
|
|
|
|
function getInput() {
|
|
return screen.getByTestId(RENAME_INPUT_TEST_ID) as HTMLInputElement;
|
|
}
|
|
|
|
function startEditing() {
|
|
fireEvent.doubleClick(getTitle());
|
|
return getInput();
|
|
}
|
|
|
|
describe("TaskTopBarTitle — idle state", () => {
|
|
it("renders the title as the interactive title crumb when idle", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
expect(getTitle()).toBeTruthy();
|
|
expect(queryInput()).toBeNull();
|
|
// Not aria-disabled, so pointer-actionability checks treat it as interactive.
|
|
expect(getTitle().getAttribute("aria-disabled")).toBe("false");
|
|
// Keyboard-operable: reachable via Tab.
|
|
expect(getTitle().getAttribute("tabindex")).toBe("0");
|
|
// The crumb wrapper carries the breadcrumb semantics; this element is the
|
|
// control, so it needs a role of its own or AT hears only plain text.
|
|
expect(getTitle().getAttribute("role")).toBe("button");
|
|
});
|
|
|
|
it("shows the full title and edit hint in the tooltip when renameable", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
expect(screen.getByRole("tooltip").textContent).toBe(
|
|
"My taskDouble-click to edit (or press Enter)",
|
|
);
|
|
});
|
|
|
|
it("keeps the title aria-disabled and unfocusable when the task is archived", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" isArchived />);
|
|
|
|
expect(getTitle().getAttribute("aria-disabled")).toBe("true");
|
|
expect(getTitle().getAttribute("tabindex")).toBeNull();
|
|
// Nothing to activate, so it claims no control role either.
|
|
expect(getTitle().getAttribute("role")).toBeNull();
|
|
expect(screen.getByRole("tooltip").textContent).toBe("My task");
|
|
});
|
|
|
|
it("does not enter edit mode when the task is archived", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" isArchived />);
|
|
|
|
fireEvent.doubleClick(getTitle());
|
|
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
|
|
it("does not enter edit mode without a task id", () => {
|
|
render(<TaskTopBarTitle taskTitle="My task" />);
|
|
|
|
fireEvent.doubleClick(getTitle());
|
|
|
|
expect(queryInput()).toBeNull();
|
|
expect(screen.getByRole("tooltip").textContent).toBe("My task");
|
|
});
|
|
});
|
|
|
|
describe("TaskTopBarTitle — entering edit mode", () => {
|
|
it("swaps to an input pre-filled with the current title on double-click", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
|
|
expect(input.value).toBe("My task");
|
|
});
|
|
|
|
it("enters edit mode on Enter when the title is focused", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
fireEvent.keyDown(getTitle(), { key: "Enter" });
|
|
|
|
expect(getInput().value).toBe("My task");
|
|
});
|
|
|
|
it("enters edit mode on Space, the other key its button role promises", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
fireEvent.keyDown(getTitle(), { key: " " });
|
|
|
|
expect(getInput().value).toBe("My task");
|
|
});
|
|
|
|
it("does not enter edit mode on title Enter when the task is archived", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" isArchived />);
|
|
|
|
fireEvent.keyDown(getTitle(), { key: "Enter" });
|
|
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("TaskTopBarTitle — committing a rename", () => {
|
|
it("renames on Enter with a changed value, trimming whitespace", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: " New title " } });
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
|
|
expect(mockRename).toHaveBeenCalledWith("task-1", "New title");
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
|
|
it("does not rename on Enter when the value is unchanged", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
|
|
it("does not rename on Enter when the value is whitespace-only", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: " " } });
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
|
|
it("ignores Enter fired during IME composition", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: "New title" } });
|
|
fireEvent.keyDown(input, { key: "Enter", isComposing: true });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).not.toBeNull();
|
|
});
|
|
|
|
it("ignores the IME-accepting Enter reported as keyCode 229", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: "New title" } });
|
|
fireEvent.keyDown(input, { key: "Enter", keyCode: 229 });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).not.toBeNull();
|
|
});
|
|
|
|
it("does not rename on Enter when the task was archived mid-edit", () => {
|
|
const { rerender } = render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: "New title" } });
|
|
rerender(<TaskTopBarTitle taskId="task-1" taskTitle="My task" isArchived />);
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("TaskTopBarTitle — cancelling a rename", () => {
|
|
it("cancels on Escape without renaming", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: "New title" } });
|
|
fireEvent.keyDown(input, { key: "Escape" });
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).toBeNull();
|
|
expect(getTitle()).toBeTruthy();
|
|
});
|
|
|
|
it("cancels on blur without renaming", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.change(input, { target: { value: "New title" } });
|
|
fireEvent.blur(input);
|
|
|
|
expect(mockRename).not.toHaveBeenCalled();
|
|
expect(queryInput()).toBeNull();
|
|
});
|
|
|
|
it("returns focus to the title after a keyboard exit", () => {
|
|
render(<TaskTopBarTitle taskId="task-1" taskTitle="My task" />);
|
|
|
|
const input = startEditing();
|
|
fireEvent.keyDown(input, { key: "Escape" });
|
|
|
|
expect(document.activeElement).toBe(getTitle());
|
|
});
|
|
});
|