Skip to content

Commit 383e9f3

Browse files
anurag2787arkid15r
andauthored
Improve frontend test coverage above 95% (#3952)
* Imrpove frontend test coverage to above 95% * Fixed coderabbit review * fixed codedevai review * update code * update command * removed comment' * Delete .markdownlint.yaml --------- Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
1 parent 89a59fd commit 383e9f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1623
-62
lines changed

frontend/__tests__/unit/components/CalendarButton.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ describe('CalendarButton', () => {
172172
const button = screen.getByRole('button')
173173
expect(button).toHaveAttribute('aria-label', 'Add Untitled to Calendar')
174174
})
175+
176+
it('uses "event" as fallback when title is missing', () => {
177+
render(<CalendarButton event={{ ...mockEvent, title: '' }} />)
178+
const button = screen.getByRole('button')
179+
expect(button).toHaveAttribute('aria-label', 'Add event to Calendar')
180+
})
175181
})
176182

177183
describe('className prop', () => {

frontend/__tests__/unit/components/CardDetailsPage.test.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@ import { render, screen, cleanup, fireEvent } from '@testing-library/react'
22
import React from 'react'
33
import '@testing-library/jest-dom'
44
import { FaCode, FaTags } from 'react-icons/fa6'
5+
import type { MenteeNode } from 'types/__generated__/graphql'
56
import type { DetailsCardProps } from 'types/card'
67
import type { PullRequest } from 'types/pullRequest'
78
import CardDetailsPage, { type CardType } from 'components/CardDetailsPage'
89

10+
jest.mock('@heroui/tooltip', () => ({
11+
Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) => (
12+
<div data-testid="mock-tooltip" title={content}>
13+
{children}
14+
</div>
15+
),
16+
}))
17+
918
jest.mock('next/navigation', () => ({
1019
useRouter: () => ({
1120
push: jest.fn(),
@@ -406,11 +415,11 @@ jest.mock('components/ContributorsList', () => ({
406415
// eslint-disable-next-line @typescript-eslint/no-unused-vars
407416
icon,
408417
title = 'Contributors',
409-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
418+
410419
getUrl,
411420
...props
412421
}: {
413-
contributors: unknown[]
422+
contributors: (Partial<MenteeNode> & { tag?: string; login?: string; name?: string })[]
414423
icon?: unknown
415424
title?: string
416425
maxInitialDisplay: number
@@ -419,6 +428,11 @@ jest.mock('components/ContributorsList', () => ({
419428
}) => (
420429
<div data-testid="contributors-list" {...props}>
421430
{title} ({contributors.length} items, max display: {maxInitialDisplay})
431+
{contributors.map((c) => (
432+
<a key={c.tag || c.login || 'unknown'} href={getUrl && getUrl(c.login || 'unknown')}>
433+
{c.name || c.login || 'Unknown'}
434+
</a>
435+
))}
422436
</div>
423437
),
424438
}))
@@ -2351,9 +2365,33 @@ describe('CardDetailsPage', () => {
23512365

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

2354-
const allContributorsLists = screen.getAllByTestId('contributors-list')
2355-
const menteesSection = allContributorsLists.find((el) => el.textContent?.includes('Mentees'))
2356-
expect(menteesSection).toHaveTextContent('Mentees (1 items, max display: 6)')
2368+
const menteeLink = screen.getByText('Test Mentee')
2369+
expect(menteeLink).toBeInTheDocument()
2370+
expect(menteeLink).toHaveAttribute('href', '/programs/program-key-123/mentees/test_mentee')
2371+
})
2372+
2373+
it('renders mentee links with empty program key segment when programKey is undefined', () => {
2374+
const mentees = [
2375+
{
2376+
id: 'mentee-1',
2377+
login: 'test_mentee',
2378+
name: 'Test Mentee',
2379+
avatarUrl: 'https://example.com/mentee.jpg',
2380+
},
2381+
]
2382+
2383+
const propsWithMentees: DetailsCardProps = {
2384+
...defaultProps,
2385+
mentees,
2386+
programKey: undefined,
2387+
entityKey: undefined,
2388+
}
2389+
2390+
render(<CardDetailsPage {...propsWithMentees} />)
2391+
2392+
const menteeLink = screen.getByText('Test Mentee')
2393+
expect(menteeLink).toBeInTheDocument()
2394+
expect(menteeLink).toHaveAttribute('href', '/programs//mentees/test_mentee')
23572395
})
23582396

23592397
it('handles null/undefined mentees array gracefully', () => {

frontend/__tests__/unit/components/EntityActions.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,4 +716,26 @@ describe('EntityActions', () => {
716716

717717
expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program/edit')
718718
})
719+
it('does nothing when an unhandled key is pressed', () => {
720+
render(<EntityActions type="program" programKey="test-program" />)
721+
const button = screen.getByRole('button', { name: /Program actions menu/ })
722+
fireEvent.click(button)
723+
724+
const menu = screen.getByRole('menu')
725+
fireEvent.keyDown(menu, { key: 'a' })
726+
expect(button).toHaveAttribute('aria-expanded', 'true')
727+
})
728+
729+
describe('Toggle Behavior', () => {
730+
it('closes the dropdown and resets focus when toggled off via click', () => {
731+
render(<EntityActions type="program" programKey="test-program" />)
732+
const button = screen.getByRole('button', { name: /Program actions menu/ })
733+
734+
fireEvent.click(button)
735+
expect(button).toHaveAttribute('aria-expanded', 'true')
736+
737+
fireEvent.click(button)
738+
expect(button).toHaveAttribute('aria-expanded', 'false')
739+
})
740+
})
719741
})

frontend/__tests__/unit/components/InfoBlock.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ describe('InfoBlock Component', () => {
265265

266266
expect(mockMillify).toHaveBeenCalledWith(1234, { precision: 1 })
267267
})
268+
269+
it('should use default value of 0 when value is not provided', () => {
270+
mockMillify.mockReturnValue('0')
271+
mockPluralize.mockReturnValue('items')
272+
273+
render(<InfoBlock icon={FaUser} />)
274+
275+
expect(screen.getByText('No items')).toBeInTheDocument()
276+
})
268277
})
269278

270279
describe('Text and content rendering', () => {

frontend/__tests__/unit/components/ItemCardList.test.tsx

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,4 +858,164 @@ describe('ItemCardList Component', () => {
858858
expect(tooltip).toHaveAttribute('data-id', 'avatar-tooltip-0')
859859
})
860860
})
861+
describe('Additional Code Coverage', () => {
862+
it('shows fallback avatar when author exists but avatarUrl is missing', () => {
863+
const issueNoAvatarUrl = {
864+
...mockIssue,
865+
author: {
866+
...mockIssue.author,
867+
avatarUrl: '',
868+
},
869+
}
870+
871+
render(
872+
<ItemCardList
873+
title="No Avatar URL"
874+
data={[issueNoAvatarUrl]}
875+
renderDetails={defaultProps.renderDetails}
876+
showAvatar={true}
877+
/>
878+
)
879+
880+
expect(screen.queryByTestId('avatar-image')).not.toBeInTheDocument()
881+
882+
const links = screen.getAllByTestId('link')
883+
const profileLink = links.find(
884+
(link) => link.getAttribute('href') === `/members/${mockIssue.author.login}`
885+
)
886+
expect(profileLink).toBeInTheDocument()
887+
})
888+
889+
it('renders avatar without link when login is missing but name exists', () => {
890+
const authorNoLogin = {
891+
avatarUrl: 'https://example.com/avatar.png',
892+
name: 'Just Name',
893+
login: '',
894+
}
895+
896+
const issueNoLogin = {
897+
...mockIssue,
898+
author: authorNoLogin,
899+
} as unknown as Issue
900+
901+
render(
902+
<ItemCardList
903+
title="No Login"
904+
data={[issueNoLogin]}
905+
renderDetails={defaultProps.renderDetails}
906+
showAvatar={true}
907+
/>
908+
)
909+
910+
expect(screen.getByTestId('avatar-image')).toBeInTheDocument()
911+
const links = screen.queryAllByTestId('link')
912+
const profileLink = links.find((link) => link.getAttribute('href')?.startsWith('/members/'))
913+
expect(profileLink).toBeUndefined()
914+
})
915+
916+
it('handles item with no title and no name gracefully', () => {
917+
const bareItem = {
918+
id: 'bare-item',
919+
author: mockUser,
920+
url: 'https://example.com',
921+
} as unknown as Issue
922+
923+
render(
924+
<ItemCardList
925+
title="Bare Item"
926+
data={[bareItem]}
927+
renderDetails={defaultProps.renderDetails}
928+
/>
929+
)
930+
931+
const truncatedText = screen.getByTestId('truncated-text')
932+
expect(truncatedText).toHaveTextContent('')
933+
})
934+
935+
it('handles item with no identifiers for key generation coverage', () => {
936+
const noIdItem = {
937+
author: mockUser,
938+
} as unknown as Issue
939+
940+
render(
941+
<ItemCardList
942+
title="No ID Item"
943+
data={[noIdItem]}
944+
renderDetails={defaultProps.renderDetails}
945+
showAvatar={true}
946+
/>
947+
)
948+
949+
const truncatedText = screen.getByTestId('truncated-text')
950+
expect(truncatedText).toHaveTextContent('')
951+
})
952+
953+
it('handles item with no URL, no title, no name for TruncatedText coverage', () => {
954+
const noUrlNoInfoItem = {
955+
id: 'no-url-item',
956+
author: {
957+
...mockUser,
958+
login: '',
959+
},
960+
} as unknown as Issue
961+
962+
render(
963+
<ItemCardList
964+
title="No URL Item"
965+
data={[noUrlNoInfoItem]}
966+
renderDetails={defaultProps.renderDetails}
967+
showAvatar={true}
968+
/>
969+
)
970+
971+
const truncatedText = screen.getByTestId('truncated-text')
972+
expect(truncatedText).toHaveTextContent('')
973+
expect(screen.queryByTestId('link')).not.toBeInTheDocument()
974+
})
975+
976+
it('handles item with URL and name (but no title) correctly', () => {
977+
const itemWithNameAndUrl = {
978+
id: 'name-only-link-item',
979+
author: mockUser,
980+
url: 'https://example.com/name',
981+
name: 'Item Name',
982+
} as unknown as Issue
983+
984+
render(
985+
<ItemCardList
986+
title="Name Link Item"
987+
data={[itemWithNameAndUrl]}
988+
renderDetails={defaultProps.renderDetails}
989+
showAvatar={true}
990+
/>
991+
)
992+
993+
const links = screen.getAllByTestId('link')
994+
const itemLink = links.find((l) => l.getAttribute('href') === 'https://example.com/name')
995+
expect(itemLink).toBeInTheDocument()
996+
expect(itemLink).toHaveTextContent('Item Name')
997+
})
998+
999+
it('handles item with URL but no title and no name', () => {
1000+
const itemWithUrlOnly = {
1001+
id: 'url-only-item',
1002+
author: mockUser,
1003+
url: 'https://example.com/empty',
1004+
} as unknown as Issue
1005+
1006+
render(
1007+
<ItemCardList
1008+
title="Empty Link Item"
1009+
data={[itemWithUrlOnly]}
1010+
renderDetails={defaultProps.renderDetails}
1011+
showAvatar={true}
1012+
/>
1013+
)
1014+
1015+
const links = screen.getAllByTestId('link')
1016+
const itemLink = links.find((l) => l.getAttribute('href') === 'https://example.com/empty')
1017+
expect(itemLink).toBeInTheDocument()
1018+
expect(itemLink).toHaveTextContent('')
1019+
})
1020+
})
8611021
})

frontend/__tests__/unit/components/MentorshipPullRequest.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,18 @@ describe('MentorshipPullRequest Component', () => {
158158
expect(links[0]).toHaveAttribute('target', '_blank')
159159
expect(links[0]).toHaveAttribute('rel', 'noopener noreferrer')
160160
})
161+
test('renders Unknown alt text when author login is empty but avatar exists', () => {
162+
const mockPrWithAvatarButNoLogin = {
163+
...mockPullRequestOpen,
164+
author: {
165+
...mockPullRequestOpen.author,
166+
login: '',
167+
},
168+
} as unknown as PullRequest
169+
170+
render(<MentorshipPullRequest pr={mockPrWithAvatarButNoLogin} />)
171+
const avatar = screen.getByAltText('Unknown')
172+
expect(avatar).toBeInTheDocument()
173+
})
161174
})
162175
})

