Skip to content

Commit cd9d19f

Browse files
add unit tests (opendatahub-io#5290)
1 parent 9b7f23b commit cd9d19f

File tree

9 files changed

+1945
-0
lines changed

9 files changed

+1945
-0
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/* eslint-disable camelcase */
2+
import * as React from 'react';
3+
import { render, screen, fireEvent } from '@testing-library/react';
4+
import { MemoryRouter } from 'react-router-dom';
5+
import '@testing-library/jest-dom';
6+
import { GenAiContext } from '~/app/context/GenAiContext';
7+
import type { AIModel, LlamaModel, MaaSModel } from '~/app/types';
8+
import AIModelTableRow from '~/app/AIAssets/components/AIModelTableRow';
9+
10+
// Mock the components and utilities
11+
jest.mock('../AIModelsTableRowEndpoint', () => ({
12+
__esModule: true,
13+
default: ({ isExternal }: { isExternal?: boolean }) => (
14+
<div data-testid={isExternal ? 'external-endpoint' : 'internal-endpoint'}>endpoint</div>
15+
),
16+
}));
17+
18+
jest.mock('../AIModelsTableRowInfo', () => ({
19+
__esModule: true,
20+
default: ({ model }: { model: AIModel }) => (
21+
<div data-testid="model-info">{model.display_name}</div>
22+
),
23+
}));
24+
25+
jest.mock('~/app/Chatbot/components/chatbotConfiguration/ChatbotConfigurationModal', () => ({
26+
__esModule: true,
27+
default: ({ onClose }: { onClose: () => void }) => (
28+
<div data-testid="configuration-modal">
29+
<button onClick={onClose} data-testid="close-modal">
30+
Close
31+
</button>
32+
</div>
33+
),
34+
}));
35+
36+
jest.mock('mod-arch-shared', () => ({
37+
...jest.requireActual('mod-arch-shared'),
38+
TableRowTitleDescription: ({ title }: { title: React.ReactNode }) => (
39+
<div data-testid="table-row-title">{title}</div>
40+
),
41+
TruncatedText: ({ content }: { content: string }) => (
42+
<div data-testid="truncated-text">{content}</div>
43+
),
44+
}));
45+
46+
const mockNavigate = jest.fn();
47+
jest.mock('react-router-dom', () => ({
48+
...jest.requireActual('react-router-dom'),
49+
useNavigate: () => mockNavigate,
50+
}));
51+
52+
const mockGenAiContextValue = {
53+
namespace: { name: 'test-namespace' },
54+
nsModel: undefined,
55+
loaded: true,
56+
error: undefined,
57+
refresh: jest.fn(),
58+
isConfigured: true,
59+
configuration: undefined,
60+
crossProjectEnabledNamespaces: [],
61+
};
62+
63+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
64+
<MemoryRouter>
65+
<GenAiContext.Provider value={mockGenAiContextValue}>
66+
<table>
67+
<tbody>{children}</tbody>
68+
</table>
69+
</GenAiContext.Provider>
70+
</MemoryRouter>
71+
);
72+
73+
const createMockAIModel = (overrides?: Partial<AIModel>): AIModel => ({
74+
model_name: 'test-model',
75+
model_id: 'test-model-id',
76+
serving_runtime: 'kserve',
77+
api_protocol: 'v2',
78+
version: 'v1',
79+
usecase: 'llm',
80+
description: 'Test model description',
81+
endpoints: [],
82+
status: 'Running',
83+
display_name: 'Test Model',
84+
internalEndpoint: 'http://internal',
85+
externalEndpoint: 'http://external',
86+
sa_token: {
87+
name: 'token-name',
88+
token_name: 'token',
89+
token: 'test-token',
90+
},
91+
...overrides,
92+
});
93+
94+
const createMockPlaygroundModel = (modelId: string): LlamaModel => ({
95+
id: `provider/${modelId}`,
96+
modelId,
97+
object: 'model',
98+
created: Date.now(),
99+
owned_by: 'test-org',
100+
});
101+
102+
describe('AIModelTableRow', () => {
103+
const defaultProps = {
104+
lsdStatus: null,
105+
aiModels: [] as AIModel[],
106+
maasModels: [] as MaaSModel[],
107+
playgroundModels: [] as LlamaModel[],
108+
};
109+
110+
beforeEach(() => {
111+
jest.clearAllMocks();
112+
});
113+
114+
it('should render model information correctly', () => {
115+
const model = createMockAIModel();
116+
render(
117+
<TestWrapper>
118+
<AIModelTableRow {...defaultProps} model={model} />
119+
</TestWrapper>,
120+
);
121+
122+
expect(screen.getByTestId('model-info')).toHaveTextContent('Test Model');
123+
expect(screen.getByTestId('truncated-text')).toHaveTextContent('Test model description');
124+
});
125+
126+
it('should render internal and external endpoints', () => {
127+
const model = createMockAIModel();
128+
render(
129+
<TestWrapper>
130+
<AIModelTableRow {...defaultProps} model={model} />
131+
</TestWrapper>,
132+
);
133+
134+
expect(screen.getByTestId('internal-endpoint')).toBeInTheDocument();
135+
expect(screen.getByTestId('external-endpoint')).toBeInTheDocument();
136+
});
137+
138+
it('should render use case', () => {
139+
const model = createMockAIModel({ usecase: 'text-generation' });
140+
render(
141+
<TestWrapper>
142+
<AIModelTableRow {...defaultProps} model={model} />
143+
</TestWrapper>,
144+
);
145+
146+
expect(screen.getByText('text-generation')).toBeInTheDocument();
147+
});
148+
149+
describe('Status', () => {
150+
it('should show Active status when model is Running', () => {
151+
const model = createMockAIModel({ status: 'Running' });
152+
render(
153+
<TestWrapper>
154+
<AIModelTableRow {...defaultProps} model={model} />
155+
</TestWrapper>,
156+
);
157+
158+
expect(screen.getByText('Active')).toBeInTheDocument();
159+
});
160+
161+
it('should show Inactive status when model is not Running', () => {
162+
const model = createMockAIModel({ status: 'Stop' });
163+
render(
164+
<TestWrapper>
165+
<AIModelTableRow {...defaultProps} model={model} />
166+
</TestWrapper>,
167+
);
168+
169+
expect(screen.getByText('Inactive')).toBeInTheDocument();
170+
});
171+
});
172+
173+
describe('Playground column', () => {
174+
it('should show "Add to playground" button when model is not in playground', () => {
175+
const model = createMockAIModel();
176+
render(
177+
<TestWrapper>
178+
<AIModelTableRow {...defaultProps} model={model} />
179+
</TestWrapper>,
180+
);
181+
182+
expect(screen.getByText('Add to playground')).toBeInTheDocument();
183+
});
184+
185+
it('should show "Try in playground" button when model is in playground', () => {
186+
const model = createMockAIModel({ model_id: 'test-model-id' });
187+
const playgroundModel = createMockPlaygroundModel('test-model-id');
188+
189+
render(
190+
<TestWrapper>
191+
<AIModelTableRow {...defaultProps} model={model} playgroundModels={[playgroundModel]} />
192+
</TestWrapper>,
193+
);
194+
195+
expect(screen.getByText('Try in playground')).toBeInTheDocument();
196+
});
197+
198+
it('should disable "Add to playground" button when model is not Running', () => {
199+
const model = createMockAIModel({ status: 'Stop' });
200+
render(
201+
<TestWrapper>
202+
<AIModelTableRow {...defaultProps} model={model} />
203+
</TestWrapper>,
204+
);
205+
206+
const button = screen.getByText('Add to playground');
207+
expect(button.closest('button')).toBeDisabled();
208+
});
209+
210+
it('should disable "Try in playground" button when model is not Running', () => {
211+
const model = createMockAIModel({ model_id: 'test-model-id', status: 'Stop' });
212+
const playgroundModel = createMockPlaygroundModel('test-model-id');
213+
214+
render(
215+
<TestWrapper>
216+
<AIModelTableRow {...defaultProps} model={model} playgroundModels={[playgroundModel]} />
217+
</TestWrapper>,
218+
);
219+
220+
const button = screen.getByText('Try in playground');
221+
expect(button.closest('button')).toBeDisabled();
222+
});
223+
224+
it('should open configuration modal when "Add to playground" is clicked', () => {
225+
const model = createMockAIModel();
226+
render(
227+
<TestWrapper>
228+
<AIModelTableRow {...defaultProps} model={model} />
229+
</TestWrapper>,
230+
);
231+
232+
const addButton = screen.getByText('Add to playground');
233+
fireEvent.click(addButton);
234+
235+
expect(screen.getByTestId('configuration-modal')).toBeInTheDocument();
236+
});
237+
238+
it('should close configuration modal when closed', () => {
239+
const model = createMockAIModel();
240+
render(
241+
<TestWrapper>
242+
<AIModelTableRow {...defaultProps} model={model} />
243+
</TestWrapper>,
244+
);
245+
246+
const addButton = screen.getByText('Add to playground');
247+
fireEvent.click(addButton);
248+
249+
expect(screen.getByTestId('configuration-modal')).toBeInTheDocument();
250+
251+
const closeButton = screen.getByTestId('close-modal');
252+
fireEvent.click(closeButton);
253+
254+
expect(screen.queryByTestId('configuration-modal')).not.toBeInTheDocument();
255+
});
256+
257+
it('should navigate to playground when "Try in playground" is clicked', () => {
258+
const model = createMockAIModel({ model_id: 'test-model-id' });
259+
const playgroundModel = createMockPlaygroundModel('test-model-id');
260+
261+
render(
262+
<TestWrapper>
263+
<AIModelTableRow {...defaultProps} model={model} playgroundModels={[playgroundModel]} />
264+
</TestWrapper>,
265+
);
266+
267+
const tryButton = screen.getByText('Try in playground');
268+
fireEvent.click(tryButton);
269+
270+
expect(mockNavigate).toHaveBeenCalledWith('/gen-ai-studio/playground/test-namespace', {
271+
state: {
272+
model: 'provider/test-model-id',
273+
},
274+
});
275+
});
276+
277+
it('should pass model as extraSelectedModels when opening configuration modal', () => {
278+
const model = createMockAIModel({ model_id: 'test-model-id' });
279+
280+
render(
281+
<TestWrapper>
282+
<AIModelTableRow {...defaultProps} model={model} />
283+
</TestWrapper>,
284+
);
285+
286+
const addButton = screen.getByText('Add to playground');
287+
fireEvent.click(addButton);
288+
289+
// The modal should be open with the model pre-selected
290+
expect(screen.getByTestId('configuration-modal')).toBeInTheDocument();
291+
});
292+
});
293+
});

0 commit comments

Comments
 (0)