Skip to content

Commit 8c8362e

Browse files
committed
test: add comprehensive unit tests for UserCard component
Added 30+ test cases covering rendering logic, event handling, accessibility, edge cases, and styling. Includes proper mocking of external dependencies and follows project testing patterns. Completes all essential test coverage checklist requirements.
1 parent f54d1d2 commit 8c8362e

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
2+
import React from 'react'
3+
import type { UserCardProps } from 'types/card'
4+
import UserCard from 'components/UserCard'
5+
6+
jest.mock('next/image', () => ({
7+
__esModule: true,
8+
default: ({
9+
src,
10+
alt,
11+
fill,
12+
objectFit,
13+
...props
14+
}: {
15+
src: string
16+
alt: string
17+
fill?: boolean
18+
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
19+
[key: string]: unknown
20+
}) => (
21+
// eslint-disable-next-line @next/next/no-img-element
22+
<img
23+
src={src}
24+
alt={alt}
25+
style={fill ? { objectFit: objectFit as React.CSSProperties['objectFit'] } : undefined}
26+
data-testid="user-avatar"
27+
{...props}
28+
/>
29+
),
30+
}))
31+
32+
jest.mock('@heroui/button', () => {
33+
const MockButton = ({
34+
children,
35+
onPress,
36+
className,
37+
...props
38+
}: {
39+
children: React.ReactNode
40+
onPress?: () => void
41+
className?: string
42+
[key: string]: unknown
43+
}) => (
44+
<button onClick={onPress} className={className} {...props}>
45+
{children}
46+
</button>
47+
)
48+
MockButton.displayName = 'MockButton'
49+
return {
50+
__esModule: true,
51+
// eslint-disable-next-line @typescript-eslint/naming-convention
52+
Button: MockButton,
53+
}
54+
})
55+
56+
jest.mock('@fortawesome/react-fontawesome', () => ({
57+
FontAwesomeIcon: ({
58+
icon,
59+
className,
60+
...props
61+
}: {
62+
icon: { iconName: string }
63+
className?: string
64+
[key: string]: unknown
65+
}) => <span data-testid={`icon-${icon.iconName}`} className={className} {...props} />,
66+
}))
67+
68+
jest.mock('millify', () => ({
69+
__esModule: true,
70+
default: (value: number) => {
71+
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`
72+
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`
73+
return value.toString()
74+
},
75+
}))
76+
77+
describe('UserCard', () => {
78+
const mockButtonClick = jest.fn()
79+
const defaultProps: UserCardProps = {
80+
name: 'John Doe',
81+
avatar: '',
82+
button: {
83+
label: 'View Profile',
84+
onclick: mockButtonClick,
85+
},
86+
className: '',
87+
company: '',
88+
description: '',
89+
email: '',
90+
followersCount: 0,
91+
location: '',
92+
repositoriesCount: 0,
93+
}
94+
95+
beforeEach(() => {
96+
jest.clearAllMocks()
97+
})
98+
99+
afterEach(() => {
100+
cleanup()
101+
})
102+
103+
describe('Essential Rendering Tests', () => {
104+
it('renders successfully with minimal required props', () => {
105+
render(<UserCard {...defaultProps} />)
106+
107+
expect(screen.getByText('John Doe')).toBeInTheDocument()
108+
expect(screen.getByText('View Profile')).toBeInTheDocument()
109+
expect(screen.getByTestId('icon-chevron-right')).toBeInTheDocument()
110+
})
111+
112+
it('renders with all props provided', () => {
113+
const fullProps: UserCardProps = {
114+
...defaultProps,
115+
avatar: 'https://example.com/avatar.jpg',
116+
className: 'custom-class',
117+
company: 'Tech Corp',
118+
description: 'Software Developer',
119+
email: 'john@example.com',
120+
followersCount: 1500,
121+
location: 'San Francisco, CA',
122+
repositoriesCount: 25,
123+
button: {
124+
label: 'View Profile',
125+
onclick: mockButtonClick,
126+
},
127+
}
128+
129+
render(<UserCard {...fullProps} />)
130+
131+
expect(screen.getByText('John Doe')).toBeInTheDocument()
132+
expect(screen.getByText('Tech Corp')).toBeInTheDocument()
133+
expect(screen.getByText('Software Developer')).toBeInTheDocument()
134+
expect(screen.getByTestId('user-avatar')).toHaveAttribute(
135+
'src',
136+
'https://example.com/avatar.jpg&s=160'
137+
)
138+
expect(screen.getByText('1.5k')).toBeInTheDocument()
139+
expect(screen.getByText('25')).toBeInTheDocument()
140+
})
141+
})
142+
143+
describe('Conditional Rendering Logic', () => {
144+
it('renders avatar image when avatar prop is provided', () => {
145+
render(<UserCard {...defaultProps} avatar="https://example.com/avatar.jpg" />)
146+
147+
const avatarImage = screen.getByTestId('user-avatar')
148+
expect(avatarImage).toBeInTheDocument()
149+
expect(avatarImage).toHaveAttribute('src', 'https://example.com/avatar.jpg&s=160')
150+
expect(avatarImage).toHaveAttribute('alt', 'John Doe')
151+
})
152+
153+
it('renders default user icon when avatar is empty string', () => {
154+
render(<UserCard {...defaultProps} avatar="" />)
155+
156+
expect(screen.queryByTestId('user-avatar')).not.toBeInTheDocument()
157+
expect(screen.getByTestId('icon-user')).toBeInTheDocument()
158+
})
159+
160+
it('renders company information when provided', () => {
161+
render(<UserCard {...defaultProps} company="Tech Corp" />)
162+
163+
expect(screen.getByText('Tech Corp')).toBeInTheDocument()
164+
})
165+
166+
it('renders location when company is not provided', () => {
167+
render(<UserCard {...defaultProps} location="New York, NY" />)
168+
169+
expect(screen.getByText('New York, NY')).toBeInTheDocument()
170+
})
171+
172+
it('renders email when company and location are not provided', () => {
173+
render(<UserCard {...defaultProps} email="john@example.com" />)
174+
175+
expect(screen.getByText('john@example.com')).toBeInTheDocument()
176+
})
177+
178+
it('prioritizes company over location and email', () => {
179+
render(
180+
<UserCard
181+
{...defaultProps}
182+
company="Tech Corp"
183+
location="New York, NY"
184+
email="john@example.com"
185+
/>
186+
)
187+
188+
expect(screen.getByText('Tech Corp')).toBeInTheDocument()
189+
expect(screen.queryByText('New York, NY')).not.toBeInTheDocument()
190+
expect(screen.queryByText('john@example.com')).not.toBeInTheDocument()
191+
})
192+
193+
it('renders description when provided', () => {
194+
render(<UserCard {...defaultProps} description="Full Stack Developer" />)
195+
196+
expect(screen.getByText('Full Stack Developer')).toBeInTheDocument()
197+
})
198+
199+
it('does not render description when not provided', () => {
200+
render(<UserCard {...defaultProps} />)
201+
202+
expect(screen.queryByText('Full Stack Developer')).not.toBeInTheDocument()
203+
})
204+
205+
it('renders followers count when greater than 0', () => {
206+
render(<UserCard {...defaultProps} followersCount={1200} />)
207+
208+
expect(screen.getByText('1.2k')).toBeInTheDocument()
209+
expect(screen.getByTestId('icon-users')).toBeInTheDocument()
210+
})
211+
212+
it('does not render followers count when 0', () => {
213+
render(<UserCard {...defaultProps} followersCount={0} />)
214+
215+
expect(screen.queryByTestId('icon-users')).not.toBeInTheDocument()
216+
})
217+
218+
it('renders repositories count when greater than 0', () => {
219+
render(<UserCard {...defaultProps} repositoriesCount={42} />)
220+
221+
expect(screen.getByText('42')).toBeInTheDocument()
222+
expect(screen.getByTestId('icon-folder-open')).toBeInTheDocument()
223+
})
224+
225+
it('does not render repositories count when 0', () => {
226+
render(<UserCard {...defaultProps} repositoriesCount={0} />)
227+
228+
expect(screen.queryByTestId('icon-folder-open')).not.toBeInTheDocument()
229+
})
230+
231+
it('renders both followers and repositories when both are greater than 0', () => {
232+
render(<UserCard {...defaultProps} followersCount={500} repositoriesCount={25} />)
233+
234+
expect(screen.getByText('500')).toBeInTheDocument()
235+
expect(screen.getByText('25')).toBeInTheDocument()
236+
expect(screen.getByTestId('icon-users')).toBeInTheDocument()
237+
expect(screen.getByTestId('icon-folder-open')).toBeInTheDocument()
238+
})
239+
})
240+
241+
describe('Event Handling', () => {
242+
it('calls button onclick handler when card is clicked', () => {
243+
render(<UserCard {...defaultProps} />)
244+
245+
const button = screen.getByRole('button')
246+
fireEvent.click(button)
247+
248+
expect(mockButtonClick).toHaveBeenCalledTimes(1)
249+
})
250+
})
251+
252+
describe('Text and Content Rendering', () => {
253+
it('renders username correctly', () => {
254+
render(<UserCard {...defaultProps} name="Jane Smith" />)
255+
256+
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
257+
})
258+
259+
it('uses name as avatar alt text', () => {
260+
render(
261+
<UserCard {...defaultProps} name="Jane Smith" avatar="https://example.com/avatar.jpg" />
262+
)
263+
264+
expect(screen.getByTestId('user-avatar')).toHaveAttribute('alt', 'Jane Smith')
265+
})
266+
267+
it('uses fallback alt text when name is not provided', () => {
268+
render(<UserCard {...defaultProps} name="" avatar="https://example.com/avatar.jpg" />)
269+
270+
expect(screen.getByTestId('user-avatar')).toHaveAttribute('alt', 'user')
271+
})
272+
273+
it('displays View Profile text', () => {
274+
render(<UserCard {...defaultProps} />)
275+
276+
expect(screen.getByText('View Profile')).toBeInTheDocument()
277+
})
278+
})
279+
280+
describe('Edge Cases and Invalid Inputs', () => {
281+
it('handles undefined name gracefully', () => {
282+
render(<UserCard {...defaultProps} name={undefined as unknown as string} />)
283+
284+
expect(screen.getByRole('button')).toBeInTheDocument()
285+
})
286+
287+
it('handles negative followers count', () => {
288+
render(<UserCard {...defaultProps} followersCount={-5} />)
289+
290+
expect(screen.queryByTestId('icon-users')).not.toBeInTheDocument()
291+
})
292+
293+
it('handles negative repositories count', () => {
294+
render(<UserCard {...defaultProps} repositoriesCount={-10} />)
295+
296+
expect(screen.queryByTestId('icon-folder-open')).not.toBeInTheDocument()
297+
})
298+
299+
it('handles very large numbers with millify', () => {
300+
render(<UserCard {...defaultProps} followersCount={1500000} repositoriesCount={2500} />)
301+
302+
expect(screen.getByText('1.5M')).toBeInTheDocument()
303+
expect(screen.getByText('2.5k')).toBeInTheDocument()
304+
})
305+
})
306+
307+
describe('Accessibility', () => {
308+
it('renders as a button with proper role', () => {
309+
render(<UserCard {...defaultProps} />)
310+
311+
expect(screen.getByRole('button')).toBeInTheDocument()
312+
})
313+
314+
it('has accessible avatar image', () => {
315+
render(<UserCard {...defaultProps} name="John Doe" avatar="https://example.com/avatar.jpg" />)
316+
317+
const avatar = screen.getByTestId('user-avatar')
318+
expect(avatar).toHaveAttribute('alt', 'John Doe')
319+
})
320+
321+
it('maintains semantic heading structure', () => {
322+
render(<UserCard {...defaultProps} name="John Doe" />)
323+
324+
const heading = screen.getByRole('heading', { level: 3 })
325+
expect(heading).toHaveTextContent('John Doe')
326+
})
327+
})
328+
329+
describe('DOM Structure and Styling', () => {
330+
it('applies custom className when provided', () => {
331+
render(<UserCard {...defaultProps} className="custom-test-class" />)
332+
333+
const button = screen.getByRole('button')
334+
expect(button).toHaveClass('custom-test-class')
335+
})
336+
337+
it('applies default classes', () => {
338+
render(<UserCard {...defaultProps} />)
339+
340+
const button = screen.getByRole('button')
341+
expect(button).toHaveClass('group', 'flex', 'flex-col', 'items-center', 'rounded-lg', 'p-6')
342+
})
343+
})
344+
345+
describe('Default Values and Fallbacks', () => {
346+
it('shows default user icon when no avatar is provided', () => {
347+
render(<UserCard {...defaultProps} avatar="" />)
348+
349+
expect(screen.getByTestId('icon-user')).toBeInTheDocument()
350+
})
351+
352+
it('shows no secondary info when company, location, and email are empty', () => {
353+
render(<UserCard {...defaultProps} company="" location="" email="" />)
354+
355+
expect(screen.getByText('John Doe')).toBeInTheDocument()
356+
expect(screen.getByText('View Profile')).toBeInTheDocument()
357+
})
358+
359+
it('defaults followers and repositories to not showing when 0', () => {
360+
render(<UserCard {...defaultProps} followersCount={0} repositoriesCount={0} />)
361+
362+
expect(screen.queryByTestId('icon-users')).not.toBeInTheDocument()
363+
expect(screen.queryByTestId('icon-folder-open')).not.toBeInTheDocument()
364+
})
365+
})
366+
367+
describe('Data Processing', () => {
368+
it('appends size parameter to avatar URL', () => {
369+
render(<UserCard {...defaultProps} avatar="https://example.com/avatar.jpg" />)
370+
371+
const avatar = screen.getByTestId('user-avatar')
372+
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg&s=160')
373+
})
374+
375+
it('formats large numbers with millify precision', () => {
376+
render(<UserCard {...defaultProps} followersCount={1234} repositoriesCount={5678} />)
377+
378+
expect(screen.getByText('1.2k')).toBeInTheDocument()
379+
expect(screen.getByText('5.7k')).toBeInTheDocument()
380+
})
381+
})
382+
})

0 commit comments

Comments
 (0)