frontend/__tests__/unit/components/MetricsCard.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('MetricsCard component', () => {
6262
[75, 'bg-green-500'],
6363
[60, 'bg-orange-500'],
6464
[50, 'bg-orange-500'],
65+
[74, 'bg-orange-500'],
6566
[30, 'bg-red-500'],
6667
]
6768

frontend/__tests__/unit/components/ModuleCard.test.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,59 @@ describe('ModuleCard', () => {
610610
expect(image.getAttribute('alt')).toBe('mentor1')
611611
expect(image.getAttribute('title')).toBe('mentor1')
612612
})
613+
it('handles module with undefined mentors and mentees gracefully', () => {
614+
const moduleWithUndefined = createMockModule({
615+
mentors: undefined,
616+
mentees: undefined,
617+
} as unknown as Partial<Module>)
618+
619+
const modules = [moduleWithUndefined, createMockModule({ key: 'mod2' })]
620+
621+
expect(() => render(<ModuleCard modules={modules} />)).not.toThrow()
622+
expect(screen.queryByText('Mentors')).not.toBeInTheDocument()
623+
expect(screen.queryByText('Mentees')).not.toBeInTheDocument()
624+
})
625+
626+
it('handles invalid avatar URL with query params correctly (separator check)', () => {
627+
const mentors = [createMockContributor('mentor1', 'invalid-url?foo=bar')]
628+
const modules = [createMockModule({ mentors }), createMockModule({ key: 'mod2' })]
629+
630+
render(<ModuleCard modules={modules} />)
631+
632+
const images = screen.getAllByTestId('next-image')
633+
expect(images[0].getAttribute('src')).toContain('&s=60')
634+
})
635+
636+
it('uses mentee name for avatar alt and title', () => {
637+
const mentees = [
638+
createMockContributor('mentee1', 'https://example.com/avatar1.png', 'Jane Doe'),
639+
]
640+
const modules = [createMockModule({ mentees }), createMockModule({ key: 'mod2' })]
641+
642+
render(<ModuleCard modules={modules} />)
643+
644+
const image = screen.getAllByTestId('next-image')[0]
645+
expect(image.getAttribute('alt')).toBe('Jane Doe')
646+
expect(image.getAttribute('title')).toBe('Jane Doe')
647+
})
648+
649+
it('falls back to mentee login for avatar alt and title', () => {
650+
const mentees = [
651+
{
652+
id: 'id-mentee1',
653+
login: 'mentee1',
654+
name: '',
655+
avatarUrl: 'https://example.com/avatar1.png',
656+
},
657+
]
658+
const modules = [createMockModule({ mentees }), createMockModule({ key: 'mod2' })]
659+
660+
render(<ModuleCard modules={modules} />)
661+
662+
const image = screen.getAllByTestId('next-image')[0]
663+
expect(image.getAttribute('alt')).toBe('mentee1')
664+
expect(image.getAttribute('title')).toBe('mentee1')
665+
})
613666
})
614667

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

639-
// Should not throw
640692
expect(() => render(<ModuleCard modules={modules} />)).not.toThrow()
641693
})
642694

0 commit comments

Comments
 (0)