diff --git a/frontend/__tests__/unit/components/CalendarButton.test.tsx b/frontend/__tests__/unit/components/CalendarButton.test.tsx
index fa8607b9de..f27c485014 100644
--- a/frontend/__tests__/unit/components/CalendarButton.test.tsx
+++ b/frontend/__tests__/unit/components/CalendarButton.test.tsx
@@ -172,6 +172,12 @@ describe('CalendarButton', () => {
const button = screen.getByRole('button')
expect(button).toHaveAttribute('aria-label', 'Add Untitled to Calendar')
})
+
+ it('uses "event" as fallback when title is missing', () => {
+ render()
+ const button = screen.getByRole('button')
+ expect(button).toHaveAttribute('aria-label', 'Add event to Calendar')
+ })
})
describe('className prop', () => {
diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
index 2692f9072a..1293412174 100644
--- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
+++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
@@ -2,10 +2,19 @@ import { render, screen, cleanup, fireEvent } from '@testing-library/react'
import React from 'react'
import '@testing-library/jest-dom'
import { FaCode, FaTags } from 'react-icons/fa6'
+import type { MenteeNode } from 'types/__generated__/graphql'
import type { DetailsCardProps } from 'types/card'
import type { PullRequest } from 'types/pullRequest'
import CardDetailsPage, { type CardType } from 'components/CardDetailsPage'
+jest.mock('@heroui/tooltip', () => ({
+ Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) => (
+
+ {children}
+
+ ),
+}))
+
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
@@ -406,11 +415,11 @@ jest.mock('components/ContributorsList', () => ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
icon,
title = 'Contributors',
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
+
getUrl,
...props
}: {
- contributors: unknown[]
+ contributors: (Partial & { tag?: string; login?: string; name?: string })[]
icon?: unknown
title?: string
maxInitialDisplay: number
@@ -419,6 +428,11 @@ jest.mock('components/ContributorsList', () => ({
}) => (
),
}))
@@ -2351,9 +2365,33 @@ describe('CardDetailsPage', () => {
render()
- const allContributorsLists = screen.getAllByTestId('contributors-list')
- const menteesSection = allContributorsLists.find((el) => el.textContent?.includes('Mentees'))
- expect(menteesSection).toHaveTextContent('Mentees (1 items, max display: 6)')
+ const menteeLink = screen.getByText('Test Mentee')
+ expect(menteeLink).toBeInTheDocument()
+ expect(menteeLink).toHaveAttribute('href', '/programs/program-key-123/mentees/test_mentee')
+ })
+
+ it('renders mentee links with empty program key segment when programKey is undefined', () => {
+ const mentees = [
+ {
+ id: 'mentee-1',
+ login: 'test_mentee',
+ name: 'Test Mentee',
+ avatarUrl: 'https://example.com/mentee.jpg',
+ },
+ ]
+
+ const propsWithMentees: DetailsCardProps = {
+ ...defaultProps,
+ mentees,
+ programKey: undefined,
+ entityKey: undefined,
+ }
+
+ render()
+
+ const menteeLink = screen.getByText('Test Mentee')
+ expect(menteeLink).toBeInTheDocument()
+ expect(menteeLink).toHaveAttribute('href', '/programs//mentees/test_mentee')
})
it('handles null/undefined mentees array gracefully', () => {
diff --git a/frontend/__tests__/unit/components/EntityActions.test.tsx b/frontend/__tests__/unit/components/EntityActions.test.tsx
index adc25f80d5..b06e30f85f 100644
--- a/frontend/__tests__/unit/components/EntityActions.test.tsx
+++ b/frontend/__tests__/unit/components/EntityActions.test.tsx
@@ -716,4 +716,26 @@ describe('EntityActions', () => {
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program/edit')
})
+ it('does nothing when an unhandled key is pressed', () => {
+ render()
+ const button = screen.getByRole('button', { name: /Program actions menu/ })
+ fireEvent.click(button)
+
+ const menu = screen.getByRole('menu')
+ fireEvent.keyDown(menu, { key: 'a' })
+ expect(button).toHaveAttribute('aria-expanded', 'true')
+ })
+
+ describe('Toggle Behavior', () => {
+ it('closes the dropdown and resets focus when toggled off via click', () => {
+ render()
+ const button = screen.getByRole('button', { name: /Program actions menu/ })
+
+ fireEvent.click(button)
+ expect(button).toHaveAttribute('aria-expanded', 'true')
+
+ fireEvent.click(button)
+ expect(button).toHaveAttribute('aria-expanded', 'false')
+ })
+ })
})
diff --git a/frontend/__tests__/unit/components/InfoBlock.test.tsx b/frontend/__tests__/unit/components/InfoBlock.test.tsx
index adba6175ac..7186e0b326 100644
--- a/frontend/__tests__/unit/components/InfoBlock.test.tsx
+++ b/frontend/__tests__/unit/components/InfoBlock.test.tsx
@@ -265,6 +265,15 @@ describe('InfoBlock Component', () => {
expect(mockMillify).toHaveBeenCalledWith(1234, { precision: 1 })
})
+
+ it('should use default value of 0 when value is not provided', () => {
+ mockMillify.mockReturnValue('0')
+ mockPluralize.mockReturnValue('items')
+
+ render()
+
+ expect(screen.getByText('No items')).toBeInTheDocument()
+ })
})
describe('Text and content rendering', () => {
diff --git a/frontend/__tests__/unit/components/ItemCardList.test.tsx b/frontend/__tests__/unit/components/ItemCardList.test.tsx
index 46f60eaf72..2c9964cda4 100644
--- a/frontend/__tests__/unit/components/ItemCardList.test.tsx
+++ b/frontend/__tests__/unit/components/ItemCardList.test.tsx
@@ -858,4 +858,164 @@ describe('ItemCardList Component', () => {
expect(tooltip).toHaveAttribute('data-id', 'avatar-tooltip-0')
})
})
+ describe('Additional Code Coverage', () => {
+ it('shows fallback avatar when author exists but avatarUrl is missing', () => {
+ const issueNoAvatarUrl = {
+ ...mockIssue,
+ author: {
+ ...mockIssue.author,
+ avatarUrl: '',
+ },
+ }
+
+ render(
+
+ )
+
+ expect(screen.queryByTestId('avatar-image')).not.toBeInTheDocument()
+
+ const links = screen.getAllByTestId('link')
+ const profileLink = links.find(
+ (link) => link.getAttribute('href') === `/members/${mockIssue.author.login}`
+ )
+ expect(profileLink).toBeInTheDocument()
+ })
+
+ it('renders avatar without link when login is missing but name exists', () => {
+ const authorNoLogin = {
+ avatarUrl: 'https://example.com/avatar.png',
+ name: 'Just Name',
+ login: '',
+ }
+
+ const issueNoLogin = {
+ ...mockIssue,
+ author: authorNoLogin,
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ expect(screen.getByTestId('avatar-image')).toBeInTheDocument()
+ const links = screen.queryAllByTestId('link')
+ const profileLink = links.find((link) => link.getAttribute('href')?.startsWith('/members/'))
+ expect(profileLink).toBeUndefined()
+ })
+
+ it('handles item with no title and no name gracefully', () => {
+ const bareItem = {
+ id: 'bare-item',
+ author: mockUser,
+ url: 'https://example.com',
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ const truncatedText = screen.getByTestId('truncated-text')
+ expect(truncatedText).toHaveTextContent('')
+ })
+
+ it('handles item with no identifiers for key generation coverage', () => {
+ const noIdItem = {
+ author: mockUser,
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ const truncatedText = screen.getByTestId('truncated-text')
+ expect(truncatedText).toHaveTextContent('')
+ })
+
+ it('handles item with no URL, no title, no name for TruncatedText coverage', () => {
+ const noUrlNoInfoItem = {
+ id: 'no-url-item',
+ author: {
+ ...mockUser,
+ login: '',
+ },
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ const truncatedText = screen.getByTestId('truncated-text')
+ expect(truncatedText).toHaveTextContent('')
+ expect(screen.queryByTestId('link')).not.toBeInTheDocument()
+ })
+
+ it('handles item with URL and name (but no title) correctly', () => {
+ const itemWithNameAndUrl = {
+ id: 'name-only-link-item',
+ author: mockUser,
+ url: 'https://example.com/name',
+ name: 'Item Name',
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ const links = screen.getAllByTestId('link')
+ const itemLink = links.find((l) => l.getAttribute('href') === 'https://example.com/name')
+ expect(itemLink).toBeInTheDocument()
+ expect(itemLink).toHaveTextContent('Item Name')
+ })
+
+ it('handles item with URL but no title and no name', () => {
+ const itemWithUrlOnly = {
+ id: 'url-only-item',
+ author: mockUser,
+ url: 'https://example.com/empty',
+ } as unknown as Issue
+
+ render(
+
+ )
+
+ const links = screen.getAllByTestId('link')
+ const itemLink = links.find((l) => l.getAttribute('href') === 'https://example.com/empty')
+ expect(itemLink).toBeInTheDocument()
+ expect(itemLink).toHaveTextContent('')
+ })
+ })
})
diff --git a/frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx b/frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx
index cc8dd149ed..a43ef263db 100644
--- a/frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx
+++ b/frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx
@@ -158,5 +158,18 @@ describe('MentorshipPullRequest Component', () => {
expect(links[0]).toHaveAttribute('target', '_blank')
expect(links[0]).toHaveAttribute('rel', 'noopener noreferrer')
})
+ test('renders Unknown alt text when author login is empty but avatar exists', () => {
+ const mockPrWithAvatarButNoLogin = {
+ ...mockPullRequestOpen,
+ author: {
+ ...mockPullRequestOpen.author,
+ login: '',
+ },
+ } as unknown as PullRequest
+
+ render()
+ const avatar = screen.getByAltText('Unknown')
+ expect(avatar).toBeInTheDocument()
+ })
})
})
diff --git a/frontend/__tests__/unit/components/MetricsCard.test.tsx b/frontend/__tests__/unit/components/MetricsCard.test.tsx
index fc06a5327f..6f33e80f7c 100644
--- a/frontend/__tests__/unit/components/MetricsCard.test.tsx
+++ b/frontend/__tests__/unit/components/MetricsCard.test.tsx
@@ -62,6 +62,7 @@ describe('MetricsCard component', () => {
[75, 'bg-green-500'],
[60, 'bg-orange-500'],
[50, 'bg-orange-500'],
+ [74, 'bg-orange-500'],
[30, 'bg-red-500'],
]
diff --git a/frontend/__tests__/unit/components/ModuleCard.test.tsx b/frontend/__tests__/unit/components/ModuleCard.test.tsx
index 000f93a8a7..4d78e825dd 100644
--- a/frontend/__tests__/unit/components/ModuleCard.test.tsx
+++ b/frontend/__tests__/unit/components/ModuleCard.test.tsx
@@ -610,6 +610,59 @@ describe('ModuleCard', () => {
expect(image.getAttribute('alt')).toBe('mentor1')
expect(image.getAttribute('title')).toBe('mentor1')
})
+ it('handles module with undefined mentors and mentees gracefully', () => {
+ const moduleWithUndefined = createMockModule({
+ mentors: undefined,
+ mentees: undefined,
+ } as unknown as Partial)
+
+ const modules = [moduleWithUndefined, createMockModule({ key: 'mod2' })]
+
+ expect(() => render()).not.toThrow()
+ expect(screen.queryByText('Mentors')).not.toBeInTheDocument()
+ expect(screen.queryByText('Mentees')).not.toBeInTheDocument()
+ })
+
+ it('handles invalid avatar URL with query params correctly (separator check)', () => {
+ const mentors = [createMockContributor('mentor1', 'invalid-url?foo=bar')]
+ const modules = [createMockModule({ mentors }), createMockModule({ key: 'mod2' })]
+
+ render()
+
+ const images = screen.getAllByTestId('next-image')
+ expect(images[0].getAttribute('src')).toContain('&s=60')
+ })
+
+ it('uses mentee name for avatar alt and title', () => {
+ const mentees = [
+ createMockContributor('mentee1', 'https://example.com/avatar1.png', 'Jane Doe'),
+ ]
+ const modules = [createMockModule({ mentees }), createMockModule({ key: 'mod2' })]
+
+ render()
+
+ const image = screen.getAllByTestId('next-image')[0]
+ expect(image.getAttribute('alt')).toBe('Jane Doe')
+ expect(image.getAttribute('title')).toBe('Jane Doe')
+ })
+
+ it('falls back to mentee login for avatar alt and title', () => {
+ const mentees = [
+ {
+ id: 'id-mentee1',
+ login: 'mentee1',
+ name: '',
+ avatarUrl: 'https://example.com/avatar1.png',
+ },
+ ]
+ const modules = [createMockModule({ mentees }), createMockModule({ key: 'mod2' })]
+
+ render()
+
+ const image = screen.getAllByTestId('next-image')[0]
+ expect(image.getAttribute('alt')).toBe('mentee1')
+ expect(image.getAttribute('title')).toBe('mentee1')
+ })
})
describe('Path Handling', () => {
@@ -636,7 +689,6 @@ describe('ModuleCard', () => {
mockPathname.mockReturnValue(undefined)
const modules = [createMockModule(), createMockModule({ key: 'mod2' })]
- // Should not throw
expect(() => render()).not.toThrow()
})
diff --git a/frontend/__tests__/unit/components/MultiSearch.test.tsx b/frontend/__tests__/unit/components/MultiSearch.test.tsx
index a349a08065..039b741c9b 100644
--- a/frontend/__tests__/unit/components/MultiSearch.test.tsx
+++ b/frontend/__tests__/unit/components/MultiSearch.test.tsx
@@ -653,18 +653,10 @@ describe('Rendering', () => {
await user.type(input, 'test')
await waitFor(expectListItemsExist)
- // Navigate to first item
await user.keyboard('{ArrowDown}')
const listItems = screen.getAllByRole('listitem')
expect(listItems[0]).toHaveClass('bg-gray-100')
-
- // Press ArrowUp
await user.keyboard('{ArrowUp}')
-
- // Should still be at first item (or index null if logic allows, but here check it doesn't crash or go weird)
- // Actually MultiSearch logic: if index>0 OR subIndex>0 decrement.
- // If index=0 and subIndex=0, nothing happens in the `if/else if`.
- // So state remains { index: 0, subIndex: 0 }
expect(listItems[0]).toHaveClass('bg-gray-100')
})
})
@@ -957,4 +949,93 @@ describe('Rendering', () => {
removeEventListenerSpy.mockRestore()
})
})
+
+ describe('Coverage Improvements', () => {
+ beforeEach(() => {
+ mockFetchAlgoliaData.mockResolvedValue({
+ hits: [mockChapter],
+ totalPages: 1,
+ })
+ })
+
+ it('uses default empty string for initialValue (line 23)', () => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { initialValue, ...minimalProps } = defaultProps
+ render()
+ const input = screen.getByPlaceholderText('Search...') as HTMLInputElement
+ expect(input.value).toBe('')
+ })
+
+ it('handles undefined eventData during search (line 61)', async () => {
+ const user = userEvent.setup()
+ render()
+
+ const input = screen.getByPlaceholderText('Search...')
+ await user.type(input, 'test')
+
+ await waitFor(() => {
+ expect(mockFetchAlgoliaData).toHaveBeenCalled()
+ })
+ expect(true).toBe(true)
+ })
+
+ it('sets empty string query when suggestion name is missing (line 89)', async () => {
+ const itemNoName = { key: 'no-name-item', name: null } as unknown as Project
+ mockFetchAlgoliaData.mockResolvedValue({
+ hits: [itemNoName],
+ totalPages: 1,
+ })
+
+ const user = userEvent.setup()
+ render()
+
+ const input = screen.getByPlaceholderText('Search...')
+ await user.type(input, 'test')
+
+ await waitFor(() => {
+ expect(screen.getByTestId('fa-folder-icon')).toBeInTheDocument()
+ })
+
+ const icon = await screen.findByTestId('fa-folder-icon')
+ const suggestionBtn = icon.closest('button')
+ if (!suggestionBtn) throw new Error('Suggestion button not found')
+
+ await user.click(suggestionBtn)
+
+ expect(input).toHaveValue('')
+ })
+
+ it('does nothing when pressing ArrowUp with no highlight (line 139)', async () => {
+ const user = userEvent.setup()
+ render()
+
+ const input = screen.getByPlaceholderText('Search...')
+ await user.type(input, 'test')
+ await waitFor(expectListItemsExist)
+
+ await user.keyboard('{ArrowUp}')
+
+ const listItems = screen.getAllByRole('listitem')
+ listItems.forEach((item) => {
+ expect(item).not.toHaveClass('bg-gray-100')
+ })
+ })
+
+ it('does not trigger action on random keys on suggestion (line 199)', async () => {
+ const user = userEvent.setup()
+ render()
+
+ const input = screen.getByPlaceholderText('Search...')
+ await user.type(input, 'test')
+ await waitFor(expectListItemsExist)
+
+ const suggestionBtns = screen.getAllByRole('button', { name: /Test Chapter/i })
+ const suggestionBtn = suggestionBtns[0]
+
+ suggestionBtn.focus()
+ await user.keyboard('a') // Random key
+
+ expect(mockPush).not.toHaveBeenCalled()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/components/NavDropDown.test.tsx b/frontend/__tests__/unit/components/NavDropDown.test.tsx
index 2d10b722e7..9222c312b9 100644
--- a/frontend/__tests__/unit/components/NavDropDown.test.tsx
+++ b/frontend/__tests__/unit/components/NavDropDown.test.tsx
@@ -640,4 +640,39 @@ describe('NavDropdown Component', () => {
removeEventListenerSpy.mockRestore()
})
})
+ describe('Coverage Improvements', () => {
+ it('does nothing when Escape is pressed on dropdown button while closed (line 50)', async () => {
+ const user = userEvent.setup()
+ render()
+ const button = screen.getByRole('button')
+ button.focus()
+
+ await user.keyboard('{Escape}')
+ expect(screen.queryByText('Getting Started')).not.toBeInTheDocument()
+ })
+
+ it('does nothing when random key is pressed on dropdown button (line 50)', async () => {
+ const user = userEvent.setup()
+ render()
+ const button = screen.getByRole('button')
+ button.focus()
+
+ await user.keyboard('a')
+ expect(screen.queryByText('Getting Started')).not.toBeInTheDocument()
+ })
+
+ it('does nothing when random key is pressed on submenu item (line 84)', async () => {
+ const user = userEvent.setup()
+ render()
+
+ const button = screen.getByRole('button')
+ await user.click(button) // Open it
+
+ const submenuItem = screen.getByText('Getting Started')
+ submenuItem.focus()
+
+ await user.keyboard('a')
+ expect(screen.getByText('Getting Started')).toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/components/ProgramForm.test.tsx b/frontend/__tests__/unit/components/ProgramForm.test.tsx
index 64762cb7e1..47b0f4f722 100644
--- a/frontend/__tests__/unit/components/ProgramForm.test.tsx
+++ b/frontend/__tests__/unit/components/ProgramForm.test.tsx
@@ -1193,5 +1193,64 @@ describe('ProgramForm Component', () => {
expect(mockOnSubmit).toHaveBeenCalled()
})
})
+
+ test('handles string menteesLimit in validation', async () => {
+ const user = userEvent.setup()
+ const stringLimitFormData = { ...filledFormData, menteesLimit: '10' as unknown as number }
+
+ render(
+
+ )
+
+ const buttons = screen.getAllByRole('button')
+ const submitButton = buttons.find((btn) => btn.textContent?.includes('Save'))
+ if (submitButton) {
+ await user.click(submitButton)
+ }
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalled()
+ })
+ })
+
+ test('handles undefined menteesLimit in submission', async () => {
+ const user = userEvent.setup()
+ ;(useApolloClient as jest.Mock).mockReturnValue({
+ query: jest.fn().mockResolvedValue({
+ data: { myPrograms: { programs: [] } },
+ }),
+ })
+
+ const undefinedLimitFormData = {
+ ...filledFormData,
+ menteesLimit: undefined as unknown as number,
+ }
+
+ render(
+
+ )
+
+ const buttons = screen.getAllByRole('button')
+ const submitButton = buttons.find((btn) => btn.textContent?.includes('Save'))
+ if (submitButton) {
+ await user.click(submitButton)
+ }
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalled()
+ })
+ })
})
})
diff --git a/frontend/__tests__/unit/components/Release.test.tsx b/frontend/__tests__/unit/components/Release.test.tsx
index a1a1b46ef3..40549caf79 100644
--- a/frontend/__tests__/unit/components/Release.test.tsx
+++ b/frontend/__tests__/unit/components/Release.test.tsx
@@ -367,4 +367,62 @@ describe('Release Component', () => {
const avatarLink = screen.getByAltText('Release author avatar').closest('a')
expect(avatarLink).toHaveAttribute('href', '#')
})
+ it('renders title as plain text when organizationName is missing', () => {
+ const releaseWithoutOrg = { ...mockReleases[0], organizationName: undefined }
+ render()
+
+ const title = screen.getByText('v1.0 The First Release')
+ expect(title).toBeInTheDocument()
+
+ const link = title.closest('a')
+ expect(link).toBeNull()
+ })
+
+ it('renders title as plain text when repositoryName is missing', () => {
+ const releaseWithoutRepo = { ...mockReleases[0], repositoryName: undefined }
+ render()
+
+ const title = screen.getByText('v1.0 The First Release')
+ expect(title).toBeInTheDocument()
+
+ const link = title.closest('a')
+ expect(link).toBeNull()
+ })
+
+ it('renders tag name as plain text when release name AND organizationName are missing', () => {
+ const releaseWithoutOrgAndName = {
+ ...mockReleases[0],
+ organizationName: undefined,
+ name: undefined,
+ }
+ render()
+
+ // Should find the tagName 'v1.0'
+ const title = screen.getByText('v1.0')
+ expect(title).toBeInTheDocument()
+
+ // Should NOT be inside a link
+ const link = title.closest('a')
+ expect(link).toBeNull()
+ })
+
+ it('safely handles keydown on disabled button to verify handler guards', () => {
+ const releaseWithoutOrg = { ...mockReleases[0], organizationName: '' }
+ render()
+
+ const repoButton = screen.getByRole('button')
+
+ fireEvent.keyDown(repoButton, { key: 'Enter' })
+
+ expect(mockRouterPush).not.toHaveBeenCalled()
+ })
+ it('safely handles keydown when repositoryName is missing', () => {
+ const releaseWithoutRepo = { ...mockReleases[0], repositoryName: '' }
+ render()
+
+ const repoButton = screen.getByRole('button')
+ fireEvent.keyDown(repoButton, { key: 'Enter' })
+
+ expect(mockRouterPush).not.toHaveBeenCalled()
+ })
})
diff --git a/frontend/__tests__/unit/components/RepositoryCard.test.tsx b/frontend/__tests__/unit/components/RepositoryCard.test.tsx
index e09a2f7bb3..071e772317 100644
--- a/frontend/__tests__/unit/components/RepositoryCard.test.tsx
+++ b/frontend/__tests__/unit/components/RepositoryCard.test.tsx
@@ -78,6 +78,13 @@ describe('RepositoryCard', () => {
expect(screen.queryByRole('button', { name: /Show/ })).not.toBeInTheDocument()
})
+ it('returns null when repositories prop is missing', () => {
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.grid')).toBeNull()
+ })
+
it('shows first 4 repositories initially when there are more than 4', () => {
const repositories = Array.from({ length: 6 }, (_, i) => createMockRepository(i))
diff --git a/frontend/__tests__/unit/components/Search.test.tsx b/frontend/__tests__/unit/components/Search.test.tsx
index 973ee039fb..a6267b035f 100644
--- a/frontend/__tests__/unit/components/Search.test.tsx
+++ b/frontend/__tests__/unit/components/Search.test.tsx
@@ -324,6 +324,17 @@ describe('SearchBar Component', () => {
expect(input).toHaveValue('')
expect(input).toHaveFocus()
})
+ it('does not send GTM event for whitespace-only input', () => {
+ render()
+ const input = screen.getByPlaceholderText('Search projects...')
+
+ fireEvent.change(input, { target: { value: ' ' } })
+
+ jest.advanceTimersByTime(750)
+
+ expect(mockOnSearch).toHaveBeenCalledWith(' ')
+ expect(sendGTMEvent).not.toHaveBeenCalled()
+ })
})
describe('Keyboard event handling on clear button', () => {
diff --git a/frontend/__tests__/unit/components/SortBy.test.tsx b/frontend/__tests__/unit/components/SortBy.test.tsx
index 9fdc7819b6..2f450e2f21 100644
--- a/frontend/__tests__/unit/components/SortBy.test.tsx
+++ b/frontend/__tests__/unit/components/SortBy.test.tsx
@@ -81,6 +81,17 @@ describe('', () => {
expect(defaultProps.onOrderChange).toHaveBeenCalledWith('desc')
})
+ it('toggles order from desc to asc when the button is clicked', async () => {
+ await act(async () => {
+ render()
+ })
+ await act(async () => {
+ const buttons = screen.getAllByRole('button')
+ fireEvent.click(buttons[1])
+ })
+ expect(defaultProps.onOrderChange).toHaveBeenCalledWith('asc')
+ })
+
it('uses proper accessibility attributes', async () => {
await act(async () => {
render()
diff --git a/frontend/__tests__/unit/components/TruncatedText.test.tsx b/frontend/__tests__/unit/components/TruncatedText.test.tsx
index a21d474893..cb7fc9be93 100644
--- a/frontend/__tests__/unit/components/TruncatedText.test.tsx
+++ b/frontend/__tests__/unit/components/TruncatedText.test.tsx
@@ -1,4 +1,5 @@
import { render, screen, act } from '@testing-library/react'
+import React from 'react'
import { TruncatedText } from 'components/TruncatedText'
type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void
@@ -232,16 +233,31 @@ describe('TruncatedText Component', () => {
})
test('observer.observe is not called when textRef.current is initially null', () => {
- // This is difficult to test directly since React always sets the ref on mount
- // But we can verify the observer behavior with rapid mount/unmount
jest.clearAllMocks()
const { unmount } = render()
expect(mockObserve).toHaveBeenCalledTimes(1)
unmount()
-
- // Verify disconnect was called on unmount
expect(mockDisconnect).toHaveBeenCalledTimes(1)
})
+ test('does not observe when textRef.current is null', () => {
+ const nullRef = {}
+ Object.defineProperty(nullRef, 'current', {
+ get: () => null,
+ set: () => {},
+ configurable: true,
+ })
+
+ const useRefSpy = jest
+ .spyOn(React, 'useRef')
+ .mockReturnValue(nullRef as unknown as React.MutableRefObject)
+
+ try {
+ render()
+ expect(mockObserve).not.toHaveBeenCalled()
+ } finally {
+ useRefSpy.mockRestore()
+ }
+ })
})
diff --git a/frontend/__tests__/unit/components/UserCard.test.tsx b/frontend/__tests__/unit/components/UserCard.test.tsx
index dca1fa0266..6bc8820c1c 100644
--- a/frontend/__tests__/unit/components/UserCard.test.tsx
+++ b/frontend/__tests__/unit/components/UserCard.test.tsx
@@ -439,5 +439,50 @@ describe('UserCard', () => {
expect(screen.queryByTestId('icon-medal')).not.toBeInTheDocument()
})
+
+ it('handles undefined metrics props (all undefined)', () => {
+ const props = { ...defaultProps }
+ Object.assign(props, {
+ followersCount: undefined,
+ repositoriesCount: undefined,
+ badgeCount: undefined,
+ })
+
+ render()
+
+ expect(screen.queryByTestId('icon-users')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('icon-folder-open')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('icon-medal')).not.toBeInTheDocument()
+ })
+
+ it('handles mixed defined and undefined metrics', () => {
+ const props = {
+ ...defaultProps,
+ followersCount: 100,
+ repositoriesCount: undefined,
+ badgeCount: 5,
+ } as UserCardProps
+
+ render()
+
+ expect(screen.getByText('100')).toBeInTheDocument()
+ expect(screen.queryByTestId('icon-folder-open')).not.toBeInTheDocument()
+ expect(screen.getByText('5')).toBeInTheDocument()
+ })
+
+ it('handles undefined followers and badge count when repositories > 0', () => {
+ const props = {
+ ...defaultProps,
+ repositoriesCount: 50,
+ followersCount: undefined,
+ badgeCount: undefined,
+ } as UserCardProps
+
+ render()
+
+ expect(screen.getByText('50')).toBeInTheDocument()
+ expect(screen.queryByTestId('icon-users')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('icon-medal')).not.toBeInTheDocument()
+ })
})
})
diff --git a/frontend/__tests__/unit/components/UserMenu.test.tsx b/frontend/__tests__/unit/components/UserMenu.test.tsx
index 5699851c87..624aa80eb1 100644
--- a/frontend/__tests__/unit/components/UserMenu.test.tsx
+++ b/frontend/__tests__/unit/components/UserMenu.test.tsx
@@ -306,6 +306,46 @@ describe('UserMenu Component', () => {
expect(avatarButton).toHaveAttribute('aria-expanded', 'false')
})
})
+
+ it('does not close dropdown when clicking inside the dropdown', async () => {
+ mockUseSession.mockReturnValue({
+ session: mockSession,
+ isSyncing: false,
+ status: 'authenticated',
+ })
+
+ render()
+
+ const avatarButton = screen.getByRole('button')
+ fireEvent.click(avatarButton)
+ await waitFor(() => {
+ expect(avatarButton).toHaveAttribute('aria-expanded', 'true')
+ })
+
+ const dropdownId = avatarButton.getAttribute('aria-controls')
+ const dropdown = document.getElementById(dropdownId!)
+ expect(dropdown).toBeInTheDocument()
+
+ fireEvent.mouseDown(dropdown!)
+
+ await waitFor(() => {
+ expect(avatarButton).toHaveAttribute('aria-expanded', 'true')
+ })
+ })
+
+ it('handles mousedown events gracefully when safely syncing (ref is null)', () => {
+ mockUseSession.mockReturnValue({
+ session: null,
+ isSyncing: true,
+ status: 'loading',
+ })
+
+ render()
+
+ fireEvent.mouseDown(document.body)
+
+ expect(document.querySelector('.animate-pulse')).toBeInTheDocument()
+ })
})
describe('State changes / internal logic', () => {
@@ -809,5 +849,40 @@ describe('UserMenu Component', () => {
expect(avatarButton).toHaveAttribute('aria-expanded', 'false')
})
})
+
+ it('closes dropdown when Project Health Dashboard link is clicked', async () => {
+ const staffSession: ExtendedSession = {
+ user: {
+ name: 'Staff User',
+ email: 'staff@example.com',
+ image: 'https://example.com/avatar.jpg',
+ isLeader: false,
+ isOwaspStaff: true,
+ },
+ expires: '2024-12-31',
+ }
+
+ mockUseSession.mockReturnValue({
+ session: staffSession,
+ isSyncing: false,
+ status: 'authenticated',
+ })
+
+ render()
+
+ const avatarButton = screen.getByRole('button')
+ fireEvent.click(avatarButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Project Health Dashboard')).toBeInTheDocument()
+ })
+
+ const dashboardLink = screen.getByText('Project Health Dashboard')
+ fireEvent.click(dashboardLink)
+
+ await waitFor(() => {
+ expect(avatarButton).toHaveAttribute('aria-expanded', 'false')
+ })
+ })
})
})
diff --git a/frontend/__tests__/unit/components/forms/shared/FormTextarea.test.tsx b/frontend/__tests__/unit/components/forms/shared/FormTextarea.test.tsx
new file mode 100644
index 0000000000..e4257ad797
--- /dev/null
+++ b/frontend/__tests__/unit/components/forms/shared/FormTextarea.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { FormTextarea } from 'components/forms/shared/FormTextarea'
+
+describe('FormTextarea', () => {
+ const defaultProps = {
+ id: 'test-textarea',
+ label: 'Test Label',
+ placeholder: 'Enter text',
+ value: '',
+ onChange: jest.fn(),
+ }
+
+ it('renders with default props (required=false, rows=4)', () => {
+ render()
+ const textarea = screen.getByRole('textbox')
+ expect(textarea).toBeInTheDocument()
+ expect(textarea).toHaveAttribute('rows', '4')
+ expect(textarea).not.toBeRequired()
+ expect(screen.queryByText('*')).not.toBeInTheDocument()
+ })
+
+ it('renders with required=true', () => {
+ render()
+ const textarea = screen.getByRole('textbox')
+ expect(textarea).toBeRequired()
+ expect(screen.getByText('*')).toBeInTheDocument()
+ })
+
+ it('renders with custom rows', () => {
+ render()
+ const textarea = screen.getByRole('textbox')
+ expect(textarea).toHaveAttribute('rows', '10')
+ })
+
+ it('renders error message and styles when touched and error exists', () => {
+ const errorMsg = 'This field is required'
+ render()
+ expect(screen.getByText(errorMsg)).toBeInTheDocument()
+ const textarea = screen.getByRole('textbox')
+ expect(textarea).toHaveClass('border-red-500')
+ expect(textarea).toHaveClass('dark:border-red-500')
+ })
+
+ it('does not render error message when not touched', () => {
+ render()
+ expect(screen.queryByText('Error')).not.toBeInTheDocument()
+ const textarea = screen.getByRole('textbox')
+ expect(textarea).toHaveClass('border-gray-300')
+ })
+
+ it('calls onChange handler when typed into', () => {
+ const handleChange = jest.fn()
+ render()
+ const textarea = screen.getByRole('textbox')
+ fireEvent.change(textarea, { target: { value: 'New Value' } })
+ expect(handleChange).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/frontend/__tests__/unit/components/skeletons/Card.test.tsx b/frontend/__tests__/unit/components/skeletons/Card.test.tsx
new file mode 100644
index 0000000000..eb56f1c33e
--- /dev/null
+++ b/frontend/__tests__/unit/components/skeletons/Card.test.tsx
@@ -0,0 +1,46 @@
+import { render, screen } from '@testing-library/react'
+import CardSkeleton from 'components/skeletons/Card'
+
+jest.mock('@heroui/skeleton', () => ({
+ Skeleton: ({ className }: { className?: string }) => (
+
+ ),
+}))
+
+describe('CardSkeleton', () => {
+ it('renders with default props', () => {
+ render()
+ expect(screen.getAllByTestId('skeleton').length).toBeGreaterThan(0)
+ })
+
+ it('renders with all props explicitly set to false', () => {
+ render(
+
+ )
+ expect(screen.queryByTestId('skeleton')).not.toBeInTheDocument()
+ })
+
+ it('renders with showIcons=false specifically', () => {
+ render()
+ const skeletons = screen.getAllByTestId('skeleton')
+ const iconSkeletons = skeletons.filter((s) => s.className?.includes('h-8 w-16'))
+ expect(iconSkeletons.length).toBe(0)
+ })
+
+ it('renders with custom numIcons', () => {
+ const numIcons = 5
+ render()
+ const skeletons = screen.getAllByTestId('skeleton')
+ const iconSkeletons = skeletons.filter((s) => s.className?.includes('h-8 w-16'))
+ expect(iconSkeletons.length).toBe(numIcons)
+ })
+})
diff --git a/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx b/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
new file mode 100644
index 0000000000..9773f61293
--- /dev/null
+++ b/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
@@ -0,0 +1,49 @@
+import { registerBreadcrumb, getBreadcrumbItems } from 'contexts/BreadcrumbContext'
+
+describe('BreadcrumbContext', () => {
+ let cleanupFns: (() => void)[] = []
+
+ afterEach(() => {
+ cleanupFns.forEach((fn) => {
+ fn()
+ })
+ cleanupFns = []
+ })
+
+ it('sorts breadcrumbs correctly (home first, then by path length)', () => {
+ cleanupFns.push(
+ registerBreadcrumb({ title: 'Level 2', path: '/level-1/level-2' }),
+ registerBreadcrumb({ title: 'Level 1', path: '/level-1' }),
+ registerBreadcrumb({ title: 'Home', path: '/' })
+ )
+
+ const items = getBreadcrumbItems()
+
+ expect(items).toHaveLength(3)
+ expect(items[0]).toEqual({ title: 'Home', path: '/' })
+ expect(items[1]).toEqual({ title: 'Level 1', path: '/level-1' })
+ expect(items[2]).toEqual({ title: 'Level 2', path: '/level-1/level-2' })
+ })
+
+ it('sorts correctly when Home is not first inserted', () => {
+ cleanupFns.push(
+ registerBreadcrumb({ title: 'A', path: '/a' }),
+ registerBreadcrumb({ title: 'Home', path: '/' })
+ )
+
+ const items = getBreadcrumbItems()
+ expect(items[0].path).toBe('/')
+ expect(items[1].path).toBe('/a')
+ })
+
+ it('sorts by path length when home is not present', () => {
+ cleanupFns.push(
+ registerBreadcrumb({ title: 'Long', path: '/very/long/path' }),
+ registerBreadcrumb({ title: 'Short', path: '/short' })
+ )
+
+ const items = getBreadcrumbItems()
+ expect(items[0].path).toBe('/short')
+ expect(items[1].path).toBe('/very/long/path')
+ })
+})
diff --git a/frontend/__tests__/unit/pages/About.test.tsx b/frontend/__tests__/unit/pages/About.test.tsx
index b9af20b724..f7617fea3e 100644
--- a/frontend/__tests__/unit/pages/About.test.tsx
+++ b/frontend/__tests__/unit/pages/About.test.tsx
@@ -704,4 +704,106 @@ describe('About Component', () => {
expect(screen.getByText('Completed')).toBeInTheDocument()
})
})
+
+ test('handles leaders with missing login', async () => {
+ ;(useQuery as unknown as jest.Mock).mockImplementation((query) => {
+ if (query === GetAboutPageDataDocument) {
+ return {
+ data: {
+ project: mockAboutData.project,
+ topContributors: mockAboutData.topContributors,
+ leader1: {
+ ...mockAboutData.users['arkid15r'],
+ login: '', // Missing login
+ name: 'No Login Leader',
+ },
+ leader2: null,
+ leader3: null,
+ },
+ loading: false,
+ error: null,
+ }
+ }
+ return { loading: true }
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('No Login Leader')).toBeInTheDocument()
+ })
+ })
+
+ test('renders milestones with missing url and title', async () => {
+ ;(useQuery as unknown as jest.Mock).mockImplementation((query) => {
+ if (query === GetAboutPageDataDocument) {
+ return {
+ loading: false,
+ data: {
+ project: {
+ ...mockAboutData.project,
+ recentMilestones: [
+ {
+ ...mockAboutData.project.recentMilestones[0],
+ url: null,
+ title: '',
+ body: 'Body only',
+ },
+ ],
+ },
+ topContributors: mockAboutData.topContributors,
+ leader1: mockAboutData.users['arkid15r'],
+ leader2: null,
+ leader3: null,
+ },
+ error: null,
+ }
+ }
+ return { loading: true }
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('Body only')).toBeInTheDocument()
+ })
+ })
+
+ test('renders project stats with zero values', async () => {
+ ;(useQuery as unknown as jest.Mock).mockImplementation((query) => {
+ if (query === GetAboutPageDataDocument) {
+ return {
+ loading: false,
+ data: {
+ project: {
+ ...mockAboutData.project,
+ forksCount: 0,
+ starsCount: 0,
+ contributorsCount: 0,
+ issuesCount: 0,
+ },
+ topContributors: mockAboutData.topContributors,
+ leader1: mockAboutData.users['arkid15r'],
+ leader2: null,
+ leader3: null,
+ },
+ error: null,
+ }
+ }
+ return { loading: true }
+ })
+
+ await act(async () => {
+ render()
+ })
+
+ await waitFor(() => {
+ const zeroStats = screen.getAllByText('0+')
+ expect(zeroStats.length).toBeGreaterThan(0)
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/CreateModule.test.tsx b/frontend/__tests__/unit/pages/CreateModule.test.tsx
index 6c8b125736..405210b68e 100644
--- a/frontend/__tests__/unit/pages/CreateModule.test.tsx
+++ b/frontend/__tests__/unit/pages/CreateModule.test.tsx
@@ -1,4 +1,5 @@
import { useMutation, useQuery, useApolloClient } from '@apollo/client/react'
+import { addToast } from '@heroui/toast'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useRouter, useParams } from 'next/navigation'
@@ -254,4 +255,109 @@ describe('CreateModulePage', () => {
expect(moduleForm).toBeInTheDocument()
})
})
+ it('handles form submission error gracefully', async () => {
+ const user = userEvent.setup({ delay: null })
+
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin-user' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: {
+ getProgram: {
+ admins: [{ login: 'admin-user' }],
+ startedAt: '2025-01-15T00:00:00Z',
+ endedAt: '2025-12-31T00:00:00Z',
+ },
+ },
+ loading: false,
+ })
+
+ const mockCreateModuleError = jest.fn().mockRejectedValue(new Error('Network error'))
+ ;(useMutation as unknown as jest.Mock).mockReturnValue([
+ mockCreateModuleError,
+ { loading: false },
+ ])
+
+ render()
+
+ await user.type(screen.getByLabelText('Name'), 'Test Module')
+ await user.type(screen.getByLabelText(/Description/i), 'Desc')
+ await user.type(screen.getByLabelText(/Start Date/i), '2025-07-15')
+ await user.type(screen.getByLabelText(/End Date/i), '2025-08-15')
+
+ const projectInput = await waitFor(() =>
+ screen.getByPlaceholderText('Start typing project name...')
+ )
+ await user.type(projectInput, 'Aw')
+
+ const projectOption = await waitFor(() => screen.getByText('Awesome Project'), {
+ timeout: 2000,
+ })
+ await user.click(projectOption)
+
+ await user.click(screen.getByRole('button', { name: /Create Module/i }))
+
+ await waitFor(() => {
+ expect(mockCreateModuleError).toHaveBeenCalled()
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Creation Failed',
+ description: 'Network error',
+ color: 'danger',
+ })
+ )
+ })
+ })
+ it('handles non-Error submission failure', async () => {
+ const user = userEvent.setup({ delay: null })
+
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin-user' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: {
+ getProgram: {
+ admins: [{ login: 'admin-user' }],
+ startedAt: '2025-01-15T00:00:00Z',
+ endedAt: '2025-12-31T00:00:00Z',
+ },
+ },
+ loading: false,
+ })
+
+ const mockCreateModuleError = jest.fn().mockRejectedValue('String error')
+ ;(useMutation as unknown as jest.Mock).mockReturnValue([
+ mockCreateModuleError,
+ { loading: false },
+ ])
+
+ render()
+
+ await user.type(screen.getByLabelText('Name'), 'Test Module 2')
+ await user.type(screen.getByLabelText(/Description/i), 'Desc 2')
+ await user.type(screen.getByLabelText(/Start Date/i), '2025-07-15')
+ await user.type(screen.getByLabelText(/End Date/i), '2025-08-15')
+
+ const projectInput = await waitFor(() =>
+ screen.getByPlaceholderText('Start typing project name...')
+ )
+ await user.type(projectInput, 'Aw')
+ const projectOption = await waitFor(() => screen.getByText('Awesome Project'), {
+ timeout: 2000,
+ })
+ await user.click(projectOption)
+
+ await user.click(screen.getByRole('button', { name: /Create Module/i }))
+
+ await waitFor(() => {
+ expect(mockCreateModuleError).toHaveBeenCalled()
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Something went wrong while creating the module.',
+ })
+ )
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/CreateProgram.test.tsx b/frontend/__tests__/unit/pages/CreateProgram.test.tsx
index 1165d4a9a6..7480d2f1ea 100644
--- a/frontend/__tests__/unit/pages/CreateProgram.test.tsx
+++ b/frontend/__tests__/unit/pages/CreateProgram.test.tsx
@@ -222,4 +222,47 @@ describe('CreateProgramPage (comprehensive tests)', () => {
)
})
})
+ test('shows generic error toast if createProgram fails with non-Error object', async () => {
+ ;(mockUseSession as jest.Mock).mockReturnValue({
+ data: {
+ user: {
+ name: 'Test User',
+ email: 'test@example.com',
+ login: 'testuser',
+ isLeader: true,
+ },
+ expires: '2099-01-01T00:00:00.000Z',
+ },
+ status: 'authenticated',
+ loading: false,
+ })
+
+ mockCreateProgram.mockRejectedValue('String error')
+
+ render()
+
+ fireEvent.change(screen.getByLabelText('Name'), {
+ target: { value: 'Test Program' },
+ })
+ fireEvent.change(screen.getByLabelText(/^Description/), {
+ target: { value: 'A description' },
+ })
+ fireEvent.change(screen.getByLabelText('Start Date'), {
+ target: { value: '2025-01-01' },
+ })
+ fireEvent.change(screen.getByLabelText('End Date'), {
+ target: { value: '2025-12-31' },
+ })
+
+ fireEvent.submit(screen.getByText('Save').closest('form')!)
+
+ await waitFor(() => {
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'GraphQL Request Failed',
+ description: 'Unable to complete the requested operation.',
+ })
+ )
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/EditModule.test.tsx b/frontend/__tests__/unit/pages/EditModule.test.tsx
index 71e13cd14c..0af4eb86d8 100644
--- a/frontend/__tests__/unit/pages/EditModule.test.tsx
+++ b/frontend/__tests__/unit/pages/EditModule.test.tsx
@@ -28,6 +28,12 @@ jest.mock('@heroui/toast', () => ({
addToast: jest.fn(),
}))
+jest.mock('components/forms/shared/formValidationUtils', () => ({
+ ...jest.requireActual('components/forms/shared/formValidationUtils'),
+ validateStartDate: jest.fn(),
+ validateEndDate: jest.fn(),
+}))
+
describe('EditModulePage', () => {
const mockPush = jest.fn()
const mockReplace = jest.fn()
@@ -336,7 +342,6 @@ describe('EditModulePage', () => {
},
})
;(useMutation as unknown as jest.Mock).mockReturnValue([jest.fn(), { loading: false }])
-
render()
await act(async () => {
@@ -345,4 +350,71 @@ describe('EditModulePage', () => {
expect(await screen.findByDisplayValue('Test Module')).toBeInTheDocument()
})
+ it('submits form with null dates using mocked validation', async () => {
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin-user' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ loading: false,
+ data: {
+ getProgram: {
+ admins: [{ login: 'admin-user' }],
+ startedAt: null,
+ endedAt: null,
+ },
+ getModule: {
+ name: null,
+ description: 'Desc',
+ experienceLevel: ExperienceLevelEnum.Beginner,
+ startedAt: null,
+ endedAt: null,
+ domains: [],
+ tags: [],
+ projectName: 'Awesome Project',
+ projectId: '123',
+ mentors: [],
+ labels: [],
+ },
+ },
+ })
+ ;(useMutation as unknown as jest.Mock).mockReturnValue([
+ mockUpdateModule.mockResolvedValue({ data: { updateModule: { key: 'mod-key' } } }),
+ { loading: false },
+ ])
+
+ render()
+
+ // Wait for form to load with fallback empty name
+ expect(await screen.findByLabelText('Name')).toHaveValue('')
+
+ await act(async () => {
+ // Fill required fields that aren't dates
+ fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Valid Name' } })
+
+ // Project is already "valid" from mock data (projectId: '123')
+ // Dates are null/empty, but we mocked validators to return undefined (valid).
+
+ // Advance timers
+ jest.runAllTimers()
+
+ // Submit the form
+ fireEvent.click(screen.getByRole('button', { name: /Save/i }))
+ })
+
+ await waitFor(() => {
+ expect(mockUpdateModule).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: expect.objectContaining({
+ input: expect.objectContaining({
+ name: 'Valid Name',
+ projectId: '123',
+ startedAt: '',
+ endedAt: '',
+ }),
+ }),
+ })
+ )
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/EditProgram.test.tsx b/frontend/__tests__/unit/pages/EditProgram.test.tsx
index b989db22ab..44cb4fa670 100644
--- a/frontend/__tests__/unit/pages/EditProgram.test.tsx
+++ b/frontend/__tests__/unit/pages/EditProgram.test.tsx
@@ -355,4 +355,81 @@ describe('EditProgramPage', () => {
jest.useRealTimers()
})
+
+ test('handles program with null name field', async () => {
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin1' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ loading: false,
+ data: {
+ getProgram: {
+ name: null as unknown as string,
+ description: 'Test description',
+ menteesLimit: 10,
+ startedAt: '2025-01-01',
+ endedAt: '2025-12-31',
+ tags: ['react'],
+ domains: ['web'],
+ admins: [{ login: 'admin1' }],
+ status: ProgramStatusEnum.Draft,
+ },
+ },
+ })
+
+ render()
+
+ await waitFor(async () => {
+ const nameInput = await screen.findByLabelText('Name')
+ expect(nameInput).toHaveValue('')
+ })
+ })
+
+ test('submits form with null status using default', async () => {
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin1' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ loading: false,
+ data: {
+ getProgram: {
+ name: 'Test',
+ description: 'Test description',
+ menteesLimit: 10,
+ startedAt: '2025-01-01',
+ endedAt: '2025-12-31',
+ tags: ['react'],
+ domains: ['web'],
+ admins: [{ login: 'admin1' }],
+ status: null as unknown as ProgramStatusEnum,
+ },
+ },
+ })
+ mockUpdateProgram.mockResolvedValue({
+ data: { updateProgram: { key: 'program_1' } },
+ })
+
+ render()
+
+ await waitFor(async () => {
+ expect(await screen.findByLabelText('Name')).toBeInTheDocument()
+ })
+
+ const submitButton = screen.getByRole('button', { name: /save/i })
+ fireEvent.click(submitButton)
+
+ await waitFor(() => {
+ expect(mockUpdateProgram).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: expect.objectContaining({
+ input: expect.objectContaining({
+ status: ProgramStatusEnum.Draft,
+ }),
+ }),
+ })
+ )
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/Header.test.tsx b/frontend/__tests__/unit/pages/Header.test.tsx
index b4ad6a3810..ca6c203f60 100644
--- a/frontend/__tests__/unit/pages/Header.test.tsx
+++ b/frontend/__tests__/unit/pages/Header.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, fireEvent, act } from '@testing-library/react'
+import { render, screen, fireEvent, act, within } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import { SessionProvider } from 'next-auth/react'
import React from 'react'
@@ -109,9 +109,11 @@ jest.mock('utils/constants', () => {
submenu: [
{ text: 'Web Development', href: '/services/web' },
{ text: 'Mobile Development', href: '/services/mobile' },
+ { text: 'SubNoHref' },
],
},
{ text: 'Contact', href: '/contact' },
+ { text: 'NoHref' },
],
}
})
@@ -387,15 +389,42 @@ describe('Header Component', () => {
expect(isMobileMenuOpen()).toBe(true)
// Find and click the logo link in mobile menu
- const logoLinks = screen.getAllByRole('link')
- const mobileLogoLink = logoLinks.find(
- (link) => link.getAttribute('href') === '/' && link.querySelector('img[alt="OWASP Logo"]')
- )
+ const mobileMenu = findMobileMenu() as HTMLElement
+ expect(mobileMenu).not.toBeNull()
+
+ const mobileLogoLink = within(mobileMenu)
+ .getAllByRole('link')
+ .find((link) => link.querySelector('img[alt="OWASP Logo"]'))
// Assert that mobileLogoLink is not null before clicking
- expect(mobileLogoLink).not.toBeNull()
+ expect(mobileLogoLink).toBeDefined()
await act(async () => {
- fireEvent.click(mobileLogoLink)
+ fireEvent.click(mobileLogoLink!)
+ })
+ expect(isMobileMenuClosed()).toBe(true)
+ })
+
+ it('closes mobile menu when desktop logo is clicked', async () => {
+ renderWithSession()
+
+ const toggleButton = screen.getByRole('button', { name: /open main menu/i })
+
+ await act(async () => {
+ fireEvent.click(toggleButton)
+ })
+
+ expect(isMobileMenuOpen()).toBe(true)
+
+ const navbar = document.getElementById('navbar-sticky')
+ expect(navbar).toBeInTheDocument()
+
+ const desktopLogoLink = within(navbar!)
+ .getAllByRole('link')
+ .find((link) => link.querySelector('img[alt="OWASP Logo"]'))
+
+ expect(desktopLogoLink).toBeDefined()
+ await act(async () => {
+ fireEvent.click(desktopLogoLink!)
})
expect(isMobileMenuClosed()).toBe(true)
})
@@ -415,7 +444,7 @@ describe('Header Component', () => {
const allAboutLinks = screen.getAllByText('About')
const allContactLinks = screen.getAllByText('Contact')
- expect(allHomeLinks.length).toBeGreaterThan(1) // Desktop + Mobile
+ expect(allHomeLinks.length).toBeGreaterThan(1)
expect(allAboutLinks.length).toBeGreaterThan(1)
expect(allContactLinks.length).toBeGreaterThan(1)
})
@@ -424,7 +453,6 @@ describe('Header Component', () => {
const navButtons = screen.getAllByTestId('nav-button')
expect(navButtons.length).toBeGreaterThanOrEqual(2)
- // Check for the specific button texts from the actual component
const starButton = navButtons.find((btn) => btn.textContent?.includes('Star'))
const sponsorButton = navButtons.find((btn) => btn.textContent?.includes('Sponsor'))
@@ -480,40 +508,33 @@ describe('Header Component', () => {
expect(window.addEventListener).toHaveBeenCalledWith('click', expect.any(Function))
})
- // Simplified resize test - just check that the functionality works
it('handles window resize events', async () => {
renderWithSession()
- // Open mobile menu first
const toggleButton = screen.getByRole('button', { name: /open main menu/i })
await act(async () => {
fireEvent.click(toggleButton)
})
- // Simulate resize event
await act(async () => {
globalThis.dispatchEvent(new Event('resize'))
})
- // Test passes if no errors are thrown
expect(true).toBe(true)
})
it('handles outside click correctly', async () => {
renderWithSession()
- // Open mobile menu
const toggleButton = screen.getByRole('button', { name: /open main menu/i })
await act(async () => {
fireEvent.click(toggleButton)
})
- // Click outside
await act(async () => {
document.body.click()
})
- // Verify the event listener is set up
expect(window.addEventListener).toHaveBeenCalledWith('click', expect.any(Function))
})
})
@@ -543,7 +564,6 @@ describe('Header Component', () => {
mockUsePathname.mockReturnValue('/')
renderWithSession()
- // Find the Home links that should be active
const homeLinks = screen.getAllByRole('link', { name: 'Home' })
const activeHomeLinks = homeLinks.filter(
(link) => link.getAttribute('aria-current') === 'page'
diff --git a/frontend/__tests__/unit/pages/IssuesPage.test.tsx b/frontend/__tests__/unit/pages/IssuesPage.test.tsx
index c792536924..6f3ffa11ff 100644
--- a/frontend/__tests__/unit/pages/IssuesPage.test.tsx
+++ b/frontend/__tests__/unit/pages/IssuesPage.test.tsx
@@ -338,4 +338,35 @@ describe('IssuesPage', () => {
expect(screen.getByText('Test Module Issues')).toBeInTheDocument()
expect(screen.getAllByText('First Issue Title')[0]).toBeInTheDocument()
})
+ it('extracts labels from issues and handles null labels when availableLabels is empty', async () => {
+ mockUseQuery.mockReturnValue({
+ loading: false,
+ data: {
+ getModule: {
+ ...mockModuleData.getModule,
+ availableLabels: [], // Force extraction from issues
+ issues: [
+ {
+ ...mockModuleData.getModule.issues[0],
+ id: '1',
+ labels: ['extracted-label'],
+ },
+ {
+ ...mockModuleData.getModule.issues[0],
+ id: '2',
+ labels: null, // Test null labels handling
+ },
+ ],
+ },
+ },
+ })
+
+ render()
+
+ const selectTrigger = screen.getByRole('button', { name: /Label/i })
+ fireEvent.click(selectTrigger)
+
+ const listbox = await screen.findByRole('listbox')
+ expect(within(listbox).getByText('extracted-label')).toBeInTheDocument()
+ })
})
diff --git a/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx b/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
index 636c0b3f83..abbac2f87e 100644
--- a/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
+++ b/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
@@ -299,6 +299,34 @@ describe('ModuleIssueDetailsPage', () => {
expect(setTaskDeadlineMutation).toHaveBeenCalled()
})
})
+ it('populates input with existing deadline when clicked', async () => {
+ const setDeadlineInput = jest.fn()
+ const setIsEditingDeadline = jest.fn()
+ const baseMocks = (useIssueMutations as jest.Mock)()
+
+ mockUseIssueMutations.mockReturnValue({
+ ...baseMocks,
+ setDeadlineInput,
+ setIsEditingDeadline,
+ })
+
+ const pastDate = new Date('2020-01-01').toISOString()
+ const dataWithDeadline = {
+ ...mockIssueData,
+ getModule: { ...mockIssueData.getModule, taskDeadline: pastDate },
+ }
+
+ mockUseQuery.mockReturnValue({ data: dataWithDeadline, loading: false, error: undefined })
+ render()
+
+ const deadlineButton = screen.getByRole('button', { name: /\(overdue\)/i })
+ fireEvent.click(deadlineButton)
+
+ await waitFor(() => {
+ expect(setDeadlineInput).toHaveBeenCalledWith('2020-01-01')
+ expect(setIsEditingDeadline).toHaveBeenCalledWith(true)
+ })
+ })
})
describe('issue states', () => {
@@ -320,7 +348,6 @@ describe('ModuleIssueDetailsPage', () => {
}
mockUseQuery.mockReturnValue({ data: issueWithState, loading: false, error: undefined })
render()
- // The issue status is the first badge of its kind.
expect(screen.getAllByText(expectedText)[0]).toBeInTheDocument()
})
})
@@ -568,4 +595,60 @@ describe('ModuleIssueDetailsPage', () => {
)
expect(placeholderDivs.length).toBeGreaterThan(0)
})
+
+ it('handles null assignees, labels, and interestedUsers', () => {
+ const dataWithNulls = {
+ getModule: {
+ ...mockIssueData.getModule,
+ interestedUsers: null,
+ issueByNumber: {
+ ...mockIssueData.getModule.issueByNumber,
+ assignees: null,
+ labels: null,
+ },
+ },
+ }
+ mockUseQuery.mockReturnValue({ data: dataWithNulls, loading: false, error: undefined })
+ render()
+
+ expect(screen.getByText('Test Issue Title')).toBeInTheDocument()
+ })
+
+ it('does not trigger mutations when they are already in progress', () => {
+ const assignIssue = jest.fn()
+ const unassignIssue = jest.fn()
+ const setTaskDeadlineMutation = jest.fn()
+ const baseMocks = (useIssueMutations as jest.Mock)()
+
+ mockUseIssueMutations.mockReturnValue({
+ ...baseMocks,
+ assignIssue,
+ unassignIssue,
+ setTaskDeadlineMutation,
+ assigning: true,
+ unassigning: true,
+ settingDeadline: true,
+ isEditingDeadline: true,
+ deadlineInput: '2025-01-01',
+ setDeadlineInput: jest.fn(),
+ })
+
+ mockUseQuery.mockReturnValue({ data: mockIssueData, loading: false, error: undefined })
+ render()
+
+ const interestedUsersHeading = screen.getByRole('heading', { name: /Interested Users/i })
+ const userGrid = interestedUsersHeading.nextElementSibling
+ const assignButton = within(userGrid as HTMLElement).getByRole('button', { name: /Assign/i })
+
+ fireEvent.click(assignButton)
+ expect(assignIssue).not.toHaveBeenCalled()
+
+ const unassignButton = screen.getByRole('button', { name: /Unassign/i })
+ fireEvent.click(unassignButton)
+ expect(unassignIssue).not.toHaveBeenCalled()
+
+ const dateInputEl = screen.getByDisplayValue('2025-01-01')
+ fireEvent.change(dateInputEl, { target: { value: '2025-02-02' } })
+ expect(setTaskDeadlineMutation).not.toHaveBeenCalled()
+ })
})
diff --git a/frontend/__tests__/unit/pages/MyMentorship.test.tsx b/frontend/__tests__/unit/pages/MyMentorship.test.tsx
index baad3f5119..d759fc4af6 100644
--- a/frontend/__tests__/unit/pages/MyMentorship.test.tsx
+++ b/frontend/__tests__/unit/pages/MyMentorship.test.tsx
@@ -283,7 +283,7 @@ describe('MyMentorshipPage', () => {
}
})
- it('handles search callback', async () => {
+ it('updates URL when search or page changes', async () => {
;(mockUseSession as jest.Mock).mockReturnValue({
data: {
user: {
@@ -310,8 +310,112 @@ describe('MyMentorshipPage', () => {
})
const searchInput = screen.getByTestId('search-input')
- fireEvent.change(searchInput, { target: { value: 'test search' } })
+ fireEvent.change(searchInput, { target: { value: 'query' } })
- expect(searchInput).toBeInTheDocument()
+ await waitFor(
+ () => {
+ expect(mockPush).toHaveBeenCalledWith('?q=query', { scroll: false })
+ },
+ { timeout: 1000 }
+ )
+ })
+
+ it('handles missing totalPages in program data', async () => {
+ ;(mockUseSession as jest.Mock).mockReturnValue({
+ data: {
+ user: {
+ name: 'User',
+ email: 'user@example.com',
+ login: 'user',
+ isLeader: true,
+ },
+ expires: '2099-01-01T00:00:00.000Z',
+ },
+ status: 'authenticated',
+ })
+
+ mockUseQuery.mockReturnValue({
+ data: {
+ myPrograms: {
+ programs: mockProgramData.myPrograms.programs,
+ totalPages: null, // Test fallback
+ },
+ },
+ loading: false,
+ error: undefined,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Program')).toBeInTheDocument()
+ })
+ })
+
+ it('cleans up debounce on unmount', () => {
+ ;(mockUseSession as jest.Mock).mockReturnValue({
+ data: {
+ user: {
+ name: 'User',
+ email: 'user@example.com',
+ login: 'user',
+ isLeader: true,
+ },
+ expires: '2099-01-01T00:00:00.000Z',
+ },
+ status: 'authenticated',
+ })
+ mockUseQuery.mockReturnValue({
+ data: mockProgramData,
+ loading: false,
+ error: undefined,
+ })
+
+ const { unmount } = render()
+ expect(screen.getByText('My Mentorship')).toBeInTheDocument()
+ unmount()
+ })
+
+ it('updates debounced search query', async () => {
+ jest.useFakeTimers()
+ try {
+ ;(mockUseSession as jest.Mock).mockReturnValue({
+ data: {
+ user: {
+ name: 'User',
+ email: 'user@example.com',
+ login: 'user',
+ isLeader: true,
+ },
+ expires: '2099-01-01T00:00:00.000Z',
+ },
+ status: 'authenticated',
+ })
+ mockUseQuery.mockReturnValue({
+ data: mockProgramData,
+ loading: false,
+ error: undefined,
+ })
+
+ render()
+
+ const searchInput = screen.getByTestId('search-input')
+ fireEvent.change(searchInput, { target: { value: 'debounced' } })
+
+ React.act(() => {
+ jest.advanceTimersByTime(500)
+ })
+
+ await waitFor(() => {
+ expect(mockUseQuery).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ variables: expect.objectContaining({ search: 'debounced' }),
+ })
+ )
+ })
+ } finally {
+ jest.useRealTimers()
+ }
})
})
diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
index 45d4f3b5e3..70f832992a 100644
--- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
@@ -239,4 +239,33 @@ describe('OrganizationDetailsPage', () => {
expect(screen.getByRole('heading', { name: 'Test Organization' })).toBeInTheDocument()
})
})
+ test('renders repositories with organization details', async () => {
+ const dataWithRepos = {
+ ...mockOrganizationDetailsData,
+ repositories: [
+ {
+ name: 'Test Repo With Org',
+ url: 'https://github.com/test-org/test-repo-org',
+ contributorsCount: 10,
+ forksCount: 5,
+ openIssuesCount: 2,
+ starsCount: 20,
+ key: 'test-org/test-repo-org',
+ organization: { login: 'test-org' },
+ },
+ ],
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithRepos,
+ error: null,
+ loading: false,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Repo With Org')).toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/ProjectHealthDashboardMetricsDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectHealthDashboardMetricsDetails.test.tsx
index 05bb923ef4..55a99f19f0 100644
--- a/frontend/__tests__/unit/pages/ProjectHealthDashboardMetricsDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/ProjectHealthDashboardMetricsDetails.test.tsx
@@ -260,4 +260,29 @@ describe('ProjectHealthMetricsDetails', () => {
expect(screen.getByText('Stars')).toBeInTheDocument()
})
})
+ test('renders metrics with valid createdAt', async () => {
+ const dataWithCreatedAt = {
+ ...mockProjectsDashboardMetricsDetailsData,
+ project: {
+ ...mockProjectsDashboardMetricsDetailsData.project,
+ healthMetricsList: [
+ {
+ ...mockProjectsDashboardMetricsDetailsData.project.healthMetricsList[0],
+ createdAt: '2023-01-01T00:00:00Z',
+ },
+ ],
+ },
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: dataWithCreatedAt,
+ loading: false,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getAllByTestId('mock-apexcharts').length).toBeGreaterThan(0)
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/Users.test.tsx b/frontend/__tests__/unit/pages/Users.test.tsx
index e8a6d49bac..78b1543046 100644
--- a/frontend/__tests__/unit/pages/Users.test.tsx
+++ b/frontend/__tests__/unit/pages/Users.test.tsx
@@ -147,4 +147,25 @@ describe('UsersPage Component', () => {
expect(screen.getByText('@fallback_login')).toBeInTheDocument()
})
})
+
+ test('handles missing company field', async () => {
+ ;(fetchAlgoliaData as jest.Mock).mockResolvedValue({
+ hits: [
+ {
+ key: 'user_4',
+ login: 'user_no_company',
+ name: 'User Without Company',
+ avatarUrl: 'https://example.com/avatar.jpg',
+ company: null,
+ },
+ ],
+ totalPages: 1,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('User Without Company')).toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx
index d037752fa8..663ea1311a 100644
--- a/frontend/src/app/chapters/[chapterKey]/page.tsx
+++ b/frontend/src/app/chapters/[chapterKey]/page.tsx
@@ -56,7 +56,7 @@ export default function ChapterDetailsPage() {
}
const details = [
- { label: 'Last Updated', value: formatDate(chapter.updatedAt) ?? '' },
+ { label: 'Last Updated', value: formatDate(chapter.updatedAt) },
{ label: 'Location', value: chapter.suggestedLocation ?? '' },
{ label: 'Region', value: chapter.region ?? '' },
{
diff --git a/frontend/src/app/projects/dashboard/metrics/page.tsx b/frontend/src/app/projects/dashboard/metrics/page.tsx
index 0c58068f8e..7ef73484fe 100644
--- a/frontend/src/app/projects/dashboard/metrics/page.tsx
+++ b/frontend/src/app/projects/dashboard/metrics/page.tsx
@@ -234,10 +234,8 @@ const MetricsPage: FC = () => {
},
]}
selectionMode="single"
- selectedKeys={urlKey ? [urlKey] : []}
- selectedLabels={
- urlKey ? [SORT_FIELDS.find((f) => f.key === urlKey)?.label || ''] : []
- }
+ selectedKeys={[urlKey]}
+ selectedLabels={[SORT_FIELDS.find((f) => f.key === urlKey)!.label]}
onAction={(key: Key) => {
if (key === 'reset-sort') {
handleSort(null)
diff --git a/frontend/src/components/EntityActions.tsx b/frontend/src/components/EntityActions.tsx
index e56015c606..329b15a378 100644
--- a/frontend/src/components/EntityActions.tsx
+++ b/frontend/src/components/EntityActions.tsx
@@ -98,8 +98,6 @@ const EntityActions: React.FC = ({
}, [focusIndex])
const handleKeyDown = (e: React.KeyboardEvent) => {
- if (!dropdownOpen) return
-
const optionsCount = options.length
switch (e.key) {
@@ -120,9 +118,7 @@ const EntityActions: React.FC = ({
case 'Enter':
case ' ':
e.preventDefault()
- if (focusIndex >= 0) {
- menuItemsRef.current[focusIndex]?.click()
- }
+ menuItemsRef.current[focusIndex]?.click()
break
default:
break
@@ -159,7 +155,7 @@ const EntityActions: React.FC = ({
className="absolute right-0 z-20 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
onKeyDown={handleKeyDown}
role="menu"
- tabIndex={dropdownOpen ? 0 : -1}
+ tabIndex={0}
>
{options.map((option, index) => {
const handleMenuItemClick = (e: React.MouseEvent) => {
diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx
index bc852da346..ae3a999c21 100644
--- a/frontend/src/components/ItemCardList.tsx
+++ b/frontend/src/components/ItemCardList.tsx
@@ -100,7 +100,8 @@ const ItemCardList = ({
const title = 'title' in i ? i.title : ''
const name = 'name' in i ? i.name : ''
const url = 'url' in i ? i.url : ''
- const key = `${repoName || ''}-${title || name || ''}-${url || ''}`
+ const keyParts = [repoName, title || name, url].filter(Boolean)
+ const key = keyParts.join('-')
return key || `item-${idx}`
}
return (
diff --git a/frontend/src/components/MetricsCard.tsx b/frontend/src/components/MetricsCard.tsx
index d900909b96..38661682fb 100644
--- a/frontend/src/components/MetricsCard.tsx
+++ b/frontend/src/components/MetricsCard.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { FC } from 'react'
import { HealthMetricsProps } from 'types/healthMetrics'
const MetricsCard: FC<{ metric: HealthMetricsProps }> = ({ metric }) => {
+ const score = metric.score ?? 0
return (
= ({ metric }) => {
className={clsx(
'flex-shrink-0 rounded px-3 py-1.5 text-center text-white lg:px-4 lg:py-2 dark:text-gray-900',
{
- 'bg-green-500': (metric.score ?? 0) >= 75,
- 'bg-orange-500': (metric.score ?? 0) >= 50 && (metric.score ?? 0) < 75,
- 'bg-red-500': (metric.score ?? 0) < 50,
+ 'bg-green-500': score >= 75,
+ 'bg-orange-500': score >= 50 && score < 75,
+ 'bg-red-500': score < 50,
}
)}
>
- Score: {metric.score ?? 0}
+ Score: {score}
diff --git a/frontend/src/components/MultiSearch.tsx b/frontend/src/components/MultiSearch.tsx
index 51d263f433..efa3c69fc2 100644
--- a/frontend/src/components/MultiSearch.tsx
+++ b/frontend/src/components/MultiSearch.tsx
@@ -86,7 +86,7 @@ const MultiSearchBar: React.FC = ({
const handleSuggestionClick = useCallback(
(suggestion: SearchHit, indexName: string) => {
- setSearchQuery(suggestion.name ?? '')
+ setSearchQuery(suggestion.name || '')
setShowSuggestions(false)
switch (indexName) {
diff --git a/frontend/src/components/ProgramForm.tsx b/frontend/src/components/ProgramForm.tsx
index 80695f8a25..57429c6155 100644
--- a/frontend/src/components/ProgramForm.tsx
+++ b/frontend/src/components/ProgramForm.tsx
@@ -251,7 +251,7 @@ const ProgramForm = ({
type="number"
label="Mentees Limit"
placeholder="Enter mentees limit (0 for unlimited)"
- value={formData.menteesLimit.toString()}
+ value={formData.menteesLimit?.toString() ?? ''}
onValueChange={(value) => handleInputChange('menteesLimit', Number(value) || 0)}
error={errors.menteesLimit}
touched={touched.menteesLimit}
diff --git a/frontend/src/components/TruncatedText.tsx b/frontend/src/components/TruncatedText.tsx
index c885088fd0..25a2bfcbf2 100644
--- a/frontend/src/components/TruncatedText.tsx
+++ b/frontend/src/components/TruncatedText.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useEffect, useCallback } from 'react'
+import React from 'react'
export const TruncatedText = ({
text,
@@ -9,16 +9,16 @@ export const TruncatedText = ({
children?: React.ReactNode
className?: string
}) => {
- const textRef = useRef(null)
+ const textRef = React.useRef(null)
- const checkTruncation = useCallback(() => {
+ const checkTruncation = React.useCallback(() => {
const element = textRef.current
if (element) {
element.title = text || element.textContent || ''
}
}, [text])
- useEffect(() => {
+ React.useEffect(() => {
checkTruncation()
const observer = new ResizeObserver(() => checkTruncation())