Skip to content

Commit 2350398

Browse files
RizWaaN3024kasya
andauthored
feat: add comprehensive unit tests for DisplayIcon component (#2048)
* feat: add comprehensive unit tests for DisplayIcon component - 30+ test cases covering rendering, props, edge cases, and accessibility - Mock all dependencies and ensure SonarQube compliance - Test both snake_case and camelCase property conventions * Fixed issues flagged by the bot * Fixed issues flagged by the bot * Fixed issues flagged by the bot and added unhover word in cspell/custom-dict.txt --------- Co-authored-by: Kate Golovanova <[email protected]>
1 parent 01fb848 commit 2350398

File tree

2 files changed

+364
-0
lines changed

2 files changed

+364
-0
lines changed

cspell/custom-dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ superfences
117117
tiktok
118118
tsc
119119
turbopack
120+
unhover
120121
usefixtures
121122
winsrdf
122123
wsgi
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
import { render, screen } from '@testing-library/react'
2+
import userEvent from '@testing-library/user-event'
3+
import React from 'react'
4+
import type { Icon } from 'types/icon'
5+
import DisplayIcon from 'components/DisplayIcon'
6+
7+
interface TooltipProps {
8+
children: React.ReactNode
9+
content: string
10+
delay: number
11+
closeDelay: number
12+
showArrow: boolean
13+
placement: string
14+
}
15+
16+
interface IconWrapperProps {
17+
className?: string
18+
icon: string
19+
}
20+
21+
jest.mock('@heroui/tooltip', () => ({
22+
Tooltip: ({ children, content, delay, closeDelay, showArrow, placement }: TooltipProps) => (
23+
<div
24+
data-testid="tooltip"
25+
data-tooltip-content={content}
26+
data-delay={delay}
27+
data-close-delay={closeDelay}
28+
data-show-arrow={showArrow}
29+
data-placement={placement}
30+
>
31+
{children}
32+
</div>
33+
),
34+
}))
35+
36+
jest.mock('millify', () => ({
37+
millify: jest.fn((value: number, options?: { precision: number }) => {
38+
if (value >= 1000000000) return `${(value / 1000000000).toFixed(options?.precision || 1)}B`
39+
if (value >= 1000000) return `${(value / 1000000).toFixed(options?.precision || 1)}M`
40+
if (value >= 1000) return `${(value / 1000).toFixed(options?.precision || 1)}k`
41+
return value.toString()
42+
}),
43+
}))
44+
45+
jest.mock('wrappers/FontAwesomeIconWrapper', () => {
46+
return function MockFontAwesomeIconWrapper({ className, icon }: IconWrapperProps) {
47+
return <span data-testid="font-awesome-icon" data-icon={icon} className={className} />
48+
}
49+
})
50+
51+
jest.mock('utils/data', () => ({
52+
ICONS: {
53+
starsCount: { label: 'Stars', icon: 'fa-star' },
54+
forksCount: { label: 'Forks', icon: 'fa-code-fork' },
55+
contributorsCount: { label: 'Contributors', icon: 'fa-users' },
56+
contributionCount: { label: 'Contributors', icon: 'fa-users' },
57+
issuesCount: { label: 'Issues', icon: 'fa-exclamation-circle' },
58+
license: { label: 'License', icon: 'fa-balance-scale' },
59+
unknownItem: { label: 'Unknown', icon: 'fa-question' },
60+
},
61+
}))
62+
63+
describe('DisplayIcon', () => {
64+
const mockIcons: Icon = {
65+
starsCount: 1250,
66+
forksCount: 350,
67+
contributorsCount: 25,
68+
contributionCount: 25,
69+
issuesCount: 42,
70+
license: 'MIT',
71+
}
72+
73+
describe('Basic Rendering', () => {
74+
it('renders successfully with minimal required props', () => {
75+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
76+
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
77+
})
78+
79+
it('renders nothing when item is not in icons object', () => {
80+
const { container } = render(<DisplayIcon item="nonexistentItem" icons={mockIcons} />)
81+
expect(container.firstChild).toBeNull()
82+
})
83+
84+
it('renders nothing when icons object is empty', () => {
85+
const { container } = render(<DisplayIcon item="starsCount" icons={{}} />)
86+
expect(container.firstChild).toBeNull()
87+
})
88+
})
89+
90+
describe('Conditional Rendering Logic', () => {
91+
it('renders when item exists in icons object', () => {
92+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
93+
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
94+
})
95+
96+
it('does not render when item does not exist in icons object', () => {
97+
const { container } = render(<DisplayIcon item="nonexistent" icons={mockIcons} />)
98+
expect(container.firstChild).toBeNull()
99+
})
100+
101+
it('does not render when icons[item] is falsy', () => {
102+
const iconsWithFalsy: Icon = { ...mockIcons, starsCount: 0 }
103+
const { container } = render(<DisplayIcon item="starsCount" icons={iconsWithFalsy} />)
104+
expect(container.firstChild).toBeNull()
105+
})
106+
})
107+
108+
describe('Prop-based Behavior', () => {
109+
it('displays correct icon based on item prop', () => {
110+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
111+
const icon = screen.getByTestId('font-awesome-icon')
112+
expect(icon).toHaveAttribute('data-icon', 'fa-star')
113+
})
114+
115+
it('displays different icons for different items', () => {
116+
const { rerender } = render(<DisplayIcon item="forksCount" icons={mockIcons} />)
117+
expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-code-fork')
118+
119+
rerender(<DisplayIcon item="contributorsCount" icons={mockIcons} />)
120+
expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-users')
121+
})
122+
123+
it('applies different container classes based on item type', () => {
124+
const { rerender, container } = render(<DisplayIcon item="starsCount" icons={mockIcons} />)
125+
let containerDiv = container.querySelector('div[class*="rotate-container"]')
126+
expect(containerDiv).toBeInTheDocument()
127+
128+
rerender(<DisplayIcon item="forksCount" icons={mockIcons} />)
129+
containerDiv = container.querySelector('div[class*="flip-container"]')
130+
expect(containerDiv).toBeInTheDocument()
131+
})
132+
133+
it('applies different icon classes based on item type', () => {
134+
const { rerender } = render(<DisplayIcon item="starsCount" icons={mockIcons} />)
135+
let icon = screen.getByTestId('font-awesome-icon')
136+
expect(icon).toHaveClass('icon-rotate')
137+
138+
rerender(<DisplayIcon item="forksCount" icons={mockIcons} />)
139+
icon = screen.getByTestId('font-awesome-icon')
140+
expect(icon).toHaveClass('icon-flip')
141+
})
142+
})
143+
144+
describe('Text and Content Rendering', () => {
145+
it('displays formatted numbers using millify for numeric values', () => {
146+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
147+
expect(screen.getByText('1.3k')).toBeInTheDocument()
148+
})
149+
150+
it('displays string values as-is', () => {
151+
render(<DisplayIcon item="license" icons={mockIcons} />)
152+
expect(screen.getByText('MIT')).toBeInTheDocument()
153+
})
154+
155+
it('displays tooltip with correct label', () => {
156+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
157+
const tooltip = screen.getByTestId('tooltip')
158+
expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars')
159+
})
160+
161+
it('formats large numbers correctly', () => {
162+
const largeNumberIcons: Icon = { starsCount: 1500000 }
163+
render(<DisplayIcon item="starsCount" icons={largeNumberIcons} />)
164+
expect(screen.getByText('1.5M')).toBeInTheDocument()
165+
})
166+
})
167+
168+
describe('Default Values and Fallbacks', () => {
169+
it('handles items not in ICONS constant gracefully', () => {
170+
const testIcons: Icon = { unknownItem: 'test' }
171+
172+
render(<DisplayIcon item="unknownItem" icons={testIcons} />)
173+
174+
const tooltip = screen.getByTestId('tooltip')
175+
expect(tooltip).toHaveAttribute('data-tooltip-content', 'Unknown')
176+
})
177+
178+
it('applies base classes even without special item types', () => {
179+
render(<DisplayIcon item="license" icons={mockIcons} />)
180+
const tooltipContainer = screen.getByTestId('tooltip').querySelector('div')
181+
expect(tooltipContainer).toHaveClass(
182+
'flex',
183+
'flex-row-reverse',
184+
'items-center',
185+
'justify-center'
186+
)
187+
})
188+
})
189+
190+
describe('Edge Cases and Invalid Inputs', () => {
191+
it('throws error when icons object is null', () => {
192+
expect(() => {
193+
render(<DisplayIcon item="starsCount" icons={null as never} />)
194+
}).toThrow('Cannot read properties of null')
195+
})
196+
197+
it('throws error when icons object is undefined', () => {
198+
expect(() => {
199+
render(<DisplayIcon item="starsCount" icons={undefined as never} />)
200+
}).toThrow('Cannot read properties of undefined')
201+
})
202+
203+
it('handles empty string item', () => {
204+
const { container } = render(<DisplayIcon item="" icons={mockIcons} />)
205+
expect(container.firstChild).toBeNull()
206+
})
207+
208+
it('handles zero values correctly', () => {
209+
const zeroIcons: Icon = { starsCount: 0 }
210+
const { container } = render(<DisplayIcon item="starsCount" icons={zeroIcons} />)
211+
expect(container.firstChild).toBeNull()
212+
})
213+
214+
it('handles negative numbers', () => {
215+
const negativeIcons: Icon = { starsCount: -5 }
216+
render(<DisplayIcon item="starsCount" icons={negativeIcons} />)
217+
expect(screen.getByText('-5')).toBeInTheDocument()
218+
})
219+
220+
it('handles very large numbers', () => {
221+
const largeIcons: Icon = { starsCount: 1500000000 }
222+
render(<DisplayIcon item="starsCount" icons={largeIcons} />)
223+
expect(screen.getByText('1.5B')).toBeInTheDocument()
224+
})
225+
})
226+
227+
describe('DOM Structure and Classes', () => {
228+
it('has correct base container structure', () => {
229+
render(<DisplayIcon item="license" icons={mockIcons} />)
230+
const tooltip = screen.getByTestId('tooltip')
231+
const containerDiv = tooltip.querySelector('div')
232+
233+
expect(containerDiv).toHaveClass(
234+
'flex',
235+
'flex-row-reverse',
236+
'items-center',
237+
'justify-center',
238+
'gap-1',
239+
'px-4',
240+
'pb-1',
241+
'-ml-2'
242+
)
243+
})
244+
245+
it('applies rotate-container class for stars items', () => {
246+
const { rerender } = render(<DisplayIcon item="starsCount" icons={mockIcons} />)
247+
let tooltipContainer = screen.getByTestId('tooltip').querySelector('div')
248+
expect(tooltipContainer).toHaveClass('rotate-container')
249+
250+
rerender(<DisplayIcon item="starsCount" icons={mockIcons} />)
251+
tooltipContainer = screen.getByTestId('tooltip').querySelector('div')
252+
expect(tooltipContainer).toHaveClass('rotate-container')
253+
})
254+
255+
it('applies flip-container class for forks and contributors items', () => {
256+
const testCases = [
257+
{ item: 'forksCount', value: 100 },
258+
{ item: 'contributors_count', value: 50 },
259+
{ item: 'contributionCount', value: 30 },
260+
]
261+
262+
testCases.forEach(({ item, value }) => {
263+
const iconsWithItem: Icon = { [item]: value }
264+
const { container } = render(<DisplayIcon item={item} icons={iconsWithItem} />)
265+
const containerDiv = container.querySelector('div[class*="flip-container"]')
266+
expect(containerDiv).toBeInTheDocument()
267+
})
268+
})
269+
270+
it('applies correct icon classes', () => {
271+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
272+
const icon = screen.getByTestId('font-awesome-icon')
273+
expect(icon).toHaveClass('text-gray-600', 'dark:text-gray-300', 'icon-rotate')
274+
})
275+
276+
it('applies correct text span classes', () => {
277+
render(<DisplayIcon item="license" icons={mockIcons} />)
278+
const textSpan = screen.getByText('MIT')
279+
expect(textSpan).toHaveClass('text-gray-600', 'dark:text-gray-300')
280+
})
281+
})
282+
283+
describe('Accessibility', () => {
284+
it('provides tooltip with descriptive content', () => {
285+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
286+
const tooltip = screen.getByTestId('tooltip')
287+
expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars')
288+
})
289+
290+
it('has proper tooltip configuration', () => {
291+
render(<DisplayIcon item="forksCount" icons={mockIcons} />)
292+
const tooltip = screen.getByTestId('tooltip')
293+
expect(tooltip).toHaveAttribute('data-delay', '150')
294+
expect(tooltip).toHaveAttribute('data-close-delay', '100')
295+
expect(tooltip).toHaveAttribute('data-show-arrow', 'true')
296+
expect(tooltip).toHaveAttribute('data-placement', 'top')
297+
})
298+
})
299+
300+
describe('Internal Logic', () => {
301+
it('correctly determines numeric vs string values', () => {
302+
const mixedIcons: Icon = {
303+
starsCount: 1000,
304+
license: 'Apache-2.0',
305+
}
306+
307+
const { rerender } = render(<DisplayIcon item="starsCount" icons={mixedIcons} />)
308+
expect(screen.getByText('1.0k')).toBeInTheDocument()
309+
310+
rerender(<DisplayIcon item="license" icons={mixedIcons} />)
311+
expect(screen.getByText('Apache-2.0')).toBeInTheDocument()
312+
})
313+
314+
it('filters and joins className arrays correctly', () => {
315+
render(<DisplayIcon item="license" icons={mockIcons} />)
316+
const tooltipContainer = screen.getByTestId('tooltip').querySelector('div')
317+
const classes = tooltipContainer?.className.split(' ') || []
318+
319+
expect(classes.filter((cls) => cls === '')).toHaveLength(0)
320+
})
321+
})
322+
323+
describe('Event Handling', () => {
324+
it('renders interactive tooltip that responds to user events', async () => {
325+
const user = userEvent.setup()
326+
render(<DisplayIcon item="starsCount" icons={mockIcons} />)
327+
328+
const tooltip = screen.getByTestId('tooltip')
329+
expect(tooltip).toBeInTheDocument()
330+
331+
await user.tab()
332+
expect(tooltip).toBeInTheDocument()
333+
334+
await user.hover(tooltip)
335+
expect(tooltip).toBeInTheDocument()
336+
337+
await user.unhover(tooltip)
338+
expect(tooltip).toBeInTheDocument()
339+
340+
expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars')
341+
expect(tooltip).toHaveAttribute('data-show-arrow', 'true')
342+
})
343+
344+
it('maintains tooltip accessibility during interactions', async () => {
345+
const user = userEvent.setup()
346+
render(<DisplayIcon item="forksCount" icons={mockIcons} />)
347+
348+
const tooltip = screen.getByTestId('tooltip')
349+
350+
await user.tab()
351+
expect(tooltip).toBeInTheDocument()
352+
353+
expect(tooltip).toHaveAttribute('data-placement', 'top')
354+
expect(tooltip).toHaveAttribute('data-delay', '150')
355+
356+
const iconElement = screen.getByTestId('font-awesome-icon')
357+
const textContent = screen.getByText('350')
358+
359+
expect(iconElement).toBeInTheDocument()
360+
expect(textContent).toBeInTheDocument()
361+
})
362+
})
363+
})

0 commit comments

Comments
 (0)