-
-
Couldn't load subscription status.
- Fork 246
feat: add comprehensive unit tests for DisplayIcon component #2048
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ac1dfa9
30632f7
449df0d
1089f08
6ad08c9
7cdbaad
77cd89a
20ea907
222eb8b
04f2a3b
32d54e9
790299c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,322 @@ | ||
| import { render, screen } from '@testing-library/react' | ||
| import userEvent from '@testing-library/user-event' | ||
| import type { Icon } from 'types/icon' | ||
| import DisplayIcon from 'components/DisplayIcon' | ||
|
|
||
| jest.mock('@heroui/tooltip', () => ({ | ||
| Tooltip: ({ children, content, delay, closeDelay, showArrow, placement }: never) => ( | ||
| <div | ||
| data-testid="tooltip" | ||
| data-tooltip-content={content} | ||
| data-delay={delay} | ||
| data-close-delay={closeDelay} | ||
| data-show-arrow={showArrow} | ||
| data-placement={placement} | ||
| > | ||
| {children} | ||
| </div> | ||
| ), | ||
| })) | ||
|
|
||
| jest.mock('millify', () => ({ | ||
| millify: jest.fn((value: number, options?: { precision: number }) => { | ||
| if (value >= 1000000000) return `${(value / 1000000000).toFixed(options?.precision || 1)}B` | ||
| if (value >= 1000000) return `${(value / 1000000).toFixed(options?.precision || 1)}M` | ||
| if (value >= 1000) return `${(value / 1000).toFixed(options?.precision || 1)}k` | ||
| return value.toString() | ||
| }), | ||
| })) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| jest.mock('wrappers/FontAwesomeIconWrapper', () => { | ||
| return function MockFontAwesomeIconWrapper({ className, icon }: never) { | ||
| return <span data-testid="font-awesome-icon" data-icon={icon} className={className} /> | ||
| } | ||
| }) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| jest.mock('utils/data', () => ({ | ||
| ICONS: { | ||
| starsCount: { label: 'Stars', icon: 'fa-star' }, | ||
| forksCount: { label: 'Forks', icon: 'fa-code-fork' }, | ||
| contributorsCount: { label: 'Contributors', icon: 'fa-users' }, | ||
| contributionCount: { label: 'Contributors', icon: 'fa-users' }, | ||
| issuesCount: { label: 'Issues', icon: 'fa-exclamation-circle' }, | ||
| license: { label: 'License', icon: 'fa-balance-scale' }, | ||
| unknownItem: { label: 'Unknown', icon: 'fa-question' }, | ||
| }, | ||
| iconKeys: 'starsCount' as never, | ||
| })) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| describe('DisplayIcon', () => { | ||
| const mockIcons: Icon = { | ||
| starsCount: 1250, | ||
| forksCount: 350, | ||
| contributorsCount: 25, | ||
| contributionCount: 25, | ||
| issuesCount: 42, | ||
| license: 'MIT', | ||
| } | ||
|
|
||
| describe('Basic Rendering', () => { | ||
| it('renders successfully with minimal required props', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| expect(screen.getByTestId('tooltip')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('renders nothing when item is not in icons object', () => { | ||
| const { container } = render(<DisplayIcon item="nonexistentItem" icons={mockIcons} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
|
|
||
| it('renders nothing when icons object is empty', () => { | ||
| const { container } = render(<DisplayIcon item="starsCount" icons={{}} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Conditional Rendering Logic', () => { | ||
| it('renders when item exists in icons object', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| expect(screen.getByTestId('tooltip')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('does not render when item does not exist in icons object', () => { | ||
| const { container } = render(<DisplayIcon item="nonexistent" icons={mockIcons} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
|
|
||
| it('does not render when icons[item] is falsy', () => { | ||
| const iconsWithFalsy: Icon = { ...mockIcons, starsCount: 0 } | ||
| const { container } = render(<DisplayIcon item="starsCount" icons={iconsWithFalsy} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Prop-based Behavior', () => { | ||
| it('displays correct icon based on item prop', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| const icon = screen.getByTestId('font-awesome-icon') | ||
| expect(icon).toHaveAttribute('data-icon', 'fa-star') | ||
| }) | ||
|
|
||
| it('displays different icons for different items', () => { | ||
| const { rerender } = render(<DisplayIcon item="forksCount" icons={mockIcons} />) | ||
| expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-code-fork') | ||
|
|
||
| rerender(<DisplayIcon item="contributorsCount" icons={mockIcons} />) | ||
| expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-users') | ||
| }) | ||
|
|
||
| it('applies different container classes based on item type', () => { | ||
| const { rerender, container } = render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| let containerDiv = container.querySelector('div[class*="rotate-container"]') | ||
| expect(containerDiv).toBeInTheDocument() | ||
|
|
||
| rerender(<DisplayIcon item="forksCount" icons={mockIcons} />) | ||
| containerDiv = container.querySelector('div[class*="flip-container"]') | ||
| expect(containerDiv).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('applies different icon classes based on item type', () => { | ||
| const { rerender } = render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| let icon = screen.getByTestId('font-awesome-icon') | ||
| expect(icon).toHaveClass('icon-rotate') | ||
|
|
||
| rerender(<DisplayIcon item="forksCount" icons={mockIcons} />) | ||
| icon = screen.getByTestId('font-awesome-icon') | ||
| expect(icon).toHaveClass('icon-flip') | ||
| }) | ||
| }) | ||
|
|
||
| describe('Text and Content Rendering', () => { | ||
| it('displays formatted numbers using millify for numeric values', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| expect(screen.getByText('1.3k')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('displays string values as-is', () => { | ||
| render(<DisplayIcon item="license" icons={mockIcons} />) | ||
| expect(screen.getByText('MIT')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('displays tooltip with correct label', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| const tooltip = screen.getByTestId('tooltip') | ||
| expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars') | ||
| }) | ||
|
|
||
| it('formats large numbers correctly', () => { | ||
| const largeNumberIcons: Icon = { starsCount: 1500000 } | ||
| render(<DisplayIcon item="starsCount" icons={largeNumberIcons} />) | ||
| expect(screen.getByText('1.5M')).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Default Values and Fallbacks', () => { | ||
| it('handles items not in ICONS constant gracefully', () => { | ||
| const testIcons: Icon = { unknownItem: 'test' } | ||
|
|
||
| render(<DisplayIcon item="unknownItem" icons={testIcons} />) | ||
|
|
||
| const tooltip = screen.getByTestId('tooltip') | ||
| expect(tooltip).toHaveAttribute('data-tooltip-content', 'Unknown') | ||
| }) | ||
|
|
||
| it('applies base classes even without special item types', () => { | ||
| render(<DisplayIcon item="license" icons={mockIcons} />) | ||
| const tooltipContainer = screen.getByTestId('tooltip').querySelector('div') | ||
| expect(tooltipContainer).toHaveClass( | ||
| 'flex', | ||
| 'flex-row-reverse', | ||
| 'items-center', | ||
| 'justify-center' | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Edge Cases and Invalid Inputs', () => { | ||
| it('throws error when icons object is null', () => { | ||
| expect(() => { | ||
| render(<DisplayIcon item="starsCount" icons={null as never} />) | ||
| }).toThrow('Cannot read properties of null') | ||
| }) | ||
|
|
||
| it('throws error when icons object is undefined', () => { | ||
| expect(() => { | ||
| render(<DisplayIcon item="starsCount" icons={undefined as never} />) | ||
| }).toThrow('Cannot read properties of undefined') | ||
| }) | ||
|
Comment on lines
+191
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider graceful handling instead of throwing errors for null/undefined icons. The current tests expect the component to throw errors for null/undefined
Consider updating the component to handle these cases without throwing: - it('throws error when icons object is null', () => {
- expect(() => {
- render(<DisplayIcon item="starsCount" icons={null as never} />)
- }).toThrow('Cannot read properties of null')
- })
+ it('handles null icons object gracefully', () => {
+ const { container } = render(<DisplayIcon item="starsCount" icons={null as never} />)
+ expect(container.firstChild).toBeNull()
+ })
- it('throws error when icons object is undefined', () => {
- expect(() => {
- render(<DisplayIcon item="starsCount" icons={undefined as never} />)
- }).toThrow('Cannot read properties of undefined')
- })
+ it('handles undefined icons object gracefully', () => {
+ const { container } = render(<DisplayIcon item="starsCount" icons={undefined as never} />)
+ expect(container.firstChild).toBeNull()
+ })This would require updating the component to add a null check: // In DisplayIcon component
if (!icons || !icons[item]) return null🤖 Prompt for AI Agents |
||
|
|
||
| it('handles empty string item', () => { | ||
| const { container } = render(<DisplayIcon item="" icons={mockIcons} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
|
|
||
| it('handles zero values correctly', () => { | ||
| const zeroIcons: Icon = { starsCount: 0 } | ||
| const { container } = render(<DisplayIcon item="starsCount" icons={zeroIcons} />) | ||
| expect(container.firstChild).toBeNull() | ||
| }) | ||
|
|
||
| it('handles negative numbers', () => { | ||
| const negativeIcons: Icon = { starsCount: -5 } | ||
| render(<DisplayIcon item="starsCount" icons={negativeIcons} />) | ||
| expect(screen.getByText('-5')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('handles very large numbers', () => { | ||
| const largeIcons: Icon = { starsCount: 1500000000 } | ||
| render(<DisplayIcon item="starsCount" icons={largeIcons} />) | ||
| expect(screen.getByText('1.5B')).toBeInTheDocument() | ||
| }) | ||
| }) | ||
|
|
||
| describe('DOM Structure and Classes', () => { | ||
| it('has correct base container structure', () => { | ||
| render(<DisplayIcon item="license" icons={mockIcons} />) | ||
| const tooltip = screen.getByTestId('tooltip') | ||
| const containerDiv = tooltip.querySelector('div') | ||
|
|
||
| expect(containerDiv).toHaveClass( | ||
| 'flex', | ||
| 'flex-row-reverse', | ||
| 'items-center', | ||
| 'justify-center', | ||
| 'gap-1', | ||
| 'px-4', | ||
| 'pb-1', | ||
| '-ml-2' | ||
| ) | ||
| }) | ||
|
|
||
| it('applies rotate-container class for stars items', () => { | ||
| const { rerender } = render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| let tooltipContainer = screen.getByTestId('tooltip').querySelector('div') | ||
| expect(tooltipContainer).toHaveClass('rotate-container') | ||
|
|
||
| rerender(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| tooltipContainer = screen.getByTestId('tooltip').querySelector('div') | ||
| expect(tooltipContainer).toHaveClass('rotate-container') | ||
| }) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| it('applies flip-container class for forks and contributors items', () => { | ||
| const testCases = [ | ||
| { item: 'forksCount', value: 100 }, | ||
| { item: 'forksCount', value: 100 }, | ||
| { item: 'contributors_count', value: 50 }, | ||
| { item: 'contributionCount', value: 30 }, | ||
| ] | ||
|
|
||
| testCases.forEach(({ item, value }) => { | ||
| const iconsWithItem: Icon = { [item]: value } | ||
| const { container } = render(<DisplayIcon item={item} icons={iconsWithItem} />) | ||
| const containerDiv = container.querySelector('div[class*="flip-container"]') | ||
| expect(containerDiv).toBeInTheDocument() | ||
| }) | ||
| }) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| it('applies correct icon classes', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| const icon = screen.getByTestId('font-awesome-icon') | ||
| expect(icon).toHaveClass('text-gray-600', 'dark:text-gray-300', 'icon-rotate') | ||
| }) | ||
|
|
||
| it('applies correct text span classes', () => { | ||
| render(<DisplayIcon item="license" icons={mockIcons} />) | ||
| const textSpan = screen.getByText('MIT') | ||
| expect(textSpan).toHaveClass('text-gray-600', 'dark:text-gray-300') | ||
| }) | ||
| }) | ||
|
|
||
| describe('Accessibility', () => { | ||
| it('provides tooltip with descriptive content', () => { | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
| const tooltip = screen.getByTestId('tooltip') | ||
| expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars') | ||
| }) | ||
|
|
||
| it('has proper tooltip configuration', () => { | ||
| render(<DisplayIcon item="forksCount" icons={mockIcons} />) | ||
| const tooltip = screen.getByTestId('tooltip') | ||
| expect(tooltip).toHaveAttribute('data-delay', '150') | ||
| expect(tooltip).toHaveAttribute('data-close-delay', '100') | ||
| expect(tooltip).toHaveAttribute('data-show-arrow', 'true') | ||
| expect(tooltip).toHaveAttribute('data-placement', 'top') | ||
| }) | ||
| }) | ||
|
|
||
| describe('Internal Logic', () => { | ||
| it('correctly determines numeric vs string values', () => { | ||
| const mixedIcons: Icon = { | ||
| starsCount: 1000, | ||
| license: 'Apache-2.0', | ||
| } | ||
|
|
||
| const { rerender } = render(<DisplayIcon item="starsCount" icons={mixedIcons} />) | ||
| expect(screen.getByText('1.0k')).toBeInTheDocument() | ||
|
|
||
| rerender(<DisplayIcon item="license" icons={mixedIcons} />) | ||
| expect(screen.getByText('Apache-2.0')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('filters and joins className arrays correctly', () => { | ||
| render(<DisplayIcon item="license" icons={mockIcons} />) | ||
| const tooltipContainer = screen.getByTestId('tooltip').querySelector('div') | ||
| const classes = tooltipContainer?.className.split(' ') || [] | ||
|
|
||
| expect(classes.filter((cls) => cls === '')).toHaveLength(0) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Event Handling', () => { | ||
| it('renders tooltip wrapper that can handle hover events', async () => { | ||
| const user = userEvent.setup() | ||
| render(<DisplayIcon item="starsCount" icons={mockIcons} />) | ||
|
|
||
| const tooltip = screen.getByTestId('tooltip') | ||
| expect(tooltip).toBeInTheDocument() | ||
|
|
||
| await user.hover(tooltip) | ||
| expect(tooltip).toBeInTheDocument() | ||
| }) | ||
| }) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.