From ac1dfa98bc2e4329f5a0171bdd8d19319dce3fe6 Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Tue, 12 Aug 2025 01:15:29 +0530 Subject: [PATCH 1/4] 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 --- .../unit/components/DisplayIcon.test.tsx | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 frontend/__tests__/unit/components/DisplayIcon.test.tsx diff --git a/frontend/__tests__/unit/components/DisplayIcon.test.tsx b/frontend/__tests__/unit/components/DisplayIcon.test.tsx new file mode 100644 index 0000000000..b24f4132fb --- /dev/null +++ b/frontend/__tests__/unit/components/DisplayIcon.test.tsx @@ -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) => ( +
+ {children} +
+ ), +})) + +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() + }), +})) + +jest.mock('wrappers/FontAwesomeIconWrapper', () => { + return function MockFontAwesomeIconWrapper({ className, icon }: never) { + return + } +}) + +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, +})) + +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() + expect(screen.getByTestId('tooltip')).toBeInTheDocument() + }) + + it('renders nothing when item is not in icons object', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('renders nothing when icons object is empty', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + }) + + describe('Conditional Rendering Logic', () => { + it('renders when item exists in icons object', () => { + render() + expect(screen.getByTestId('tooltip')).toBeInTheDocument() + }) + + it('does not render when item does not exist in icons object', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('does not render when icons[item] is falsy', () => { + const iconsWithFalsy: Icon = { ...mockIcons, starsCount: 0 } + const { container } = render() + expect(container.firstChild).toBeNull() + }) + }) + + describe('Prop-based Behavior', () => { + it('displays correct icon based on item prop', () => { + render() + const icon = screen.getByTestId('font-awesome-icon') + expect(icon).toHaveAttribute('data-icon', 'fa-star') + }) + + it('displays different icons for different items', () => { + const { rerender } = render() + expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-code-fork') + + rerender() + expect(screen.getByTestId('font-awesome-icon')).toHaveAttribute('data-icon', 'fa-users') + }) + + it('applies different container classes based on item type', () => { + const { rerender, container } = render() + let containerDiv = container.querySelector('div[class*="rotate-container"]') + expect(containerDiv).toBeInTheDocument() + + rerender() + containerDiv = container.querySelector('div[class*="flip-container"]') + expect(containerDiv).toBeInTheDocument() + }) + + it('applies different icon classes based on item type', () => { + const { rerender } = render() + let icon = screen.getByTestId('font-awesome-icon') + expect(icon).toHaveClass('icon-rotate') + + rerender() + 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() + expect(screen.getByText('1.3k')).toBeInTheDocument() + }) + + it('displays string values as-is', () => { + render() + expect(screen.getByText('MIT')).toBeInTheDocument() + }) + + it('displays tooltip with correct label', () => { + render() + const tooltip = screen.getByTestId('tooltip') + expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars') + }) + + it('formats large numbers correctly', () => { + const largeNumberIcons: Icon = { starsCount: 1500000 } + render() + 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() + + const tooltip = screen.getByTestId('tooltip') + expect(tooltip).toHaveAttribute('data-tooltip-content', 'Unknown') + }) + + it('applies base classes even without special item types', () => { + render() + 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() + }).toThrow('Cannot read properties of null') + }) + + it('throws error when icons object is undefined', () => { + expect(() => { + render() + }).toThrow('Cannot read properties of undefined') + }) + + it('handles empty string item', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('handles zero values correctly', () => { + const zeroIcons: Icon = { starsCount: 0 } + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('handles negative numbers', () => { + const negativeIcons: Icon = { starsCount: -5 } + render() + expect(screen.getByText('-5')).toBeInTheDocument() + }) + + it('handles very large numbers', () => { + const largeIcons: Icon = { starsCount: 1500000000 } + render() + expect(screen.getByText('1.5B')).toBeInTheDocument() + }) + }) + + describe('DOM Structure and Classes', () => { + it('has correct base container structure', () => { + render() + 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() + let tooltipContainer = screen.getByTestId('tooltip').querySelector('div') + expect(tooltipContainer).toHaveClass('rotate-container') + + rerender() + tooltipContainer = screen.getByTestId('tooltip').querySelector('div') + expect(tooltipContainer).toHaveClass('rotate-container') + }) + + 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() + const containerDiv = container.querySelector('div[class*="flip-container"]') + expect(containerDiv).toBeInTheDocument() + }) + }) + + it('applies correct icon classes', () => { + render() + 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() + const textSpan = screen.getByText('MIT') + expect(textSpan).toHaveClass('text-gray-600', 'dark:text-gray-300') + }) + }) + + describe('Accessibility', () => { + it('provides tooltip with descriptive content', () => { + render() + const tooltip = screen.getByTestId('tooltip') + expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars') + }) + + it('has proper tooltip configuration', () => { + render() + 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() + expect(screen.getByText('1.0k')).toBeInTheDocument() + + rerender() + expect(screen.getByText('Apache-2.0')).toBeInTheDocument() + }) + + it('filters and joins className arrays correctly', () => { + render() + 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() + + const tooltip = screen.getByTestId('tooltip') + expect(tooltip).toBeInTheDocument() + + await user.hover(tooltip) + expect(tooltip).toBeInTheDocument() + }) + }) +}) From 30632f7a995db3a66cbb2e1c59b33ade85a0a007 Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Tue, 12 Aug 2025 01:30:14 +0530 Subject: [PATCH 2/4] Fixed issues flagged by the bot --- .../unit/components/DisplayIcon.test.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/__tests__/unit/components/DisplayIcon.test.tsx b/frontend/__tests__/unit/components/DisplayIcon.test.tsx index b24f4132fb..8a70420a50 100644 --- a/frontend/__tests__/unit/components/DisplayIcon.test.tsx +++ b/frontend/__tests__/unit/components/DisplayIcon.test.tsx @@ -308,15 +308,43 @@ describe('DisplayIcon', () => { }) describe('Event Handling', () => { - it('renders tooltip wrapper that can handle hover events', async () => { + it('renders interactive tooltip that responds to user events', async () => { const user = userEvent.setup() render() const tooltip = screen.getByTestId('tooltip') expect(tooltip).toBeInTheDocument() + await user.tab() + expect(tooltip).toBeInTheDocument() + await user.hover(tooltip) expect(tooltip).toBeInTheDocument() + + await user.unhover(tooltip) + expect(tooltip).toBeInTheDocument() + + expect(tooltip).toHaveAttribute('data-tooltip-content', 'Stars') + expect(tooltip).toHaveAttribute('data-show-arrow', 'true') + }) + + it('maintains tooltip accessibility during interactions', async () => { + const user = userEvent.setup() + render() + + const tooltip = screen.getByTestId('tooltip') + + await user.tab() + expect(tooltip).toBeInTheDocument() + + expect(tooltip).toHaveAttribute('data-placement', 'top') + expect(tooltip).toHaveAttribute('data-delay', '150') + + const iconElement = screen.getByTestId('font-awesome-icon') + const textContent = screen.getByText('350') + + expect(iconElement).toBeInTheDocument() + expect(textContent).toBeInTheDocument() }) }) }) From 1089f08911152966990847a657835ada3da6d13e Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Wed, 13 Aug 2025 12:44:03 +0530 Subject: [PATCH 3/4] Fixed issues flagged by the bot --- .../unit/components/DisplayIcon.test.tsx | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/frontend/__tests__/unit/components/DisplayIcon.test.tsx b/frontend/__tests__/unit/components/DisplayIcon.test.tsx index 8a70420a50..8036915fe0 100644 --- a/frontend/__tests__/unit/components/DisplayIcon.test.tsx +++ b/frontend/__tests__/unit/components/DisplayIcon.test.tsx @@ -1,10 +1,25 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import React from 'react' import type { Icon } from 'types/icon' import DisplayIcon from 'components/DisplayIcon' +interface TooltipProps { + children: React.ReactNode + content: string + delay: number + closeDelay: number + showArrow: boolean + placement: string +} + +interface IconWrapperProps { + className?: string + icon: string +} + jest.mock('@heroui/tooltip', () => ({ - Tooltip: ({ children, content, delay, closeDelay, showArrow, placement }: never) => ( + Tooltip: ({ children, content, delay, closeDelay, showArrow, placement }: TooltipProps) => (
({ })) jest.mock('wrappers/FontAwesomeIconWrapper', () => { - return function MockFontAwesomeIconWrapper({ className, icon }: never) { + return function MockFontAwesomeIconWrapper({ className, icon }: IconWrapperProps) { return } }) 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, + 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' }, + } })) describe('DisplayIcon', () => { @@ -240,7 +254,6 @@ describe('DisplayIcon', () => { 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 }, From 7cdbaadddfbe5ed79ab138ab039a288ea29bf705 Mon Sep 17 00:00:00 2001 From: RizWaaN3024 Date: Wed, 13 Aug 2025 12:51:42 +0530 Subject: [PATCH 4/4] Fixed issues flagged by the bot and added unhover word in cspell/custom-dict.txt --- cspell/custom-dict.txt | 1 + .../unit/components/DisplayIcon.test.tsx | 34 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index b9365e4c76..c1ee6544e9 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -117,6 +117,7 @@ superfences tiktok tsc turbopack +unhover usefixtures winsrdf wsgi diff --git a/frontend/__tests__/unit/components/DisplayIcon.test.tsx b/frontend/__tests__/unit/components/DisplayIcon.test.tsx index 8036915fe0..122b2aa03d 100644 --- a/frontend/__tests__/unit/components/DisplayIcon.test.tsx +++ b/frontend/__tests__/unit/components/DisplayIcon.test.tsx @@ -5,17 +5,17 @@ import type { Icon } from 'types/icon' import DisplayIcon from 'components/DisplayIcon' interface TooltipProps { - children: React.ReactNode - content: string - delay: number - closeDelay: number - showArrow: boolean - placement: string + children: React.ReactNode + content: string + delay: number + closeDelay: number + showArrow: boolean + placement: string } interface IconWrapperProps { - className?: string - icon: string + className?: string + icon: string } jest.mock('@heroui/tooltip', () => ({ @@ -49,15 +49,15 @@ jest.mock('wrappers/FontAwesomeIconWrapper', () => { }) 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' }, - } + 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' }, + }, })) describe('DisplayIcon', () => {