Skip to content

Commit 943194c

Browse files
claudekittyyueli
authored andcommitted
fix(apollo-react): refactor adhoc tasks
- Add menu on right click and 3 dot click - Update style for title padding - Use import type for type-only imports in AdhocTask.tsx - Fix formatting in AdhocTask.test.tsx - Remove unused biome suppression comments in StageNode.tsx - Consolidate duplicate @uipath/apollo-core imports in TaskMenu.tsx - Fix export ordering in index.ts https://claude.ai/code/session_015BzRh3KFTKaL1WVoDAMajV
1 parent 887fcf1 commit 943194c

7 files changed

Lines changed: 513 additions & 78 deletions

File tree

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, expect, it, vi } from 'vitest';
4+
import type { NodeMenuItem } from '../NodeContextMenu';
5+
import { AdhocTaskItem } from './AdhocTask';
6+
import type { StageTaskItem } from './StageNode.types';
7+
8+
const createTask = (id: string, label?: string): StageTaskItem => ({
9+
id,
10+
label: label ?? `Task ${id}`,
11+
isAdhoc: true,
12+
});
13+
14+
const createMenuItems = (onRemoveClick: () => void): NodeMenuItem[] => [
15+
{
16+
id: 'replace-task',
17+
label: 'Replace task',
18+
onClick: vi.fn(),
19+
},
20+
{
21+
type: 'divider' as const,
22+
},
23+
{
24+
id: 'remove-task',
25+
label: 'Delete task',
26+
onClick: onRemoveClick,
27+
},
28+
];
29+
30+
describe('AdhocTaskItem', () => {
31+
const defaultProps = {
32+
task: createTask('adhoc-1', 'Adhoc Task'),
33+
taskExecution: undefined,
34+
isSelected: false,
35+
contextMenuItems: [] as NodeMenuItem[],
36+
onTaskClick: vi.fn(),
37+
};
38+
39+
describe('Rendering', () => {
40+
it('renders task with correct testid', () => {
41+
render(<AdhocTaskItem {...defaultProps} />);
42+
43+
expect(screen.getByTestId('stage-task-adhoc-1')).toBeInTheDocument();
44+
});
45+
46+
it('renders task label', () => {
47+
render(<AdhocTaskItem {...defaultProps} />);
48+
49+
expect(screen.getByText('Adhoc Task')).toBeInTheDocument();
50+
});
51+
52+
it('renders with selected state', () => {
53+
render(<AdhocTaskItem {...defaultProps} isSelected={true} />);
54+
55+
expect(screen.getByTestId('stage-task-adhoc-1')).toBeInTheDocument();
56+
});
57+
});
58+
59+
describe('Task Click Behavior', () => {
60+
it('calls onTaskClick when task is clicked', async () => {
61+
const user = userEvent.setup();
62+
const onTaskClick = vi.fn();
63+
64+
render(<AdhocTaskItem {...defaultProps} onTaskClick={onTaskClick} />);
65+
66+
const task = screen.getByTestId('stage-task-adhoc-1');
67+
await user.click(task);
68+
69+
expect(onTaskClick).toHaveBeenCalledTimes(1);
70+
expect(onTaskClick).toHaveBeenCalledWith(expect.any(Object), 'adhoc-1');
71+
});
72+
73+
it('prevents task click when menu is open', async () => {
74+
const user = userEvent.setup();
75+
const onTaskClick = vi.fn();
76+
const onRemove = vi.fn();
77+
const menuItems = createMenuItems(onRemove);
78+
79+
render(
80+
<AdhocTaskItem {...defaultProps} onTaskClick={onTaskClick} contextMenuItems={menuItems} />
81+
);
82+
83+
// Open menu
84+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
85+
await user.click(menuButton);
86+
87+
await waitFor(() => {
88+
expect(screen.getByText('Replace task')).toBeInTheDocument();
89+
});
90+
91+
// Try to click the task while menu is open
92+
const task = screen.getByTestId('stage-task-adhoc-1');
93+
await user.click(task);
94+
95+
expect(onTaskClick).not.toHaveBeenCalled();
96+
});
97+
98+
it('allows task click after menu is closed', async () => {
99+
const user = userEvent.setup();
100+
const onTaskClick = vi.fn();
101+
const onRemove = vi.fn();
102+
const menuItems = createMenuItems(onRemove);
103+
104+
render(
105+
<AdhocTaskItem {...defaultProps} onTaskClick={onTaskClick} contextMenuItems={menuItems} />
106+
);
107+
108+
// Open menu
109+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
110+
await user.click(menuButton);
111+
112+
await waitFor(() => {
113+
expect(screen.getByText('Replace task')).toBeInTheDocument();
114+
});
115+
116+
// Click a menu item to close it
117+
const replaceItem = screen.getByText('Replace task');
118+
await user.click(replaceItem);
119+
120+
await waitFor(() => {
121+
expect(screen.queryByText('Replace task')).not.toBeInTheDocument();
122+
});
123+
124+
// Now task click should work
125+
const task = screen.getByTestId('stage-task-adhoc-1');
126+
await user.click(task);
127+
128+
expect(onTaskClick).toHaveBeenCalledWith(expect.any(Object), 'adhoc-1');
129+
});
130+
});
131+
132+
describe('Play Button', () => {
133+
it('does not render play button when onTaskPlay is not provided', () => {
134+
render(<AdhocTaskItem {...defaultProps} />);
135+
136+
expect(screen.queryByTestId('stage-task-play-adhoc-1')).not.toBeInTheDocument();
137+
});
138+
139+
it('renders play button when onTaskPlay is provided', () => {
140+
const onTaskPlay = vi.fn().mockResolvedValue(undefined);
141+
142+
render(<AdhocTaskItem {...defaultProps} onTaskPlay={onTaskPlay} />);
143+
144+
expect(screen.getByTestId('stage-task-play-adhoc-1')).toBeInTheDocument();
145+
});
146+
147+
it('calls onTaskPlay when play button is clicked', async () => {
148+
const user = userEvent.setup();
149+
const onTaskPlay = vi.fn().mockResolvedValue(undefined);
150+
151+
render(<AdhocTaskItem {...defaultProps} onTaskPlay={onTaskPlay} />);
152+
153+
const playButton = screen.getByTestId('stage-task-play-adhoc-1');
154+
await user.click(playButton);
155+
156+
expect(onTaskPlay).toHaveBeenCalledWith('adhoc-1');
157+
});
158+
159+
it('does not trigger task click when play button is clicked', async () => {
160+
const user = userEvent.setup();
161+
const onTaskClick = vi.fn();
162+
const onTaskPlay = vi.fn().mockResolvedValue(undefined);
163+
164+
render(<AdhocTaskItem {...defaultProps} onTaskClick={onTaskClick} onTaskPlay={onTaskPlay} />);
165+
166+
const playButton = screen.getByTestId('stage-task-play-adhoc-1');
167+
await user.click(playButton);
168+
169+
expect(onTaskClick).not.toHaveBeenCalled();
170+
});
171+
172+
it('shows loading indicator while task play is in progress', async () => {
173+
const user = userEvent.setup();
174+
let resolvePlay: () => void;
175+
const onTaskPlay = vi.fn(
176+
() =>
177+
new Promise<void>((resolve) => {
178+
resolvePlay = resolve;
179+
})
180+
);
181+
182+
render(<AdhocTaskItem {...defaultProps} onTaskPlay={onTaskPlay} />);
183+
184+
const playButton = screen.getByTestId('stage-task-play-adhoc-1');
185+
await user.click(playButton);
186+
187+
// Should show circular progress while loading
188+
await waitFor(() => {
189+
expect(screen.getByTestId('ap-circular-progress')).toBeInTheDocument();
190+
});
191+
192+
// Resolve the play promise
193+
resolvePlay!();
194+
195+
// Loading indicator should disappear
196+
await waitFor(() => {
197+
expect(screen.queryByTestId('ap-circular-progress')).not.toBeInTheDocument();
198+
});
199+
});
200+
201+
it('recovers from play error and hides loading indicator', async () => {
202+
const user = userEvent.setup();
203+
const onTaskPlay = vi.fn().mockRejectedValue(new Error('play failed'));
204+
205+
render(<AdhocTaskItem {...defaultProps} onTaskPlay={onTaskPlay} />);
206+
207+
const playButton = screen.getByTestId('stage-task-play-adhoc-1');
208+
await user.click(playButton);
209+
210+
// Loading should eventually clear after error
211+
await waitFor(() => {
212+
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
213+
});
214+
});
215+
});
216+
217+
describe('Context Menu', () => {
218+
it('renders menu button when contextMenuItems are provided', () => {
219+
const onRemove = vi.fn();
220+
const menuItems = createMenuItems(onRemove);
221+
222+
render(<AdhocTaskItem {...defaultProps} contextMenuItems={menuItems} />);
223+
224+
expect(screen.getByTestId('stage-task-menu-adhoc-1')).toBeInTheDocument();
225+
});
226+
227+
it('does not render menu button when contextMenuItems is empty', () => {
228+
render(<AdhocTaskItem {...defaultProps} contextMenuItems={[]} />);
229+
230+
expect(screen.queryByTestId('stage-task-menu-adhoc-1')).not.toBeInTheDocument();
231+
});
232+
233+
it('opens menu when button is clicked', async () => {
234+
const user = userEvent.setup();
235+
const onRemove = vi.fn();
236+
const menuItems = createMenuItems(onRemove);
237+
238+
render(<AdhocTaskItem {...defaultProps} contextMenuItems={menuItems} />);
239+
240+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
241+
await user.click(menuButton);
242+
243+
await waitFor(() => {
244+
expect(screen.getByText('Replace task')).toBeInTheDocument();
245+
expect(screen.getByText('Delete task')).toBeInTheDocument();
246+
});
247+
});
248+
249+
it('triggers menu item onClick when clicked', async () => {
250+
const user = userEvent.setup();
251+
const onRemove = vi.fn();
252+
const menuItems = createMenuItems(onRemove);
253+
254+
render(<AdhocTaskItem {...defaultProps} contextMenuItems={menuItems} />);
255+
256+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
257+
await user.click(menuButton);
258+
259+
await waitFor(() => {
260+
expect(screen.getByText('Delete task')).toBeInTheDocument();
261+
});
262+
263+
await user.click(screen.getByText('Delete task'));
264+
265+
expect(onRemove).toHaveBeenCalledTimes(1);
266+
});
267+
268+
it('closes menu after menu item is clicked', async () => {
269+
const user = userEvent.setup();
270+
const onRemove = vi.fn();
271+
const menuItems = createMenuItems(onRemove);
272+
273+
render(<AdhocTaskItem {...defaultProps} contextMenuItems={menuItems} />);
274+
275+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
276+
await user.click(menuButton);
277+
278+
await waitFor(() => {
279+
expect(screen.getByText('Delete task')).toBeInTheDocument();
280+
});
281+
282+
await user.click(screen.getByText('Delete task'));
283+
284+
await waitFor(() => {
285+
expect(screen.queryByText('Delete task')).not.toBeInTheDocument();
286+
});
287+
});
288+
});
289+
290+
describe('onMenuOpen callback', () => {
291+
it('calls onMenuOpen when menu is opened', async () => {
292+
const user = userEvent.setup();
293+
const onMenuOpen = vi.fn();
294+
const onRemove = vi.fn();
295+
const menuItems = createMenuItems(onRemove);
296+
297+
render(
298+
<AdhocTaskItem {...defaultProps} contextMenuItems={menuItems} onMenuOpen={onMenuOpen} />
299+
);
300+
301+
const menuButton = screen.getByTestId('stage-task-menu-adhoc-1');
302+
await user.click(menuButton);
303+
304+
expect(onMenuOpen).toHaveBeenCalled();
305+
});
306+
});
307+
});

0 commit comments

Comments
 (0)