diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 740f4466db..492f660486 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -129,6 +129,7 @@ pygoat pymdownx pypoetry pyyaml +quasis relativedelta repositorycontributor requirepass diff --git a/frontend/__tests__/a11y/components/Card.a11y.test.tsx b/frontend/__tests__/a11y/components/Card.a11y.test.tsx index 70bfb52418..4c5626a923 100644 --- a/frontend/__tests__/a11y/components/Card.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/Card.a11y.test.tsx @@ -28,6 +28,7 @@ jest.mock('next/link', () => { }) const baseProps = { + cardKey: 'test-card-1', title: 'Test Project', url: 'https://github.com/test/project', summary: 'This is a test project summary', @@ -52,7 +53,11 @@ describe('Card Accessibility', () => { it('should not have any accessibility violations when level is provided', async () => { const { container } = render( - + ) const results = await axe(container) @@ -61,7 +66,9 @@ describe('Card Accessibility', () => { }) it('should not have any accessibility violations when project name is provided', async () => { - const { container } = render() + const { container } = render( + + ) const results = await axe(container) @@ -72,6 +79,7 @@ describe('Card Accessibility', () => { const { container } = render( ) diff --git a/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx b/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx index b1e63f91ae..ce41a34d0c 100644 --- a/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/CardDetailsPage.a11y.test.tsx @@ -182,6 +182,7 @@ describe('CardDetailsPage a11y', () => { endedAt: '2025-03-01', mentors: [ { + id: 'mentor-mentor1', login: 'mentor1', avatarUrl: 'https://avatars.githubusercontent.com/u/12345', name: 'Mentor One', diff --git a/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx b/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx index b6fa9d9e8f..35f8a4f7a0 100644 --- a/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/ContributorAvatar.a11y.test.tsx @@ -36,6 +36,7 @@ jest.mock('next/link', () => { }) const mockGitHubContributor: Contributor = { + id: 'contributor-jane-doe', login: 'jane-doe', name: 'Jane Doe', avatarUrl: 'https://avatars.githubusercontent.com/u/12345', diff --git a/frontend/__tests__/a11y/components/LogoCarousel.a11y.test.tsx b/frontend/__tests__/a11y/components/LogoCarousel.a11y.test.tsx index a278123818..3694a9c2b1 100644 --- a/frontend/__tests__/a11y/components/LogoCarousel.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/LogoCarousel.a11y.test.tsx @@ -30,18 +30,21 @@ jest.mock('next/link', () => { const mockSponsors: Sponsor[] = [ { + id: 'sponsor-1-a11y', name: 'Test Sponsor 1', imageUrl: 'https://example.com/logo1.png', url: 'https://sponsor1.com', sponsorType: 'Gold', }, { + id: 'sponsor-2-a11y', name: 'Test Sponsor 2', imageUrl: 'https://example.com/logo2.png', url: 'https://sponsor2.com', sponsorType: 'Silver', }, { + id: 'sponsor-3-a11y', name: 'Test Sponsor 3', imageUrl: '', url: 'https://sponsor3.com', diff --git a/frontend/__tests__/a11y/components/SingleModuleCard.a11y.test.tsx b/frontend/__tests__/a11y/components/SingleModuleCard.a11y.test.tsx index 1ccdc65634..ca75b7e1bf 100644 --- a/frontend/__tests__/a11y/components/SingleModuleCard.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/SingleModuleCard.a11y.test.tsx @@ -46,11 +46,13 @@ const mockModule: Module = { experienceLevel: ExperienceLevelEnum.Intermediate, mentors: [ { + id: 'mentor-user1-a11y', name: 'user1', login: 'user1', avatarUrl: 'https://example.com/avatar1.jpg', }, { + id: 'mentor-user2-a11y', name: 'user2', login: 'user2', avatarUrl: 'https://example.com/avatar2.jpg', diff --git a/frontend/__tests__/a11y/components/TopContributorsList.a11y.test.tsx b/frontend/__tests__/a11y/components/TopContributorsList.a11y.test.tsx index a763ea4e34..093b44acfa 100644 --- a/frontend/__tests__/a11y/components/TopContributorsList.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/TopContributorsList.a11y.test.tsx @@ -28,6 +28,7 @@ jest.mock('next/link', () => { const mockContributors: Contributor[] = [ { + id: 'contributor-developer1-a11y', avatarUrl: 'https://github.com/developer1.avatar', login: 'developer1', name: 'Alex Developer', @@ -35,6 +36,7 @@ const mockContributors: Contributor[] = [ contributionsCount: 50, }, { + id: 'contributor-contributor2-a11y', avatarUrl: 'https://github.com/contributor2.avatar', login: 'contributor2', name: 'Jane Developer', @@ -42,6 +44,7 @@ const mockContributors: Contributor[] = [ contributionsCount: 30, }, { + id: 'contributor-user3-a11y', avatarUrl: 'https://github.com/user3.avatar', login: 'user3', name: '', diff --git a/frontend/__tests__/mockData/mockChapterData.ts b/frontend/__tests__/mockData/mockChapterData.ts index 6e6449e902..9a9765ec76 100644 --- a/frontend/__tests__/mockData/mockChapterData.ts +++ b/frontend/__tests__/mockData/mockChapterData.ts @@ -12,6 +12,7 @@ export const mockChapterData = { ], topContributors: [ { + id: 'contributor-isanori-sakanashi', avatarUrl: 'https://avatars.githubusercontent.com/u/58754211?v=4', login: 'Isanori-Sakanashi', name: 'Isanori Sakanashi', diff --git a/frontend/__tests__/unit/components/Card.test.tsx b/frontend/__tests__/unit/components/Card.test.tsx index 2abe0c477d..d527a4bed9 100644 --- a/frontend/__tests__/unit/components/Card.test.tsx +++ b/frontend/__tests__/unit/components/Card.test.tsx @@ -50,6 +50,7 @@ interface MockMarkdownProps { } interface MockLabelListProps { + entityKey?: string labels: string[] maxVisible?: number className?: string @@ -185,14 +186,19 @@ jest.mock('components/MarkdownWrapper', () => { jest.mock('components/LabelList', () => { return { - LabelList: ({ labels, maxVisible = 5, className }: MockLabelListProps) => { + LabelList: ({ + labels, + entityKey: _entityKey, + maxVisible = 5, + className, + }: MockLabelListProps) => { if (!labels || labels.length === 0) return null const visibleLabels = labels.slice(0, maxVisible) const remainingCount = labels.length - maxVisible return (
- {visibleLabels.map((label, index) => ( - + {visibleLabels.map((label) => ( + {label} ))} @@ -237,6 +243,7 @@ jest.mock('utils/data', () => ({ describe('Card', () => { const baseProps: CardProps = { + cardKey: 'test-project', title: 'Test Project', url: 'https://github.com/test/project', summary: 'This is a test project summary', @@ -369,12 +376,14 @@ describe('Card', () => { ...baseProps, topContributors: [ { + id: 'contributor-user1', login: 'user1', name: 'User One', avatarUrl: 'https://github.com/user1.png', projectKey: 'project1', }, { + id: 'contributor-user2', login: 'user2', name: 'User Two', avatarUrl: 'https://github.com/user2.png', @@ -415,12 +424,14 @@ describe('Card', () => { ...baseProps, topContributors: [ { + id: 'contributor-anonymous', login: '', name: 'Anonymous', avatarUrl: 'https://github.com/user1.png', projectKey: 'project-key-1', }, { + id: 'contributor-user2-partial', login: 'user2', name: 'User Two', avatarUrl: 'https://github.com/user2.png', @@ -447,6 +458,7 @@ describe('Card', () => { ...baseProps, topContributors: [ { + id: 'contributor-singleuser', login: 'singleuser', name: 'Single User', avatarUrl: 'https://github.com/single.png', @@ -567,6 +579,7 @@ describe('Card', () => { social: [{ title: 'GitHub', url: 'https://github.com/full', icon: MockIcon as IconType }], topContributors: [ { + id: 'contributor-expert', login: 'expert', avatarUrl: 'https://github.com/expert.png', name: 'John Doe', diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx index b387ad4270..4f44c9ec05 100644 --- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx +++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx @@ -169,7 +169,15 @@ jest.mock('components/InfoBlock', () => ({ jest.mock('components/LeadersList', () => ({ __esModule: true, - default: ({ leaders, ...props }: { leaders: string; [key: string]: unknown }) => ( + default: ({ + leaders, + entityKey: _entityKey, + ...props + }: { + leaders: string + entityKey: string + [key: string]: unknown + }) => ( {leaders} @@ -336,11 +344,13 @@ jest.mock('components/ToggleableList', () => ({ items, icon: _icon, label, + entityKey: _entityKey, ...props }: { items: string[] _icon: unknown label: React.ReactNode + entityKey: string [key: string]: unknown }) => (
@@ -454,6 +464,7 @@ describe('CardDetailsPage', () => { const mockContributors = [ { + id: 'contributor-1', avatarUrl: 'https://example.com/avatar1.jpg', login: 'john_doe', name: 'John Doe', @@ -461,6 +472,7 @@ describe('CardDetailsPage', () => { contributionsCount: 50, }, { + id: 'contributor-2', avatarUrl: 'https://example.com/avatar2.jpg', login: 'jane_smith', name: 'Jane Smith', @@ -552,6 +564,7 @@ describe('CardDetailsPage', () => { const mockRecentReleases = [ { + id: 'release-1', author: mockUser, isPreRelease: false, name: 'v1.0.0', @@ -1669,6 +1682,7 @@ describe('CardDetailsPage', () => { contributionStats, topContributors: [ { + id: 'contributor-user1', login: 'user1', name: 'User One', avatarUrl: 'https://example.com/avatar1.png', diff --git a/frontend/__tests__/unit/components/ContributorAvatar.test.tsx b/frontend/__tests__/unit/components/ContributorAvatar.test.tsx index e5eecbd315..e59736edd9 100644 --- a/frontend/__tests__/unit/components/ContributorAvatar.test.tsx +++ b/frontend/__tests__/unit/components/ContributorAvatar.test.tsx @@ -64,6 +64,7 @@ jest.mock('next/image', () => { }) const mockAlgoliaContributor: Contributor = { + id: 'contributor-johndoe', login: 'johndoe', name: 'John Doe', avatarUrl: 'https://github.com/johndoe.png', @@ -72,6 +73,7 @@ const mockAlgoliaContributor: Contributor = { } const mockGitHubContributor: Contributor = { + id: 'contributor-jane-doe', login: 'jane-doe', name: 'Jane Doe', avatarUrl: 'https://avatars.githubusercontent.com/u/12345', @@ -130,6 +132,7 @@ describe('ContributorAvatar', () => { it('shows only name when no contributions count', () => { const contributorWithoutContributions = { + id: 'contributor-newbie', login: 'newbie', name: 'New Contributor', avatarUrl: 'https://github.com/newbie.png', @@ -147,6 +150,7 @@ describe('ContributorAvatar', () => { it('handles contributor with zero contributions', () => { const contributorWithZeroContributions: Contributor = { + id: 'contributor-newcomer', login: 'newcomer', name: 'Brand New User', avatarUrl: 'https://github.com/newcomer.png', @@ -165,6 +169,7 @@ describe('ContributorAvatar', () => { it('handles empty string values gracefully', () => { const contributorWithEmptyStrings: Contributor = { + id: 'contributor-empty-strings', login: '', name: '', avatarUrl: 'https://github.com/default.png', @@ -182,6 +187,7 @@ describe('ContributorAvatar', () => { it('handles very long names and contributions', () => { const contributorWithLongData: Contributor = { + id: 'contributor-long-data', login: 'very-long-username-that-might-break-layouts', name: 'Someone With A Really Really Long Name That Might Cause Issues', avatarUrl: 'https://github.com/very-long-username-that-might-break-layouts.png', diff --git a/frontend/__tests__/unit/components/ItemCardList.test.tsx b/frontend/__tests__/unit/components/ItemCardList.test.tsx index aab37ed096..1abdcd1c1d 100644 --- a/frontend/__tests__/unit/components/ItemCardList.test.tsx +++ b/frontend/__tests__/unit/components/ItemCardList.test.tsx @@ -167,6 +167,7 @@ const mockPullRequest: PullRequest = { } const mockRelease: Release = { + id: 'release-item-card', author: { ...mockUser, login: 'author4', diff --git a/frontend/__tests__/unit/components/LeadersList.test.tsx b/frontend/__tests__/unit/components/LeadersList.test.tsx index 55debe9df1..6ad4b546de 100644 --- a/frontend/__tests__/unit/components/LeadersList.test.tsx +++ b/frontend/__tests__/unit/components/LeadersList.test.tsx @@ -40,6 +40,7 @@ jest.mock('components/TruncatedText', () => ({ describe('LeadersList Component', () => { const defaultProps: LeadersListProps = { + entityKey: 'test-entity', leaders: 'John Doe, Jane Smith, Bob Johnson', } @@ -49,7 +50,7 @@ describe('LeadersList Component', () => { describe('Renders successfully with minimal required props', () => { it('renders with valid leaders string', () => { - render() + render() expect(screen.getByText('John Doe')).toBeInTheDocument() expect(screen.getByTestId('truncated-text')).toBeInTheDocument() }) @@ -62,31 +63,31 @@ describe('LeadersList Component', () => { describe('Conditional rendering logic', () => { it('renders "Unknown" when leaders prop is empty string', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() expect(screen.queryByTestId('truncated-text')).not.toBeInTheDocument() }) it('renders "Unknown" when leaders prop is null', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() expect(screen.queryByTestId('truncated-text')).not.toBeInTheDocument() }) it('renders "Unknown" when leaders prop is undefined', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() expect(screen.queryByTestId('truncated-text')).not.toBeInTheDocument() }) it('renders "Unknown" when leaders prop is only whitespace', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() expect(screen.queryByTestId('truncated-text')).not.toBeInTheDocument() }) it('renders leaders when valid non-empty string is provided', () => { - render() + render() expect(screen.queryByText('Unknown')).not.toBeInTheDocument() expect(screen.getByTestId('truncated-text')).toBeInTheDocument() }) @@ -94,7 +95,7 @@ describe('LeadersList Component', () => { describe('Prop-based behavior', () => { it('renders single leader correctly', () => { - render() + render() expect(screen.getByText('John Doe')).toBeInTheDocument() expect(screen.getAllByTestId('leader-link')).toHaveLength(1) }) @@ -108,14 +109,16 @@ describe('LeadersList Component', () => { }) it('handles leaders with extra whitespace', () => { - render() + render() expect(screen.getByText('John Doe')).toBeInTheDocument() expect(screen.getByText('Jane Smith')).toBeInTheDocument() expect(screen.getAllByTestId('leader-link')).toHaveLength(2) }) it('handles leaders with special characters', () => { - render() + render( + + ) expect(screen.getByText("John O'Connor")).toBeInTheDocument() expect(screen.getByText('María García')).toBeInTheDocument() expect(screen.getByText('Jean-Paul Dubois')).toBeInTheDocument() @@ -137,7 +140,7 @@ describe('LeadersList Component', () => { }) it('does not add comma after single leader', () => { - const { container } = render() + const { container } = render() const textContent = container.textContent expect(textContent).toBe('John Doe') expect(textContent).not.toContain(',') @@ -151,7 +154,7 @@ describe('LeadersList Component', () => { }) it('applies correct CSS classes to links', () => { - render() + render() const link = screen.getByTestId('leader-link') expect(link).toHaveClass('text-gray-600', 'hover:underline', 'dark:text-gray-400') }) @@ -166,7 +169,7 @@ describe('LeadersList Component', () => { }) it('properly encodes special characters in URLs', () => { - render() + render() const links = screen.getAllByTestId('leader-link') expect(links[0]).toHaveAttribute('href', "/members?q=John%20O'Connor") @@ -194,7 +197,7 @@ describe('LeadersList Component', () => { }) it('ensures links are focusable and have proper attributes', () => { - render() + render() const link = screen.getByTestId('leader-link') expect(link).toHaveAttribute('href') @@ -205,38 +208,37 @@ describe('LeadersList Component', () => { describe('Handles edge cases and invalid inputs', () => { it('handles empty array from split (single comma)', () => { - render() - const { container } = render() - expect(container.textContent).toBe(', ') + render() + expect(screen.getByText('Unknown')).toBeInTheDocument() }) it('handles multiple consecutive commas', () => { - render() + render() const links = screen.getAllByTestId('leader-link') - expect(links).toHaveLength(3) // John Doe, empty string, Jane Smith + expect(links).toHaveLength(2) // John Doe, Jane Smith (empty strings filtered out) }) it('handles trailing comma', () => { - render() + render() const links = screen.getAllByTestId('leader-link') - expect(links).toHaveLength(3) // John Doe, Jane Smith, empty string + expect(links).toHaveLength(2) // John Doe, Jane Smith (empty strings filtered out) }) it('handles leading comma', () => { - render() + render() const links = screen.getAllByTestId('leader-link') - expect(links).toHaveLength(3) // empty string, John Doe, Jane Smith + expect(links).toHaveLength(2) // John Doe, Jane Smith (empty strings filtered out) }) it('handles very long leader names', () => { const longName = 'A'.repeat(100) - render() + render() expect(screen.getByText(longName)).toBeInTheDocument() expect(screen.getByTestId('leader-link')).toHaveAttribute('title', longName) }) it('handles numeric strings as leader names', () => { - render() + render() expect(screen.getByText('123')).toBeInTheDocument() expect(screen.getByText('456')).toBeInTheDocument() }) @@ -244,17 +246,17 @@ describe('LeadersList Component', () => { describe('Default values and fallbacks', () => { it('shows Unknown when no valid leaders are provided', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() }) it('handles undefined gracefully', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() }) it('handles null gracefully', () => { - render() + render() expect(screen.getByText('Unknown')).toBeInTheDocument() }) }) diff --git a/frontend/__tests__/unit/components/LogoCarousel.test.tsx b/frontend/__tests__/unit/components/LogoCarousel.test.tsx index eee363af11..964334de99 100644 --- a/frontend/__tests__/unit/components/LogoCarousel.test.tsx +++ b/frontend/__tests__/unit/components/LogoCarousel.test.tsx @@ -44,18 +44,21 @@ jest.mock('next/image', () => { const mockSponsors: Sponsor[] = [ { + id: 'sponsor-1', name: 'Test Sponsor 1', imageUrl: 'https://example.com/logo1.png', url: 'https://sponsor1.com', sponsorType: 'Gold', }, { + id: 'sponsor-2', name: 'Test Sponsor 2', imageUrl: 'https://example.com/logo2.png', url: 'https://sponsor2.com', sponsorType: 'Silver', }, { + id: 'sponsor-3', name: 'Test Sponsor 3', imageUrl: '', url: 'https://sponsor3.com', @@ -65,6 +68,7 @@ const mockSponsors: Sponsor[] = [ const mockSponsorsWithoutImages: Sponsor[] = [ { + id: 'sponsor-no-image', name: 'No Image Sponsor', imageUrl: '', url: 'https://noimage.com', @@ -129,6 +133,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('renders different sponsors based on props', () => { const customSponsors: Sponsor[] = [ { + id: 'sponsor-custom', name: 'Custom Sponsor', imageUrl: 'https://custom.com/logo.png', url: 'https://custom.com', @@ -230,7 +235,13 @@ describe('MovingLogos (LogoCarousel)', () => { const newSponsors = [ ...mockSponsors, - { name: 'New Sponsor', imageUrl: '', url: 'https://new.com', sponsorType: 'Bronze' }, + { + id: 'sponsor-new', + name: 'New Sponsor', + imageUrl: '', + url: 'https://new.com', + sponsorType: 'Bronze', + }, ] rerender() @@ -281,6 +292,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('uses generic fallback alt text when sponsor name is missing', () => { const sponsorWithoutName: Sponsor[] = [ { + id: 'sponsor-no-name', name: '', imageUrl: 'https://example.com/logo.png', url: 'https://example.com', @@ -338,6 +350,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('handles sponsors with very long names', () => { const longNameSponsors: Sponsor[] = [ { + id: 'sponsor-long-name', name: 'A'.repeat(1000), imageUrl: 'https://example.com/logo.png', url: 'https://example.com', @@ -355,6 +368,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('handles sponsors with special characters in names', () => { const specialCharSponsors: Sponsor[] = [ { + id: 'sponsor-special-chars', name: 'Sponsor & Co. (Ltd.) - "Special" ', imageUrl: 'https://example.com/logo.png', url: 'https://example.com', @@ -378,6 +392,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('handles invalid URLs gracefully', () => { const invalidUrlSponsors: Sponsor[] = [ { + id: 'sponsor-invalid-url', name: 'Invalid URL Sponsor', imageUrl: 'not-a-valid-url', url: 'also-not-valid', @@ -398,6 +413,7 @@ describe('MovingLogos (LogoCarousel)', () => { it('handles very large number of sponsors', () => { const manySponsors: Sponsor[] = Array.from({ length: 100 }, (_, i) => ({ + id: `sponsor-${i}`, name: `Sponsor ${i}`, imageUrl: `https://example.com/logo${i}.png`, url: `https://sponsor${i}.com`, diff --git a/frontend/__tests__/unit/components/MultiSearch.test.tsx b/frontend/__tests__/unit/components/MultiSearch.test.tsx index 7d268cd19b..5ac489b8cb 100644 --- a/frontend/__tests__/unit/components/MultiSearch.test.tsx +++ b/frontend/__tests__/unit/components/MultiSearch.test.tsx @@ -324,11 +324,35 @@ describe('Rendering', () => { }) it('filters event data based on query', async () => { - const eventData = [ - { name: 'JavaScript Conference', url: 'https://example.com/js' }, - { name: 'Python Workshop', url: 'https://example.com/py' }, - { name: 'React Meetup', url: 'https://example.com/react' }, - ] as Event[] + const eventData: Event[] = [ + { + id: 'event-1', + name: 'JavaScript Conference', + url: 'https://example.com/js', + objectID: 'event-1', + key: 'js-conf', + category: 'other', + startDate: '2024-01-01', + }, + { + id: 'event-2', + name: 'Python Workshop', + url: 'https://example.com/py', + objectID: 'event-2', + key: 'py-workshop', + category: 'other', + startDate: '2024-02-01', + }, + { + id: 'event-3', + name: 'React Meetup', + url: 'https://example.com/react', + objectID: 'event-3', + key: 'react-meetup', + category: 'other', + startDate: '2024-03-01', + }, + ] const user = userEvent.setup() render() diff --git a/frontend/__tests__/unit/components/RecentRelease.test.tsx b/frontend/__tests__/unit/components/RecentRelease.test.tsx index 380e611bb4..5108750b20 100644 --- a/frontend/__tests__/unit/components/RecentRelease.test.tsx +++ b/frontend/__tests__/unit/components/RecentRelease.test.tsx @@ -67,6 +67,7 @@ jest.mock('next/image', () => ({ const now = Date.now() const mockReleases: Release[] = [ { + id: 'release-recent-1', name: 'v1.0 The First Release', publishedAt: now, repositoryName: 'our-awesome-project', @@ -87,6 +88,7 @@ const mockReleases: Release[] = [ }, }, { + id: 'release-recent-2', name: 'v2.0 The Second Release', publishedAt: now, repositoryName: 'another-cool-project', diff --git a/frontend/__tests__/unit/components/Release.test.tsx b/frontend/__tests__/unit/components/Release.test.tsx index 7e324c56c9..83037dbdea 100644 --- a/frontend/__tests__/unit/components/Release.test.tsx +++ b/frontend/__tests__/unit/components/Release.test.tsx @@ -71,6 +71,7 @@ jest.mock('next/image', () => ({ const now = Date.now() const mockReleases: ReleaseType[] = [ { + id: 'release-test-1', name: 'v1.0 The First Release', publishedAt: now, repositoryName: 'our-awesome-project', @@ -91,6 +92,7 @@ const mockReleases: ReleaseType[] = [ }, }, { + id: 'release-test-2', name: 'v2.0 The Second Release', publishedAt: now, repositoryName: 'another-cool-project', diff --git a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx index e356e6bf13..91e1adc393 100644 --- a/frontend/__tests__/unit/components/SingleModuleCard.test.tsx +++ b/frontend/__tests__/unit/components/SingleModuleCard.test.tsx @@ -93,11 +93,13 @@ const mockModule: Module = { experienceLevel: ExperienceLevelEnum.Intermediate, mentors: [ { + id: 'mentor-user1', name: 'user1', login: 'user1', avatarUrl: 'https://example.com/avatar1.jpg', }, { + id: 'mentor-user2', name: 'user2', login: 'user2', avatarUrl: 'https://example.com/avatar2.jpg', diff --git a/frontend/__tests__/unit/components/ToggleableList.test.tsx b/frontend/__tests__/unit/components/ToggleableList.test.tsx index 55876d51d8..32a45f4c8a 100644 --- a/frontend/__tests__/unit/components/ToggleableList.test.tsx +++ b/frontend/__tests__/unit/components/ToggleableList.test.tsx @@ -37,7 +37,7 @@ describe('ToggleableList', () => { }) it('renders with limited props initially', () => { - render() + render() // First 10 items should be visible for (const item of mockItems.slice(0, 10)) { @@ -51,7 +51,7 @@ describe('ToggleableList', () => { }) it('renders with an icon', () => { - render() + render() const iconElement = screen.getByTestId('react-icon') expect(iconElement).toBeInTheDocument() @@ -59,7 +59,7 @@ describe('ToggleableList', () => { }) it('respects custom limit prop', () => { - render() + render() expect(screen.getByText('Item 1')).toBeInTheDocument() expect(screen.getByText('Item 2')).toBeInTheDocument() @@ -69,19 +69,19 @@ describe('ToggleableList', () => { it('does not show Show More button when item count is less than the limit', () => { const limitedItems = mockItems.slice(0, 5) - render() + render() expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument() }) it('shows Show More button when items exceed limit', () => { - render() + render() expect(screen.getByRole('button', { name: /show more/i })).toBeInTheDocument() }) it('expands to show all items when ShowMoreButton is clicked', () => { - render() + render() // Initially hidden items expect(screen.queryByText('Item 6')).not.toBeInTheDocument() @@ -96,7 +96,9 @@ describe('ToggleableList', () => { }) it('collapses back to limited view when ShowMoreButton is clicked again', () => { - render() + render( + + ) // Expand fireEvent.click(screen.getByRole('button', { name: /show more/i })) @@ -109,21 +111,28 @@ describe('ToggleableList', () => { }) it('navigates on item button click', () => { - render() + render( + + ) const button = screen.getByText('React') fireEvent.click(button) expect(mockPush).toHaveBeenCalledWith('/projects?q=React') }) it('handles empty items array', () => { - render() + render() expect(screen.getByText('Empty List')).toBeInTheDocument() expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument() }) it('handles single item', () => { - render() + render() expect(screen.getByText('Single Item')).toBeInTheDocument() expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument() @@ -131,14 +140,14 @@ describe('ToggleableList', () => { it('handles items exactly equal to limit', () => { const exactItems = Array.from({ length: 5 }, (_, i) => `Item ${i + 1}`) - render() + render() expect(screen.getByText('Item 5')).toBeInTheDocument() expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument() }) it('handles limit of 0', () => { - render() + render() // Should show ShowMoreButton since limit is exceeded expect(screen.getByRole('button', { name: /show more/i })).toBeInTheDocument() for (const item of mockItems) { @@ -148,7 +157,7 @@ describe('ToggleableList', () => { it('properly encodes special character in item names', () => { const itemsWithSpecialChars = ['C++', 'C#', 'Node.js & Express'] - render() + render() const specialButton = screen.getByText('C++') fireEvent.click(specialButton) @@ -156,20 +165,22 @@ describe('ToggleableList', () => { }) it('applies correct CSS classes to main container', () => { - const { container } = render() + const { container } = render( + + ) const mainDiv = container.firstChild expect(mainDiv).toHaveClass('rounded-lg', 'bg-gray-100', 'p-6', 'shadow-md', 'dark:bg-gray-800') }) it('applies correct CSS classes to header', () => { - render() + render() const header = screen.getByRole('heading', { level: 2 }) expect(header).toHaveClass('mb-4', 'text-2xl', 'font-semibold') }) it('applies correct CSS to button items (no underline, no transition, only hover background)', () => { const randomItems = ['React', 'Vue', 'Angular'] - render() + render() const button = screen.getByText('React') expect(button).toHaveClass( 'rounded-lg', diff --git a/frontend/__tests__/unit/components/TopContributorsList.test.tsx b/frontend/__tests__/unit/components/TopContributorsList.test.tsx index 83d374cf7f..1ac8d2ec00 100644 --- a/frontend/__tests__/unit/components/TopContributorsList.test.tsx +++ b/frontend/__tests__/unit/components/TopContributorsList.test.tsx @@ -156,6 +156,7 @@ jest.mock('react-icons/fa6', () => ({ const mockContributors: Contributor[] = [ { + id: 'contributor-developer1', avatarUrl: 'https://github.com/developer1.avatar', login: 'developer1', name: 'Alex Developer', @@ -163,6 +164,7 @@ const mockContributors: Contributor[] = [ contributionsCount: 50, }, { + id: 'contributor-contributor2', avatarUrl: 'https://github.com/contributor2.avatar', login: 'contributor2', name: 'Jane Developer', @@ -170,6 +172,7 @@ const mockContributors: Contributor[] = [ contributionsCount: 30, }, { + id: 'contributor-user3', avatarUrl: 'https://github.com/user3.avatar', login: 'user3', name: '', @@ -256,12 +259,14 @@ describe('TopContributorsList Component', () => { it('renders contributor name when available, falls back to login', () => { const contributorsWithMissingNames: Contributor[] = [ { + id: 'contributor-dev1', avatarUrl: 'https://github.com/developer1.avatar', login: 'developer1', name: 'Alex Developer', projectKey: 'project1', }, { + id: 'contributor-contrib2', avatarUrl: 'https://github.com/contributor2.avatar', login: 'contributor2', name: '', @@ -438,6 +443,7 @@ describe('TopContributorsList Component', () => { it('falls back to login when name is empty or missing', () => { const contributorWithoutName: Contributor[] = [ { + id: 'contributor-testuser', avatarUrl: 'https://github.com/user1.avatar', login: 'testuser', name: '', @@ -453,6 +459,7 @@ describe('TopContributorsList Component', () => { it('handles missing avatar URL gracefully', () => { const contributorWithEmptyAvatar: Contributor[] = [ { + id: 'contributor-dev1-empty-avatar', avatarUrl: '', login: 'developer1', name: 'Alex Developer', @@ -505,12 +512,14 @@ describe('TopContributorsList Component', () => { it('handles contributors with missing required fields', () => { const incompleteContributors: Contributor[] = [ { + id: 'contributor-incomplete1', avatarUrl: 'https://github.com/user1.avatar', login: '', name: '', projectKey: 'project1', }, { + id: 'contributor-incomplete2', avatarUrl: '', login: 'user2', name: 'User 2', diff --git a/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx b/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx index 95c7256f9d..058c8565bf 100644 --- a/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx +++ b/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx @@ -25,7 +25,7 @@ jest.mock('app/global-error', () => ({ // Mock components jest.mock('components/LabelList', () => ({ - LabelList: ({ labels }: { labels: string[] }) => ( + LabelList: ({ labels, entityKey: _entityKey }: { labels: string[]; entityKey: string }) => (
{labels.join(', ')}
), })) diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index d183cf8088..4a2192d198 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -160,6 +160,7 @@ const eslintConfig = [ 'nest/no-global-nan': 'error', 'nest/no-global-parsefloat': 'error', 'nest/no-global-parseint': 'error', + 'react/no-array-index-key': 'error', quotes: ['error', 'single', { avoidEscape: true }], }, }, @@ -169,6 +170,12 @@ const eslintConfig = [ 'no-console': 'off', }, }, + { + files: ['**/skeletons/**/*.{ts,tsx,js,jsx}', '**/*.skeleton.{ts,tsx,js,jsx}'], + rules: { + 'react/no-array-index-key': 'off', + }, + }, ] export default eslintConfig diff --git a/frontend/src/app/chapters/page.tsx b/frontend/src/app/chapters/page.tsx index 03236429e4..2c050132f2 100644 --- a/frontend/src/app/chapters/page.tsx +++ b/frontend/src/app/chapters/page.tsx @@ -64,6 +64,7 @@ const ChaptersPage = () => { return ( { return ( { return ( { return ( { />
- {snapshot.newChapters.filter((chapter) => chapter.isActive).map(renderChapterCard)} + {snapshot.newChapters + .filter((chapter) => chapter.isActive) + .map((chapter) => ( + {renderChapterCard(chapter)} + ))}
)} @@ -160,7 +164,11 @@ const SnapshotDetailsPage: React.FC = () => { New Projects
- {snapshot.newProjects.filter((project) => project.isActive).map(renderProjectCard)} + {snapshot.newProjects + .filter((project) => project.isActive) + .map((project) => ( + {renderProjectCard(project)} + ))}
)} @@ -171,14 +179,16 @@ const SnapshotDetailsPage: React.FC = () => { New Releases
- {snapshot.newReleases.map((release, index) => ( - - ))} + {snapshot.newReleases.map((release, index) => { + return ( + + ) + })}
)} diff --git a/frontend/src/app/contribute/page.tsx b/frontend/src/app/contribute/page.tsx index 7e9bc47e0e..d2663bc1f4 100644 --- a/frontend/src/app/contribute/page.tsx +++ b/frontend/src/app/contribute/page.tsx @@ -44,6 +44,7 @@ const ContributePage = () => { { labels={issue.labels} /> setModalOpenIndex(null)} title={issue.title} diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index 0ef117f121..f7c95d2175 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -58,13 +58,13 @@ const UserDetailsPage: React.FC = () => { fetchData() }, [memberKey, user]) - const formattedBio = user?.bio?.split(' ').map((word, index) => { + const formattedBio = user?.bio?.split(' ').map((word) => { const mentionMatch = word.match(/^@([\w-]+(?:\.[\w-]+)*)([^\w@])?$/) if (mentionMatch && mentionMatch.length > 1) { const username = mentionMatch[1] const punctuation = mentionMatch[2] || '' return ( - + { ) } - return {word} + return {word} }) if (isLoading) { diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx index 666cd9a937..27a1a02998 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx @@ -11,6 +11,7 @@ import { ErrorDisplay } from 'app/global-error' import { GetModuleIssueViewDocument } from 'types/__generated__/issueQueries.generated' import ActionButton from 'components/ActionButton' import AnchorTitle from 'components/AnchorTitle' +import { LabelList } from 'components/LabelList' import LoadingSpinner from 'components/LoadingSpinner' import Markdown from 'components/MarkdownWrapper' import SecondaryCard from 'components/SecondaryCard' @@ -93,9 +94,6 @@ const ModuleIssueDetailsPage = () => { : 'border-gray-300 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800' }` - const labelButtonClassName = - 'rounded-lg border border-gray-400 px-3 py-1 text-sm hover:bg-gray-200 dark:border-gray-300 dark:hover:bg-gray-700' - if (error) { return } @@ -105,8 +103,6 @@ const ModuleIssueDetailsPage = () => { const assignees = issue.assignees || [] const labels = issue.labels || [] - const visibleLabels = labels.slice(0, 5) - const remainingLabels = labels.length - visibleLabels.length const canEditDeadline = assignees.length > 0 let issueStatusClass: string @@ -273,16 +269,7 @@ const ModuleIssueDetailsPage = () => { Labels -
- {visibleLabels.map((label, index) => ( - - {label} - - ))} - {remainingLabels > 0 && ( - +{remainingLabels} more - )} -
+ {assignees.length > 0 && ( diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx index 2a0e70e1e0..a3f54807e9 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/mentees/[menteeKey]/page.tsx @@ -157,13 +157,21 @@ const MenteeProfilePage = () => {
{menteeDetails.domains && menteeDetails.domains.length > 0 && ( - + )} {menteeDetails.tags && menteeDetails.tags.length > 0 && ( - + )}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8520e8481c..5b92c37f9d 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -162,7 +162,7 @@ export default function Home() { >
{data.upcomingEvents.map((event: Event, index: number) => ( -
+
@@ -288,7 +291,10 @@ export default function Home() { {project.leaders.length > 0 && (
- +
)}
@@ -359,7 +365,7 @@ export default function Home() {
- +
diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx index 920bd3f019..8f09f8faf2 100644 --- a/frontend/src/app/projects/page.tsx +++ b/frontend/src/app/projects/page.tsx @@ -47,6 +47,7 @@ const ProjectsPage = () => { return ( { return (
@@ -66,10 +67,10 @@ const Card = ({ {/* Icons associated with the project */} {icons && Object.keys(ICONS).some((key) => icons[key]) && (
- {Object.keys(ICONS).map((key, index) => + {Object.keys(ICONS).map((key) => icons[key] ? ( value))} /> @@ -133,7 +134,7 @@ const Card = ({ {/* Labels Section */} {labels && labels.length > 0 && (
- +
)} {/* Contributors section */} diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 6ced0d1490..ad9178d15a 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -164,7 +164,10 @@ const DetailsCard = ({ detail?.label === 'Leaders' ? (
{detail.label}:{' '} - +
) : (
@@ -218,13 +221,19 @@ const DetailsCard = ({ > {languages.length !== 0 && ( } /> )} {topics.length !== 0 && ( - } /> + } + /> )}
)} @@ -236,6 +245,7 @@ const DetailsCard = ({ > {tags?.length > 0 && ( } @@ -244,6 +254,7 @@ const DetailsCard = ({ )} {domains?.length > 0 && ( } @@ -255,6 +266,7 @@ const DetailsCard = ({ {labels?.length > 0 && (
} diff --git a/frontend/src/components/LabelList.tsx b/frontend/src/components/LabelList.tsx index f562fe8f6e..dd7e771bba 100644 --- a/frontend/src/components/LabelList.tsx +++ b/frontend/src/components/LabelList.tsx @@ -16,21 +16,26 @@ const Label: React.FC = ({ label, className = '' }) => { } interface LabelListProps { + entityKey: string labels: string[] maxVisible?: number className?: string } -const LabelList: React.FC = ({ labels, maxVisible = 5, className = '' }) => { +const LabelList: React.FC = ({ + entityKey, + labels, + maxVisible = 5, + className = '', +}) => { if (!labels || labels.length === 0) return null const visibleLabels = labels.slice(0, maxVisible) const remainingCount = labels.length - maxVisible - return (
- {visibleLabels.map((label, index) => ( -
diff --git a/frontend/src/components/LeadersList.tsx b/frontend/src/components/LeadersList.tsx index d79b3ae623..ddb8ad39e3 100644 --- a/frontend/src/components/LeadersList.tsx +++ b/frontend/src/components/LeadersList.tsx @@ -12,15 +12,20 @@ import { TruncatedText } from 'components/TruncatedText' * @returns {JSX.Element} A list of leader links */ -const LeadersList = ({ leaders }: LeadersListProps) => { +const LeadersList = ({ entityKey, leaders }: LeadersListProps) => { if (!leaders || leaders.trim() === '') return <>Unknown - const leadersArray = leaders.split(',').map((leader) => leader.trim()) + const leadersArray = leaders + .split(',') + .map((leader) => leader.trim()) + .filter((leader) => leader !== '') + + if (leadersArray.length === 0) return <>Unknown return ( {leadersArray.map((leader, index) => ( - + {sponsors.map((sponsor, index) => (
`/my/mentorship/programs/${programKey}/modules/${moduleKey}/mentees/${login}` @@ -50,8 +49,11 @@ const MenteeContributorsList = ({ } >
- {displayContributors.map((item, index) => ( -
+ {displayContributors.map((item) => ( +
{item?.name = ({ openIssues, closedIssues, m
{issue.labels && issue.labels.length > 0 && ( -
- {issue.labels.slice(0, 3).map((label, index) => ( - - {label} - - ))} - {issue.labels.length > 3 && ( - - +{issue.labels.length - 3} more - - )} +
+
)} diff --git a/frontend/src/components/MultiSearch.tsx b/frontend/src/components/MultiSearch.tsx index c0d09ab376..63f1859085 100644 --- a/frontend/src/components/MultiSearch.tsx +++ b/frontend/src/components/MultiSearch.tsx @@ -264,7 +264,7 @@ const MultiSearchBar: React.FC = ({
    {suggestion.hits.map((hit, subIndex) => (
  • - {link.submenu?.map((submenu, idx) => ( + {link.submenu?.map((submenu) => ( = ({ Prev {pageNumbers.map((number, index) => ( - + // eslint-disable-next-line react/no-array-index-key + {number === '...' ? (
    - {(showAll ? items : items.slice(0, limit)).map((item, index) => ( + {(showAll ? items : items.slice(0, limit)).map((item) => (