Skip to content

Commit 6bbdbba

Browse files
test(backend.ai-ui): add comprehensive tests for BAILink component (#4865)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent a4ce68d commit 6bbdbba

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import BAILink from './BAILink';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import React from 'react';
5+
import { MemoryRouter } from 'react-router-dom';
6+
7+
// jsdom does not provide ResizeObserver; antd Typography.Text ellipsis tooltip needs it
8+
global.ResizeObserver = class {
9+
observe() {}
10+
unobserve() {}
11+
disconnect() {}
12+
};
13+
14+
// Helper to render with Router
15+
const renderWithRouter = (ui: React.ReactElement) => {
16+
return render(<MemoryRouter>{ui}</MemoryRouter>);
17+
};
18+
19+
describe('BAILink', () => {
20+
describe('Basic Rendering', () => {
21+
it('should render react-router Link with to prop', () => {
22+
renderWithRouter(<BAILink to="/test">Test Link</BAILink>);
23+
const link = screen.getByText('Test Link');
24+
expect(link).toBeInTheDocument();
25+
expect(link).toHaveAttribute('href', '/test');
26+
});
27+
28+
it('should render Typography.Link without to prop', () => {
29+
render(<BAILink>Test Link</BAILink>);
30+
expect(screen.getByText('Test Link')).toBeInTheDocument();
31+
});
32+
33+
it('should render children as React elements', () => {
34+
renderWithRouter(
35+
<BAILink to="/test">
36+
<span data-testid="child-element">Complex Content</span>
37+
</BAILink>,
38+
);
39+
expect(screen.getByTestId('child-element')).toBeInTheDocument();
40+
});
41+
});
42+
43+
describe('Link Types', () => {
44+
it('should render react-router Link when type is "hover"', () => {
45+
renderWithRouter(
46+
<BAILink to="/test" type="hover">
47+
Hover Link
48+
</BAILink>,
49+
);
50+
const link = screen.getByText('Hover Link');
51+
expect(link).toHaveAttribute('href', '/test');
52+
expect(link).toHaveAttribute('class');
53+
});
54+
55+
it('should render Typography.Link when type is "disabled" even with to prop', () => {
56+
render(
57+
<BAILink type="disabled" to="/test">
58+
Disabled Link
59+
</BAILink>,
60+
);
61+
const link = screen.getByText('Disabled Link');
62+
// Disabled renders as Typography.Link (no href), not react-router Link
63+
expect(link).not.toHaveAttribute('href');
64+
});
65+
66+
it('should render without explicit type when type is undefined', () => {
67+
renderWithRouter(<BAILink to="/test">Normal Link</BAILink>);
68+
const link = screen.getByText('Normal Link');
69+
expect(link).toHaveAttribute('href', '/test');
70+
});
71+
});
72+
73+
describe('Router Integration', () => {
74+
it('should accept object-style to prop', () => {
75+
renderWithRouter(
76+
<BAILink to={{ pathname: '/test', search: '?q=value' }}>
77+
Object Link
78+
</BAILink>,
79+
);
80+
const link = screen.getByText('Object Link');
81+
expect(link).toHaveAttribute('href', '/test?q=value');
82+
});
83+
84+
it('should render Typography.Link when to prop is missing', () => {
85+
render(<BAILink>No To Prop</BAILink>);
86+
const link = screen.getByText('No To Prop');
87+
expect(link).not.toHaveAttribute('href');
88+
});
89+
});
90+
91+
describe('Ellipsis', () => {
92+
it('should apply ellipsis when ellipsis is true', () => {
93+
render(
94+
<BAILink ellipsis={true}>Long text that should be ellipsed</BAILink>,
95+
);
96+
expect(
97+
screen.getByText('Long text that should be ellipsed'),
98+
).toBeInTheDocument();
99+
});
100+
101+
it('should render with tooltip ellipsis config', () => {
102+
render(
103+
<BAILink ellipsis={{ tooltip: 'Full text here' }}>
104+
Truncated text
105+
</BAILink>,
106+
);
107+
expect(screen.getByText('Truncated text')).toBeInTheDocument();
108+
});
109+
110+
it('should render children without ellipsis when ellipsis is false', () => {
111+
render(<BAILink ellipsis={false}>No ellipsis</BAILink>);
112+
expect(screen.getByText('No ellipsis')).toBeInTheDocument();
113+
});
114+
});
115+
116+
describe('onClick Handler', () => {
117+
it('should call onClick handler when clicked on react-router Link', async () => {
118+
const onClick = jest.fn((e) => e.preventDefault());
119+
const user = userEvent.setup();
120+
renderWithRouter(
121+
<BAILink to="/test" onClick={onClick}>
122+
Clickable Link
123+
</BAILink>,
124+
);
125+
126+
await user.click(screen.getByText('Clickable Link'));
127+
expect(onClick).toHaveBeenCalledTimes(1);
128+
});
129+
130+
it('should call onClick handler when clicked on Typography.Link', async () => {
131+
const onClick = jest.fn();
132+
const user = userEvent.setup();
133+
render(<BAILink onClick={onClick}>Clickable Typography Link</BAILink>);
134+
135+
await user.click(screen.getByText('Clickable Typography Link'));
136+
expect(onClick).toHaveBeenCalledTimes(1);
137+
});
138+
139+
it('should block click interaction when link is disabled', async () => {
140+
const onClick = jest.fn();
141+
const user = userEvent.setup();
142+
render(
143+
<BAILink type="disabled" onClick={onClick}>
144+
Disabled Link
145+
</BAILink>,
146+
);
147+
148+
const link = screen.getByText('Disabled Link');
149+
// userEvent respects pointer-events: none and blocks the click
150+
await expect(user.click(link)).rejects.toThrow(/pointer-events/);
151+
expect(onClick).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('Props Passthrough', () => {
156+
it('should pass through LinkProps to react-router Link', () => {
157+
renderWithRouter(
158+
<BAILink to="/test" className="custom-class" data-testid="custom-link">
159+
Custom Link
160+
</BAILink>,
161+
);
162+
const link = screen.getByTestId('custom-link');
163+
expect(link).toHaveClass('custom-class');
164+
});
165+
166+
it('should pass through LinkProps to Typography.Link', () => {
167+
render(
168+
<BAILink className="typography-custom" data-testid="typography-link">
169+
Typography Link
170+
</BAILink>,
171+
);
172+
const link = screen.getByTestId('typography-link');
173+
expect(link).toHaveClass('typography-custom');
174+
});
175+
});
176+
177+
describe('Edge Cases', () => {
178+
it('should handle empty children gracefully', () => {
179+
renderWithRouter(<BAILink to="/test">{''}</BAILink>);
180+
expect(screen.queryByRole('link')).toBeInTheDocument();
181+
});
182+
183+
it('should handle null children', () => {
184+
renderWithRouter(<BAILink to="/test">{null}</BAILink>);
185+
expect(screen.queryByRole('link')).toBeInTheDocument();
186+
});
187+
188+
it('should handle complex nested children', () => {
189+
renderWithRouter(
190+
<BAILink to="/test">
191+
<div>
192+
<span>Nested</span>
193+
<span>Content</span>
194+
</div>
195+
</BAILink>,
196+
);
197+
expect(screen.getByText('Nested')).toBeInTheDocument();
198+
expect(screen.getByText('Content')).toBeInTheDocument();
199+
});
200+
});
201+
202+
describe('Multiple Links', () => {
203+
it('should render multiple links independently', () => {
204+
renderWithRouter(
205+
<>
206+
<BAILink to="/link1">Link 1</BAILink>
207+
<BAILink to="/link2" type="hover">
208+
Link 2
209+
</BAILink>
210+
<BAILink type="disabled">Link 3</BAILink>
211+
</>,
212+
);
213+
expect(screen.getByText('Link 1')).toHaveAttribute('href', '/link1');
214+
expect(screen.getByText('Link 2')).toHaveAttribute('href', '/link2');
215+
// Disabled link renders as Typography.Link without href
216+
expect(screen.getByText('Link 3')).not.toHaveAttribute('href');
217+
});
218+
});
219+
220+
describe('Accessibility', () => {
221+
it('should be keyboard accessible for react-router Link', async () => {
222+
const onClick = jest.fn((e) => e.preventDefault());
223+
const user = userEvent.setup();
224+
renderWithRouter(
225+
<BAILink to="/test" onClick={onClick}>
226+
Keyboard Link
227+
</BAILink>,
228+
);
229+
230+
const link = screen.getByText('Keyboard Link');
231+
link.focus();
232+
await user.keyboard('{Enter}');
233+
234+
expect(onClick).toHaveBeenCalled();
235+
});
236+
237+
it('should call onClick for Typography.Link on click', async () => {
238+
const onClick = jest.fn();
239+
const user = userEvent.setup();
240+
render(<BAILink onClick={onClick}>Typography Click</BAILink>);
241+
242+
await user.click(screen.getByText('Typography Click'));
243+
expect(onClick).toHaveBeenCalled();
244+
});
245+
246+
it('should have disabled state when type is disabled', () => {
247+
render(
248+
<BAILink type="disabled">
249+
Disabled Link
250+
</BAILink>,
251+
);
252+
253+
const link = screen.getByText('Disabled Link');
254+
// Ant Design adds ant-typography-disabled class for disabled Typography.Link
255+
expect(link).toHaveClass('ant-typography-disabled');
256+
});
257+
});
258+
});

0 commit comments

Comments
 (0)