Skip to content

Commit 1ca00f6

Browse files
committed
chore(FR-1876): add test code for FR-1876 (#4953)
# Add comprehensive tests for MyResourceWithinResourceGroup component Resolves #4952 ([FR-1876](https://lablup.atlassian.net/browse/FR-1876)) This PR adds comprehensive test coverage for the `MyResourceWithinResourceGroup` component. The tests cover various resource usage scenarios including: - Normal resource usage display - Zero resource usage - High resource usage - Handling of Infinity values - Scenarios without GPU resources - Handling of undefined values - Zero GPU usage and remaining resources The PR also fixes an import path in the component file, changing from an absolute import to a relative import for the `useFetchKey` hook. **Checklist:** - [ ] Documentation - [ ] Minium required manager version - [x] Specific setting for review (eg., KB link, endpoint or how to setup): go to `backend.ai-webui/react` and run `pnpm run test` - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1876]: https://lablup.atlassian.net/browse/FR-1876?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 0ec7e43 commit 1ca00f6

2 files changed

Lines changed: 390 additions & 1 deletion

File tree

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
import '../../__test__/matchMedia.mock.js';
2+
import * as useResourceLimitAndRemainingModule from '../hooks/useResourceLimitAndRemaining';
3+
import MyResourceWithinResourceGroup from './MyResourceWithinResourceGroup';
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5+
import '@testing-library/jest-dom';
6+
import { render, screen } from '@testing-library/react';
7+
import React from 'react';
8+
9+
// Mock all the required hooks and dependencies
10+
jest.mock('react-i18next', () => ({
11+
useTranslation: () => ({
12+
t: (key: string) => key,
13+
}),
14+
}));
15+
16+
jest.mock('antd', () => ({
17+
Segmented: ({ children }: any) => (
18+
<div data-testid="segmented">{children}</div>
19+
),
20+
Skeleton: () => <div data-testid="skeleton">Loading...</div>,
21+
Typography: {
22+
Text: ({ children }: any) => <span>{children}</span>,
23+
},
24+
theme: {
25+
useToken: () => ({ token: {} }),
26+
},
27+
}));
28+
29+
jest.mock('ahooks', () => ({
30+
useControllableValue: () => ['free', jest.fn()],
31+
}));
32+
33+
jest.mock('../hooks', () => {
34+
const isoDate = new Date().toISOString();
35+
return {
36+
useFetchKey: () => [isoDate, jest.fn(), isoDate],
37+
};
38+
});
39+
40+
jest.mock('../hooks/useCurrentProject', () => ({
41+
useCurrentProjectValue: () => ({ name: 'test-project' }),
42+
useCurrentResourceGroupValue: () => 'default',
43+
}));
44+
45+
// Helper function to create base mock data structure
46+
const createBaseMockData = () => ({
47+
keypair_limits: {},
48+
keypair_using: {},
49+
keypair_remaining: {},
50+
scaling_group_remaining: {},
51+
});
52+
53+
const mockDataScenarios = {
54+
normal: {
55+
...createBaseMockData(),
56+
scaling_groups: {
57+
default: {
58+
using: { cpu: '4.0', mem: '8.0 GiB', 'cuda.device': '2' },
59+
remaining: { cpu: '12.0', mem: '24.0 GiB', 'cuda.device': '6' },
60+
},
61+
},
62+
},
63+
zero: {
64+
...createBaseMockData(),
65+
scaling_groups: {
66+
default: {
67+
using: { cpu: '0.0', mem: '0.0 GiB', 'cuda.device': '0' },
68+
remaining: { cpu: '16.0', mem: '32.0 GiB', 'cuda.device': '8' },
69+
},
70+
},
71+
},
72+
high: {
73+
...createBaseMockData(),
74+
scaling_groups: {
75+
default: {
76+
using: { cpu: '14.0', mem: '28.0 GiB', 'cuda.device': '7' },
77+
remaining: { cpu: '2.0', mem: '4.0 GiB', 'cuda.device': '1' },
78+
},
79+
},
80+
},
81+
infinity: {
82+
...createBaseMockData(),
83+
scaling_groups: {
84+
default: {
85+
using: { cpu: '2.0', mem: '4.0 GiB', 'cuda.device': '1' },
86+
remaining: {
87+
cpu: 'Infinity',
88+
mem: 'Infinity',
89+
'cuda.device': 'Infinity',
90+
},
91+
},
92+
},
93+
},
94+
noGpu: {
95+
...createBaseMockData(),
96+
scaling_groups: {
97+
default: {
98+
using: { cpu: '8.0', mem: '16.0 GiB' },
99+
remaining: { cpu: '8.0', mem: '16.0 GiB' },
100+
},
101+
},
102+
},
103+
undefined: {
104+
...createBaseMockData(),
105+
scaling_groups: {
106+
default: {
107+
using: { cpu: undefined, mem: '4.0 GiB' },
108+
remaining: { cpu: '8.0', mem: undefined },
109+
},
110+
},
111+
},
112+
zeroGpu: {
113+
...createBaseMockData(),
114+
scaling_groups: {
115+
default: {
116+
using: { cpu: '4.0', mem: '8.0 GiB', 'cuda.device': '0' },
117+
remaining: { cpu: '8.0', mem: '16.0 GiB', 'cuda.device': '0' },
118+
},
119+
},
120+
},
121+
};
122+
123+
jest.mock('../hooks/useResourceLimitAndRemaining', () => ({
124+
useResourceLimitAndRemaining: jest.fn(() => [
125+
{
126+
resourceGroupResourceSize: { cpu: 0, mem: '0 GiB', accelerators: {} },
127+
resourceLimits: { accelerators: {} },
128+
resourceLimitsWithoutResourceGroup: { accelerators: {} },
129+
remaining: { accelerators: {} },
130+
remainingWithoutResourceGroup: { accelerators: {} },
131+
currentImageMinM: '0g',
132+
isRefetching: false,
133+
checkPresetInfo: mockDataScenarios.normal as any,
134+
},
135+
{
136+
refetch: jest.fn(),
137+
},
138+
]),
139+
}));
140+
141+
jest.mock('backend.ai-ui', () => ({
142+
useResourceSlotsDetails: () => ({
143+
isLoading: false,
144+
resourceSlotsInRG: {
145+
cpu: { human_readable_name: 'CPU', display_unit: 'Core' },
146+
mem: { human_readable_name: 'Memory', display_unit: 'GiB' },
147+
'cuda.device': { human_readable_name: 'CUDA GPU', display_unit: 'GPU' },
148+
},
149+
}),
150+
convertToNumber: (value: any) => parseFloat(value) || 0,
151+
processMemoryValue: (value: any) => {
152+
if (!value || value === 'Infinity' || value === Infinity) return value;
153+
return (
154+
parseFloat(value.replace ? value.replace(/[^0-9.]/g, '') : value) || 0
155+
);
156+
},
157+
BAIFlex: ({ children }: any) => <div data-testid="bai-flex">{children}</div>,
158+
BAIBoardItemTitle: ({ title, extra }: any) => (
159+
<div data-testid="board-title">
160+
<div>{title}</div>
161+
<div data-testid="board-extra">{extra}</div>
162+
</div>
163+
),
164+
ResourceStatistics: ({ resourceData }: any) => (
165+
<div data-testid="resource-statistics">
166+
{resourceData.cpu && (
167+
<div data-testid="cpu-data">
168+
CPU: {resourceData.cpu.used.current}/{resourceData.cpu.free.current}
169+
</div>
170+
)}
171+
{resourceData.memory && (
172+
<div data-testid="memory-data">
173+
Memory: {resourceData.memory.used.current}/
174+
{resourceData.memory.free.current}
175+
</div>
176+
)}
177+
{resourceData.accelerators?.map((acc: any, idx: number) => (
178+
<div key={idx} data-testid="gpu-data">
179+
GPU: {acc.used.current}/{acc.free.current}
180+
</div>
181+
))}
182+
</div>
183+
),
184+
BAIFetchKeyButton: () => <button data-testid="refresh-btn">Refresh</button>,
185+
}));
186+
187+
jest.mock('./SharedResourceGroupSelectForCurrentProject', () => {
188+
const MockedComponent = () => (
189+
<div data-testid="resource-group-select">Select</div>
190+
);
191+
MockedComponent.displayName = 'SharedResourceGroupSelectForCurrentProject';
192+
return MockedComponent;
193+
});
194+
195+
// Helper function to create mock return value
196+
const createMockReturnValue = (checkPresetInfo: any) =>
197+
[
198+
{
199+
resourceGroupResourceSize: { cpu: 0, mem: '0 GiB', accelerators: {} },
200+
resourceLimits: { accelerators: {} },
201+
resourceLimitsWithoutResourceGroup: { accelerators: {} },
202+
remaining: { accelerators: {} },
203+
remainingWithoutResourceGroup: { accelerators: {} },
204+
currentImageMinM: '0g',
205+
isRefetching: false,
206+
checkPresetInfo,
207+
},
208+
{
209+
refetch: jest.fn(),
210+
},
211+
] as const;
212+
213+
// Test wrapper component
214+
const TestWrapper = ({
215+
children,
216+
queryClient,
217+
}: {
218+
children: React.ReactNode;
219+
queryClient?: QueryClient;
220+
}) => {
221+
const client =
222+
queryClient ||
223+
new QueryClient({
224+
defaultOptions: { queries: { retry: false } },
225+
});
226+
227+
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
228+
};
229+
TestWrapper.displayName = 'TestWrapper';
230+
231+
describe('MyResourceWithinResourceGroup', () => {
232+
let queryClient: QueryClient;
233+
234+
const mockHook = jest.spyOn(
235+
useResourceLimitAndRemainingModule,
236+
'useResourceLimitAndRemaining',
237+
);
238+
239+
beforeEach(() => {
240+
queryClient = new QueryClient({
241+
defaultOptions: { queries: { retry: false } },
242+
});
243+
});
244+
245+
afterEach(() => {
246+
jest.clearAllMocks();
247+
mockHook.mockReset();
248+
});
249+
250+
it('should render basic components', () => {
251+
render(
252+
<TestWrapper queryClient={queryClient}>
253+
<MyResourceWithinResourceGroup />
254+
</TestWrapper>,
255+
);
256+
257+
expect(screen.getByTestId('board-title')).toBeInTheDocument();
258+
expect(screen.getByTestId('resource-group-select')).toBeInTheDocument();
259+
expect(screen.getByTestId('refresh-btn')).toBeInTheDocument();
260+
});
261+
262+
describe('Resource Usage Scenarios', () => {
263+
it('should display normal resource usage correctly', () => {
264+
mockHook.mockReturnValue(
265+
createMockReturnValue(mockDataScenarios.normal as any),
266+
);
267+
268+
render(
269+
<TestWrapper queryClient={queryClient}>
270+
<MyResourceWithinResourceGroup />
271+
</TestWrapper>,
272+
);
273+
274+
expect(screen.getByTestId('resource-statistics')).toBeInTheDocument();
275+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 4/12');
276+
expect(screen.getByTestId('memory-data')).toHaveTextContent(
277+
'Memory: 8/24',
278+
);
279+
expect(screen.getByTestId('gpu-data')).toHaveTextContent('GPU: 2/6');
280+
});
281+
282+
it('should display zero usage correctly', () => {
283+
mockHook.mockReturnValue(
284+
createMockReturnValue(mockDataScenarios.zero as any),
285+
);
286+
287+
render(
288+
<TestWrapper queryClient={queryClient}>
289+
<MyResourceWithinResourceGroup />
290+
</TestWrapper>,
291+
);
292+
293+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 0/16');
294+
expect(screen.getByTestId('memory-data')).toHaveTextContent(
295+
'Memory: 0/32',
296+
);
297+
expect(screen.getByTestId('gpu-data')).toHaveTextContent('GPU: 0/8');
298+
});
299+
300+
it('should display high usage correctly', () => {
301+
mockHook.mockReturnValue(
302+
createMockReturnValue(mockDataScenarios.high as any),
303+
);
304+
305+
render(
306+
<TestWrapper queryClient={queryClient}>
307+
<MyResourceWithinResourceGroup />
308+
</TestWrapper>,
309+
);
310+
311+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 14/2');
312+
expect(screen.getByTestId('memory-data')).toHaveTextContent(
313+
'Memory: 28/4',
314+
);
315+
expect(screen.getByTestId('gpu-data')).toHaveTextContent('GPU: 7/1');
316+
});
317+
318+
it('should handle Infinity values correctly', () => {
319+
mockHook.mockReturnValue(
320+
createMockReturnValue(mockDataScenarios.infinity as any),
321+
);
322+
323+
render(
324+
<TestWrapper queryClient={queryClient}>
325+
<MyResourceWithinResourceGroup />
326+
</TestWrapper>,
327+
);
328+
329+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 2');
330+
expect(screen.getByTestId('memory-data')).toHaveTextContent('Memory: 4');
331+
expect(screen.getByTestId('gpu-data')).toHaveTextContent('GPU: 1');
332+
});
333+
334+
it('should handle no GPU scenario correctly', () => {
335+
mockHook.mockReturnValue(
336+
createMockReturnValue(mockDataScenarios.noGpu as any),
337+
);
338+
339+
render(
340+
<TestWrapper queryClient={queryClient}>
341+
<MyResourceWithinResourceGroup />
342+
</TestWrapper>,
343+
);
344+
345+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 8/8');
346+
expect(screen.getByTestId('memory-data')).toHaveTextContent(
347+
'Memory: 16/16',
348+
);
349+
expect(screen.queryByTestId('gpu-data')).not.toBeInTheDocument();
350+
});
351+
352+
it('should handle undefined values gracefully', () => {
353+
mockHook.mockReturnValue(
354+
createMockReturnValue(mockDataScenarios.undefined as any),
355+
);
356+
357+
render(
358+
<TestWrapper queryClient={queryClient}>
359+
<MyResourceWithinResourceGroup />
360+
</TestWrapper>,
361+
);
362+
363+
expect(screen.getByTestId('resource-statistics')).toBeInTheDocument();
364+
// Memory data shows partial content due to undefined 'remaining' value
365+
expect(screen.getByTestId('memory-data')).toHaveTextContent('Memory: 4/');
366+
// CPU data shows partial content due to undefined 'using' value
367+
expect(screen.queryByTestId('cpu-data')).not.toBeInTheDocument();
368+
expect(screen.queryByTestId('gpu-data')).not.toBeInTheDocument();
369+
});
370+
371+
it('should handle zero GPU usage and remaining correctly', () => {
372+
mockHook.mockReturnValue(
373+
createMockReturnValue(mockDataScenarios.zeroGpu as any),
374+
);
375+
376+
render(
377+
<TestWrapper queryClient={queryClient}>
378+
<MyResourceWithinResourceGroup />
379+
</TestWrapper>,
380+
);
381+
382+
expect(screen.getByTestId('cpu-data')).toHaveTextContent('CPU: 4/8');
383+
expect(screen.getByTestId('memory-data')).toHaveTextContent(
384+
'Memory: 8/16',
385+
);
386+
expect(screen.getByTestId('gpu-data')).toHaveTextContent('GPU: 0/0');
387+
});
388+
});
389+
});

0 commit comments

Comments
 (0)