Skip to content

Commit 60a5906

Browse files
committed
test(web): enhance ShortcutTip tests for hover functionality
- Update tests for ShortcutTip to verify shortcut visibility on hover and hiding on mouse leave. - Utilize userEvent for simulating user interactions in tests. - Ensure shortcuts are not visible initially and appear correctly when hovering over buttons.
1 parent 97c15b9 commit 60a5906

File tree

4 files changed

+168
-43
lines changed

4 files changed

+168
-43
lines changed

packages/web/src/views/Day/components/Shortcuts/components/ShortcutTip.test.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import "@testing-library/jest-dom";
2-
import { render, screen } from "@testing-library/react";
2+
import { render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
34
import { ShortcutTip } from "./ShortcutTip";
45

56
describe("ShortcutTip", () => {
@@ -40,37 +41,68 @@ describe("ShortcutTip", () => {
4041
expect(shortcutElement).toBeInTheDocument();
4142
});
4243

43-
it("wraps child components with shortcut", () => {
44+
it("wraps child components and shows shortcut on hover", async () => {
45+
const user = userEvent.setup();
4446
render(
4547
<ShortcutTip shortcut="C">
4648
<button>Some Button</button>
4749
</ShortcutTip>,
4850
);
51+
4952
const button = screen.getByText("Some Button");
5053
expect(button).toBeInTheDocument();
51-
const shortcutElement = screen.getByText("C");
52-
expect(shortcutElement).toBeInTheDocument();
54+
55+
// Shortcut should not be visible initially
56+
expect(screen.queryByText("C")).not.toBeInTheDocument();
57+
58+
// Hover over button to show shortcut
59+
await user.hover(button);
60+
await waitFor(() => {
61+
expect(screen.getByText("C")).toBeInTheDocument();
62+
});
5363
});
5464

55-
it("renders wrapped component with array shortcut", () => {
65+
it("renders wrapped component with array shortcut on hover", async () => {
66+
const user = userEvent.setup();
5667
render(
5768
<ShortcutTip shortcut={["Cmd", "K"]}>
5869
<button>Quick Action</button>
5970
</ShortcutTip>,
6071
);
72+
6173
const button = screen.getByText("Quick Action");
6274
expect(button).toBeInTheDocument();
63-
const shortcutElement = screen.getByText("Cmd + K");
64-
expect(shortcutElement).toBeInTheDocument();
75+
76+
// Shortcut should not be visible initially
77+
expect(screen.queryByText("Cmd + K")).not.toBeInTheDocument();
78+
79+
// Hover over button to show shortcut
80+
await user.hover(button);
81+
await waitFor(() => {
82+
expect(screen.getByText("Cmd + K")).toBeInTheDocument();
83+
});
6584
});
6685

67-
it("applies flex layout when wrapping children", () => {
86+
it("hides shortcut when mouse leaves", async () => {
87+
const user = userEvent.setup();
6888
render(
6989
<ShortcutTip shortcut="C">
7090
<button>Test Button</button>
7191
</ShortcutTip>,
7292
);
73-
const wrapper = screen.getByText("Test Button").parentElement;
74-
expect(wrapper).toHaveClass("flex", "items-center", "gap-2");
93+
94+
const button = screen.getByText("Test Button");
95+
96+
// Hover to show shortcut
97+
await user.hover(button);
98+
await waitFor(() => {
99+
expect(screen.getByText("C")).toBeInTheDocument();
100+
});
101+
102+
// Move mouse away to hide shortcut
103+
await user.unhover(button);
104+
await waitFor(() => {
105+
expect(screen.queryByText("C")).not.toBeInTheDocument();
106+
});
75107
});
76108
});

packages/web/src/views/Day/components/Shortcuts/components/ShortcutTip.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from "react";
1+
import { ReactNode, useState } from "react";
22

33
interface ShortcutProps {
44
shortcut: string | string[];
@@ -34,11 +34,25 @@ export const ShortcutTip = ({
3434
children,
3535
"aria-label": ariaLabel,
3636
}: ShortcutTipProps) => {
37+
const [isHovered, setIsHovered] = useState(false);
38+
3739
if (children) {
3840
return (
39-
<div className="flex items-center gap-2">
40-
{children}
41-
<Shortcut shortcut={shortcut} aria-label={ariaLabel} />
41+
<div className="relative inline-block">
42+
<div
43+
onMouseEnter={() => setIsHovered(true)}
44+
onMouseLeave={() => setIsHovered(false)}
45+
>
46+
{children}
47+
</div>
48+
{isHovered && (
49+
<div className="absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 transform">
50+
<div className="rounded border border-gray-600 bg-gray-800 px-2 py-1 shadow-lg">
51+
<Shortcut shortcut={shortcut} aria-label={ariaLabel} />
52+
</div>
53+
<div className="absolute top-full left-1/2 h-0 w-0 -translate-x-1/2 transform border-t-4 border-r-4 border-l-4 border-transparent border-t-gray-600"></div>
54+
</div>
55+
)}
4256
</div>
4357
);
4458
}

packages/web/src/views/Day/components/Task/Task.test.tsx

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1+
import { act } from "react";
12
import "@testing-library/jest-dom";
23
import { fireEvent, render, screen } from "@testing-library/react";
4+
import userEvent from "@testing-library/user-event";
35
import { Task as TaskType } from "../../task.types";
4-
import { Task } from "./Task";
5-
6-
// Mock the getMetaKey utility
7-
jest.mock("@web/common/utils/shortcut/shortcut.util", () => ({
8-
getMetaKey: jest.fn(() => <span data-testid="meta-key"></span>),
9-
}));
10-
11-
// Mock TooltipWrapper
12-
jest.mock("@web/components/Tooltip/TooltipWrapper", () => ({
13-
TooltipWrapper: ({ children, onClick }: any) => (
14-
<div onClick={onClick}>{children}</div>
15-
),
16-
}));
6+
import { Task, TaskProps } from "./Task";
177

188
describe("Task - migration", () => {
199
const mockTask: TaskType = {
@@ -23,7 +13,7 @@ describe("Task - migration", () => {
2313
createdAt: "2025-10-27T10:00:00Z",
2414
};
2515

26-
const mockProps = {
16+
const mockProps: TaskProps = {
2717
task: mockTask,
2818
index: 0,
2919
title: "Test Task",
@@ -70,3 +60,96 @@ describe("Task - migration", () => {
7060
expect(mockProps.onMigrate).toHaveBeenCalledWith(mockTask.id, "backward");
7161
});
7262
});
63+
64+
describe("Task - migration icon visibility on focus", () => {
65+
const mockTask: TaskType = {
66+
id: "task-1",
67+
title: "Test Task",
68+
status: "todo",
69+
createdAt: "2025-10-27T10:00:00Z",
70+
};
71+
72+
const mockProps: TaskProps = {
73+
task: mockTask,
74+
index: 0,
75+
title: "Test Task",
76+
isEditing: false,
77+
onCheckboxKeyDown: jest.fn(),
78+
onInputBlur: jest.fn(),
79+
onInputClick: jest.fn(),
80+
onInputKeyDown: jest.fn(),
81+
onStatusToggle: jest.fn(),
82+
onTitleChange: jest.fn(),
83+
onFocus: jest.fn(),
84+
onMigrate: jest.fn(),
85+
};
86+
87+
beforeEach(() => {
88+
jest.clearAllMocks();
89+
});
90+
91+
it("shows migration icons when checkbox button is focused", async () => {
92+
const { container } = render(<Task {...mockProps} />);
93+
94+
const checkbox = screen.getByRole("checkbox", {
95+
name: /toggle test task/i,
96+
});
97+
const migrationContainer = container.querySelector(".ml-auto");
98+
99+
// Initially hidden
100+
expect(migrationContainer).toHaveClass("opacity-0");
101+
102+
// Focus on checkbox
103+
await act(() => checkbox.focus());
104+
105+
// Migration buttons should be visible via group-focus-within
106+
const forwardButton = screen.getByLabelText("Move task to next day");
107+
const backwardButton = screen.getByLabelText("Move task to previous day");
108+
109+
expect(forwardButton).toBeVisible();
110+
expect(backwardButton).toBeVisible();
111+
});
112+
113+
it("shows migration icons when input is focused via click", async () => {
114+
const { container } = render(<Task {...mockProps} isEditing={true} />);
115+
116+
const input = screen.getByRole("textbox", { name: /edit test task/i });
117+
const migrationContainer = container.querySelector(".ml-auto");
118+
119+
// Initially hidden
120+
expect(migrationContainer).toHaveClass("opacity-0");
121+
122+
// Focus on input
123+
await act(() => input.focus());
124+
125+
// Migration buttons should be visible
126+
const forwardButton = screen.getByLabelText("Move task to next day");
127+
const backwardButton = screen.getByLabelText("Move task to previous day");
128+
129+
expect(forwardButton).toBeVisible();
130+
expect(backwardButton).toBeVisible();
131+
});
132+
133+
it("shows migration icons when tabbing from checkbox to input", async () => {
134+
const user = userEvent.setup();
135+
render(<Task {...mockProps} />);
136+
137+
const checkbox = screen.getByRole("checkbox", {
138+
name: /toggle test task/i,
139+
});
140+
141+
// Focus checkbox first
142+
await act(() => checkbox.focus());
143+
144+
// Verify icons are visible
145+
let forwardButton = screen.getByLabelText("Move task to next day");
146+
expect(forwardButton).toBeVisible();
147+
148+
// Tab to next focusable element (should be one of the migration buttons or input)
149+
await user.tab();
150+
151+
// Icons should still be visible as focus is within the task row
152+
forwardButton = screen.getByLabelText("Move task to next day");
153+
expect(forwardButton).toBeVisible();
154+
});
155+
});

packages/web/src/views/Day/components/Task/Task.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import React from "react";
22
import { DATA_TASK_ELEMENT_ID } from "@web/common/constants/web.constants";
3-
import { getMetaKey } from "@web/common/utils/shortcut/shortcut.util";
43
import { Task as TaskType } from "../../task.types";
54
import { ChevronLeftIcon } from "../Icons/ChevronLeftIcon";
65
import { ChevronRightIcon } from "../Icons/ChevronRightIcon";
76
import { TaskCircleIcon } from "../Icons/TaskCircleIcon";
8-
import { ShortcutTip } from "../Shortcuts/components/ShortcutTip";
97

10-
interface TaskProps {
8+
export interface TaskProps {
119
task: TaskType;
1210
index: number;
1311
title: string;
@@ -45,8 +43,6 @@ export const Task = ({
4543
onTitleChange,
4644
onMigrate,
4745
}: TaskProps) => {
48-
const metaKey = getMetaKey();
49-
5046
return (
5147
<div
5248
key={task.id}
@@ -91,22 +87,22 @@ export const Task = ({
9187
/>
9288
</div>
9389
{/* Migration buttons */}
94-
<div className="ml-auto flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
90+
<div className="ml-auto flex gap-1 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100">
9591
<button
9692
aria-label="Move task to previous day"
97-
className="flex h-6 w-6 items-center justify-center rounded-full text-white/60 transition-colors hover:bg-white/10 hover:text-white focus:bg-white/10 focus:text-white focus:ring-2 focus:ring-white/50 focus:outline-none"
93+
className="flex h-6 w-6 items-center justify-center rounded-full text-white transition-colors hover:bg-white/10 hover:text-white focus:bg-white/10 focus:text-white focus:ring-2 focus:ring-white/50 focus:outline-none"
94+
onClick={() => onMigrate(task.id, "backward")}
9895
>
9996
<ChevronLeftIcon />
10097
</button>
10198

102-
<ShortcutTip shortcut={["CTRL", "Meta", "→"]}>
103-
<button
104-
aria-label="Move task to next day"
105-
className="flex h-6 w-6 items-center justify-center rounded-full text-white/60 transition-colors hover:bg-white/10 hover:text-white focus:bg-white/10 focus:text-white focus:ring-2 focus:ring-white/50 focus:outline-none"
106-
>
107-
<ChevronRightIcon />
108-
</button>
109-
</ShortcutTip>
99+
<button
100+
aria-label="Move task to next day"
101+
className="flex h-6 w-6 items-center justify-center rounded-full text-white transition-colors hover:bg-white/10 hover:text-white focus:bg-white/10 focus:text-white focus:ring-2 focus:ring-white/50 focus:outline-none"
102+
onClick={() => onMigrate(task.id, "forward")}
103+
>
104+
<ChevronRightIcon />
105+
</button>
110106
</div>
111107
</div>
112108
);

0 commit comments

Comments
 (0)