Skip to content

Commit ac1dfa9

Browse files
committed
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
1 parent e137c07 commit ac1dfa9

File tree

1 file changed

+322
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)