Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/__tests__/unit/components/CalendarButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CalendarButton event={{ ...mockEvent, title: '' }} />)
const button = screen.getByRole('button')
expect(button).toHaveAttribute('aria-label', 'Add event to Calendar')
})
})

describe('className prop', () => {
Expand Down
48 changes: 43 additions & 5 deletions frontend/__tests__/unit/components/CardDetailsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<div data-testid="mock-tooltip" title={content}>
{children}
</div>
),
}))

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
Expand Down Expand Up @@ -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<MenteeNode> & { tag?: string; login?: string; name?: string })[]
icon?: unknown
title?: string
maxInitialDisplay: number
Expand All @@ -419,6 +428,11 @@ jest.mock('components/ContributorsList', () => ({
}) => (
<div data-testid="contributors-list" {...props}>
{title} ({contributors.length} items, max display: {maxInitialDisplay})
{contributors.map((c) => (
<a key={c.tag || c.login || 'unknown'} href={getUrl && getUrl(c.login || 'unknown')}>
{c.name || c.login || 'Unknown'}
</a>
))}
</div>
),
}))
Expand Down Expand Up @@ -2351,9 +2365,33 @@ describe('CardDetailsPage', () => {

render(<CardDetailsPage {...propsWithMentees} />)

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(<CardDetailsPage {...propsWithMentees} />)

const menteeLink = screen.getByText('Test Mentee')
expect(menteeLink).toBeInTheDocument()
expect(menteeLink).toHaveAttribute('href', '/programs//mentees/test_mentee')
})

it('handles null/undefined mentees array gracefully', () => {
Expand Down
22 changes: 22 additions & 0 deletions frontend/__tests__/unit/components/EntityActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<EntityActions type="program" programKey="test-program" />)
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(<EntityActions type="program" programKey="test-program" />)
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')
})
})
})
9 changes: 9 additions & 0 deletions frontend/__tests__/unit/components/InfoBlock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<InfoBlock icon={FaUser} />)

expect(screen.getByText('No items')).toBeInTheDocument()
})
})

describe('Text and content rendering', () => {
Expand Down
160 changes: 160 additions & 0 deletions frontend/__tests__/unit/components/ItemCardList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ItemCardList
title="No Avatar URL"
data={[issueNoAvatarUrl]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

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(
<ItemCardList
title="No Login"
data={[issueNoLogin]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

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(
<ItemCardList
title="Bare Item"
data={[bareItem]}
renderDetails={defaultProps.renderDetails}
/>
)

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(
<ItemCardList
title="No ID Item"
data={[noIdItem]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

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(
<ItemCardList
title="No URL Item"
data={[noUrlNoInfoItem]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

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(
<ItemCardList
title="Name Link Item"
data={[itemWithNameAndUrl]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

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(
<ItemCardList
title="Empty Link Item"
data={[itemWithUrlOnly]}
renderDetails={defaultProps.renderDetails}
showAvatar={true}
/>
)

const links = screen.getAllByTestId('link')
const itemLink = links.find((l) => l.getAttribute('href') === 'https://example.com/empty')
expect(itemLink).toBeInTheDocument()
expect(itemLink).toHaveTextContent('')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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(<MentorshipPullRequest pr={mockPrWithAvatarButNoLogin} />)
const avatar = screen.getByAltText('Unknown')
expect(avatar).toBeInTheDocument()
})
})
})
1 change: 1 addition & 0 deletions frontend/__tests__/unit/components/MetricsCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
]

Expand Down
54 changes: 53 additions & 1 deletion frontend/__tests__/unit/components/ModuleCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Module>)

const modules = [moduleWithUndefined, createMockModule({ key: 'mod2' })]

expect(() => render(<ModuleCard modules={modules} />)).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(<ModuleCard modules={modules} />)

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(<ModuleCard modules={modules} />)

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(<ModuleCard modules={modules} />)

const image = screen.getAllByTestId('next-image')[0]
expect(image.getAttribute('alt')).toBe('mentee1')
expect(image.getAttribute('title')).toBe('mentee1')
})
})

describe('Path Handling', () => {
Expand All @@ -636,7 +689,6 @@ describe('ModuleCard', () => {
mockPathname.mockReturnValue(undefined)
const modules = [createMockModule(), createMockModule({ key: 'mod2' })]

// Should not throw
expect(() => render(<ModuleCard modules={modules} />)).not.toThrow()
})

Expand Down
Loading