Skip to content

Commit 78e62f6

Browse files
committed
test: increase frontend coverage to 93.7% statements and 86.4% functions
Add comprehensive unit tests for Devfile Creator components, stores, containers, pages, and services to meet the 92%/85% coverage thresholds. Assisted-by: Claude Opus 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent e14d704 commit 78e62f6

23 files changed

Lines changed: 7923 additions & 189 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
* Copyright (c) 2018-2025 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
13+
import { api } from '@eclipse-che/common';
14+
import { Nav } from '@patternfly/react-core';
15+
import { render, screen } from '@testing-library/react';
16+
import userEvent from '@testing-library/user-event';
17+
import React from 'react';
18+
19+
import { NavigationAgentList } from '@/Layout/Navigation/AgentList';
20+
import { AgentPodPhase, AgentPodStatus } from '@/store/LocalDevfiles';
21+
22+
jest.mock('@/contexts/ThemeContext', () => ({
23+
useTheme: () => ({
24+
themePreference: 'LIGHT',
25+
isDarkTheme: false,
26+
setThemePreference: jest.fn(),
27+
}),
28+
}));
29+
30+
function buildAgentDef(overrides?: Partial<api.AiAgentDefinition>): api.AiAgentDefinition {
31+
return {
32+
id: 'test-agent',
33+
name: 'Test Agent',
34+
publisher: 'test-publisher',
35+
description: 'A test agent',
36+
icon: '',
37+
docsUrl: '',
38+
image: 'test-image',
39+
tag: 'latest',
40+
memoryLimit: '512Mi',
41+
cpuLimit: '500m',
42+
terminalPort: 8080,
43+
env: [],
44+
initCommand: '',
45+
...overrides,
46+
};
47+
}
48+
49+
function buildAgentPodStatus(overrides?: Partial<AgentPodStatus>): AgentPodStatus {
50+
return {
51+
agentId: 'test-agent',
52+
name: 'agent-test-agent',
53+
phase: AgentPodPhase.RUNNING,
54+
ready: true,
55+
serviceUrl: undefined,
56+
...overrides,
57+
};
58+
}
59+
60+
const mockStopAgent = jest.fn();
61+
62+
function renderComponent(
63+
agentPodStatuses: AgentPodStatus[] = [],
64+
agents: api.AiAgentDefinition[] = [],
65+
) {
66+
return render(
67+
<Nav onSelect={jest.fn()}>
68+
<NavigationAgentList
69+
activePath="/some-path"
70+
agentPodStatuses={agentPodStatuses}
71+
agents={agents}
72+
stopAgent={mockStopAgent}
73+
/>
74+
</Nav>,
75+
);
76+
}
77+
78+
describe('NavigationAgentList', () => {
79+
beforeEach(() => {
80+
jest.clearAllMocks();
81+
});
82+
83+
describe('empty state', () => {
84+
test('should not render AGENT PODS title when no agent pod statuses', () => {
85+
renderComponent([], []);
86+
expect(screen.queryByText('AGENT PODS')).not.toBeInTheDocument();
87+
});
88+
});
89+
90+
describe('with agent pods', () => {
91+
const agents: api.AiAgentDefinition[] = [
92+
buildAgentDef({ id: 'anthropic/claude', name: 'Claude Code', description: 'AI assistant' }),
93+
buildAgentDef({ id: 'openai/gpt', name: 'GPT Agent', description: '' }),
94+
];
95+
96+
test('should render AGENT PODS nav group title', () => {
97+
const statuses = [buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'agent-claude' })];
98+
renderComponent(statuses, agents);
99+
expect(screen.getByText('AGENT PODS')).toBeInTheDocument();
100+
});
101+
102+
test('should render agent pod items', () => {
103+
const statuses = [
104+
buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'agent-claude' }),
105+
buildAgentPodStatus({
106+
agentId: 'openai/gpt',
107+
name: 'agent-gpt',
108+
phase: AgentPodPhase.PENDING,
109+
ready: false,
110+
}),
111+
];
112+
renderComponent(statuses, agents);
113+
const items = screen.getAllByTestId('agent-pod-item');
114+
expect(items).toHaveLength(2);
115+
});
116+
117+
test('should display agent name without agent- prefix', () => {
118+
const statuses = [buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'agent-claude' })];
119+
renderComponent(statuses, agents);
120+
expect(screen.getByText('claude')).toBeInTheDocument();
121+
});
122+
123+
test('should display full name when name has no agent- prefix', () => {
124+
const statuses = [buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'claude-pod' })];
125+
renderComponent(statuses, agents);
126+
expect(screen.getByText('claude-pod')).toBeInTheDocument();
127+
});
128+
129+
test('should render status indicator for each agent', () => {
130+
const statuses = [buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'agent-claude' })];
131+
renderComponent(statuses, agents);
132+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
133+
});
134+
135+
test('should render multiple status indicators for multiple agents', () => {
136+
const statuses = [
137+
buildAgentPodStatus({ agentId: 'anthropic/claude', name: 'agent-claude' }),
138+
buildAgentPodStatus({ agentId: 'openai/gpt', name: 'agent-gpt' }),
139+
];
140+
renderComponent(statuses, agents);
141+
expect(screen.getAllByTestId('agent-status-indicator')).toHaveLength(2);
142+
});
143+
});
144+
145+
describe('agent pod phases', () => {
146+
const agents = [buildAgentDef({ id: 'test-agent', name: 'Test Agent' })];
147+
148+
test('should render RUNNING phase with ready=true', () => {
149+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.RUNNING, ready: true })];
150+
renderComponent(statuses, agents);
151+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
152+
});
153+
154+
test('should render RUNNING phase with ready=false as STARTING', () => {
155+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.RUNNING, ready: false })];
156+
renderComponent(statuses, agents);
157+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
158+
});
159+
160+
test('should render PENDING phase', () => {
161+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.PENDING, ready: false })];
162+
renderComponent(statuses, agents);
163+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
164+
});
165+
166+
test('should render FAILED phase', () => {
167+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.FAILED, ready: false })];
168+
renderComponent(statuses, agents);
169+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
170+
});
171+
172+
test('should render UNKNOWN phase', () => {
173+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.UNKNOWN, ready: false })];
174+
renderComponent(statuses, agents);
175+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
176+
});
177+
178+
test('should render SUCCEEDED phase', () => {
179+
const statuses = [buildAgentPodStatus({ phase: AgentPodPhase.SUCCEEDED, ready: false })];
180+
renderComponent(statuses, agents);
181+
expect(screen.getByTestId('agent-status-indicator')).toBeInTheDocument();
182+
});
183+
});
184+
185+
describe('stop action', () => {
186+
const agents = [buildAgentDef({ id: 'test-agent', name: 'Test Agent' })];
187+
188+
test('should show dropdown with Stop option when kebab menu is clicked', async () => {
189+
const statuses = [buildAgentPodStatus()];
190+
renderComponent(statuses, agents);
191+
192+
// Hover over the nav item to make actions visible
193+
const navItem = screen.getByTestId('agent-pod-item').closest('[class*="navItem"]')!;
194+
await userEvent.hover(navItem);
195+
196+
const kebabButton = screen.getByRole('button', { name: 'Agent actions' });
197+
await userEvent.click(kebabButton);
198+
199+
expect(screen.getByText('Stop')).toBeInTheDocument();
200+
});
201+
202+
test('should call stopAgent when Stop is clicked', async () => {
203+
const statuses = [buildAgentPodStatus({ agentId: 'my-agent-id' })];
204+
renderComponent(statuses, agents);
205+
206+
const navItem = screen.getByTestId('agent-pod-item').closest('[class*="navItem"]')!;
207+
await userEvent.hover(navItem);
208+
209+
const kebabButton = screen.getByRole('button', { name: 'Agent actions' });
210+
await userEvent.click(kebabButton);
211+
212+
const stopButton = screen.getByText('Stop');
213+
await userEvent.click(stopButton);
214+
215+
expect(mockStopAgent).toHaveBeenCalledWith('my-agent-id');
216+
});
217+
218+
test('should close dropdown after Stop is clicked', async () => {
219+
const statuses = [buildAgentPodStatus()];
220+
renderComponent(statuses, agents);
221+
222+
const navItem = screen.getByTestId('agent-pod-item').closest('[class*="navItem"]')!;
223+
await userEvent.hover(navItem);
224+
225+
const kebabButton = screen.getByRole('button', { name: 'Agent actions' });
226+
await userEvent.click(kebabButton);
227+
expect(kebabButton).toHaveAttribute('aria-expanded', 'true');
228+
229+
const stopButton = screen.getByText('Stop');
230+
await userEvent.click(stopButton);
231+
232+
// Dropdown toggle should be collapsed after Stop is clicked
233+
expect(kebabButton).toHaveAttribute('aria-expanded', 'false');
234+
});
235+
});
236+
237+
describe('agent tooltip content', () => {
238+
test('should render agent pod item when agent definition matches by prefix', () => {
239+
const agents = [
240+
buildAgentDef({ id: 'anthropic/claude', name: 'Claude Code', description: 'AI assistant' }),
241+
];
242+
const statuses = [
243+
buildAgentPodStatus({ agentId: 'anthropic/claude-instance-1', name: 'agent-claude' }),
244+
];
245+
renderComponent(statuses, agents);
246+
expect(screen.getByTestId('agent-pod-item')).toBeInTheDocument();
247+
});
248+
249+
test('should render agent pod item when no matching definition exists', () => {
250+
const statuses = [buildAgentPodStatus({ agentId: 'unknown-agent', name: 'agent-unknown' })];
251+
renderComponent(statuses, []);
252+
expect(screen.getByTestId('agent-pod-item')).toBeInTheDocument();
253+
});
254+
255+
test('should render agent pod item with agent that has no description', () => {
256+
const agents = [buildAgentDef({ id: 'openai/gpt', name: 'GPT Agent', description: '' })];
257+
const statuses = [buildAgentPodStatus({ agentId: 'openai/gpt', name: 'agent-gpt' })];
258+
renderComponent(statuses, agents);
259+
expect(screen.getByTestId('agent-pod-item')).toBeInTheDocument();
260+
});
261+
});
262+
263+
describe('hover and focus states', () => {
264+
const agents = [buildAgentDef({ id: 'test-agent', name: 'Test Agent' })];
265+
266+
test('should handle mouse enter and leave events', async () => {
267+
const statuses = [buildAgentPodStatus()];
268+
renderComponent(statuses, agents);
269+
const item = screen.getByTestId('agent-pod-item').closest('[class*="navItem"]');
270+
expect(item).toBeDefined();
271+
272+
await userEvent.hover(item!);
273+
await userEvent.unhover(item!);
274+
});
275+
276+
test('should handle focus and blur events', () => {
277+
const statuses = [buildAgentPodStatus()];
278+
renderComponent(statuses, agents);
279+
const item = screen.getByTestId('agent-pod-item').closest('[class*="navItem"]');
280+
expect(item).toBeDefined();
281+
282+
item!.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
283+
item!.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
284+
});
285+
});
286+
});

0 commit comments

Comments
 (0)