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
2 changes: 2 additions & 0 deletions frontend/__tests__/mockData/mockSnapshotData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const mockSnapshotDetailsData = {
errorMessage: '',
newReleases: [
{
id: 'release-1',
name: 'v0.9.2',
publishedAt: '2024-12-13T14:43:46+00:00',
tagName: 'v0.9.2',
Expand All @@ -24,6 +25,7 @@ export const mockSnapshotDetailsData = {
},
},
{
id: 'release-2',
name: 'Latest pre-release',
publishedAt: '2024-12-13T13:17:30+00:00',
tagName: 'pre-release',
Expand Down
89 changes: 39 additions & 50 deletions frontend/__tests__/unit/components/Footer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jest.mock('utils/constants', () => ({
links: [
{ text: 'About', href: '/about' },
{ text: 'Contribute', href: 'https://github.com/OWASP/Nest/blob/main/CONTRIBUTING.md' },
{ text: 'Empty Href Link', href: '' }, // Empty href to test fallback
],
},
{
Expand All @@ -71,9 +72,18 @@ jest.mock('utils/constants', () => ({
],
}))

jest.mock('utils/env.client', () => ({
let mockEnv = {
ENVIRONMENT: 'production',
RELEASE_VERSION: '1.2.3',
}

jest.mock('utils/env.client', () => ({
get ENVIRONMENT() {
return mockEnv.ENVIRONMENT
},
get RELEASE_VERSION() {
return mockEnv.RELEASE_VERSION
},
}))

import { FaGithub, FaSlack } from 'react-icons/fa6'
Expand All @@ -93,6 +103,10 @@ describe('Footer', () => {

beforeEach(() => {
jest.clearAllMocks()
mockEnv = {
ENVIRONMENT: 'production',
RELEASE_VERSION: '1.2.3',
}
})

afterEach(() => {
Expand Down Expand Up @@ -132,10 +146,12 @@ describe('Footer', () => {
}
}
}

for (const link of regularLinks) {
const linkElement = screen.getByRole('link', { name: link.text })
expect(linkElement).toBeInTheDocument()
expect(linkElement).toHaveAttribute('href', link.href)
const expectedHref = link.href || '/'
expect(linkElement).toHaveAttribute('href', expectedHref)
expect(linkElement).toHaveAttribute('target', '_blank')
}

Expand Down Expand Up @@ -266,63 +282,29 @@ describe('Footer', () => {
expect(versionLink).toHaveAttribute('rel', 'noopener noreferrer')
})

test('handles span elements correctly', () => {
renderFooter()

const spanText = screen.getByText('Plain Text')
expect(spanText.tagName).toBe('SPAN')
expect(spanText).toHaveClass('text-slate-600', 'dark:text-slate-400')
})
})

describe('Version Link Behavior', () => {
let originalEnvironment: string
let originalReleaseVersion: string
let envModule: typeof import('utils/env.client')

beforeEach(() => {
jest.clearAllMocks()
envModule = jest.requireMock<typeof import('utils/env.client')>('utils/env.client')
originalEnvironment = envModule.ENVIRONMENT
originalReleaseVersion = envModule.RELEASE_VERSION
})

afterEach(() => {
if (envModule) {
envModule.ENVIRONMENT = originalEnvironment
envModule.RELEASE_VERSION = originalReleaseVersion
}
})

test('renders version as commit link in staging environment', () => {
envModule.ENVIRONMENT = 'staging'
envModule.RELEASE_VERSION = '24.2.10-12c25c5'
test('renders version as commit link in non-production environment', () => {
mockEnv.ENVIRONMENT = 'staging'
mockEnv.RELEASE_VERSION = '24.2.10-12c25c5'

const { container } = render(<Footer />)
const versionLink = container.querySelector('a[href*="commit"]')
renderFooter()

const versionText = screen.getByText('v24.2.10-12c25c5')
const versionLink = versionText.closest('a')
expect(versionLink).toBeInTheDocument()
expect(versionLink).toHaveAttribute('href', 'https://github.com/OWASP/Nest/commit/12c25c5')
expect(versionLink).toHaveAttribute('target', '_blank')
expect(versionLink).toHaveAttribute('rel', 'noopener noreferrer')
expect(versionLink).toHaveTextContent('v24.2.10-12c25c5')
})

test('renders version as release tag link in production environment', () => {
envModule.ENVIRONMENT = 'production'
envModule.RELEASE_VERSION = '1.2.3'
mockEnv.ENVIRONMENT = 'production'
mockEnv.RELEASE_VERSION = '1.2.3'
})

const { container } = render(<Footer />)
const versionLink = container.querySelector('a[href*="releases"]')
test('handles span elements correctly', () => {
renderFooter()

expect(versionLink).toBeInTheDocument()
expect(versionLink).toHaveAttribute(
'href',
'https://github.com/OWASP/Nest/releases/tag/1.2.3'
)
expect(versionLink).toHaveAttribute('target', '_blank')
expect(versionLink).toHaveAttribute('rel', 'noopener noreferrer')
expect(versionLink).toHaveTextContent('v1.2.3')
const spanText = screen.getByText('Plain Text')
expect(spanText.tagName).toBe('SPAN')
expect(spanText).toHaveClass('text-slate-600', 'dark:text-slate-400')
})
})

Expand Down Expand Up @@ -420,6 +402,13 @@ describe('Footer', () => {
expect(aboutLink).toHaveAttribute('href', '/about')
})

test('handles empty href with fallback to root path', () => {
renderFooter()

const emptyHrefLink = screen.getByRole('link', { name: 'Empty Href Link' })
expect(emptyHrefLink).toHaveAttribute('href', '/')
})

test('handles sections with span elements', () => {
renderFooter()

Expand Down
38 changes: 38 additions & 0 deletions frontend/__tests__/unit/components/HealthMetrics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,43 @@ describe('HealthMetrics', () => {
expect(props.days).toEqual([0, 0])
expect(props.requirements).toEqual([0, 0])
})

it('handles completely null/undefined optional fields', () => {
const nullData = [
{
createdAt: null,
openIssuesCount: null,
unassignedIssuesCount: undefined,
unansweredIssuesCount: null,
openPullRequestsCount: undefined,
starsCount: null,
forksCount: undefined,
id: 'null-test',
projectKey: 'null-project',
},
]
// @ts-expect-error - testing specific edge case with mocked data
render(<HealthMetrics data={nullData} />)

const lineCharts = screen.getAllByTestId('LineChart')

const issuesProps = JSON.parse(lineCharts[0].dataset.props || '{}')
expect(issuesProps.series[0].data).toEqual([0]) // openIssuesCount
expect(issuesProps.series[1].data).toEqual([0]) // unassignedIssuesCount
expect(issuesProps.series[2].data).toEqual([0]) // unansweredIssuesCount
expect(issuesProps.labels).toEqual(['']) // createdAt

// Pull Requests Trend
const prProps = JSON.parse(lineCharts[1].dataset.props || '{}')
expect(prProps.series[0].data).toEqual([0])

// Stars Trend
const starsProps = JSON.parse(lineCharts[2].dataset.props || '{}')
expect(starsProps.series[0].data).toEqual([0])

// Forks Trend
const forksProps = JSON.parse(lineCharts[3].dataset.props || '{}')
expect(forksProps.series[0].data).toEqual([0])
})
})
})
42 changes: 41 additions & 1 deletion frontend/__tests__/unit/components/IssuesTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import React from 'react'
import IssuesTable, { type IssueRow } from 'components/IssuesTable'
import { LabelList } from 'components/LabelList'

const mockPush = jest.fn()

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
push: mockPush,
}),
}))

Expand Down Expand Up @@ -367,6 +369,26 @@ describe('<IssuesTable />', () => {
const user4Texts = screen.getAllByText('user4')
expect(user4Texts.length).toBeGreaterThan(0)
})

it('uses name when login is not available', () => {
const issueWithoutLogin: IssueRow = {
objectID: '8-no-login',
number: 133,
title: 'Issue Without Login',
state: 'open',
labels: [],
assignees: [
{
avatarUrl: 'https://example.com/avatar5.jpg',
login: '',
name: 'User Five',
},
],
}
render(<IssuesTable issues={[issueWithoutLogin]} />)
const user5Texts = screen.getAllByText('User Five')
expect(user5Texts.length).toBeGreaterThan(0)
})
})

describe('Click Handlers', () => {
Expand All @@ -378,6 +400,24 @@ describe('<IssuesTable />', () => {
fireEvent.click(issueButtons[0])
expect(onIssueClick).toHaveBeenCalledWith(123)
})

it('navigates to issue URL when onIssueClick is not provided', () => {
const issueUrl = (num: number) => `/issues/${num}`
render(<IssuesTable {...defaultProps} onIssueClick={undefined} issueUrl={issueUrl} />)
const issueButtons = screen.getAllByRole('button', { name: /Test Issue 1/i })
expect(issueButtons.length).toBeGreaterThan(0)
fireEvent.click(issueButtons[0])
expect(mockPush).toHaveBeenCalledWith('/issues/123')
})

it('does nothing when onIssueClick and issueUrl are not provided', () => {
mockPush.mockClear()
render(<IssuesTable {...defaultProps} onIssueClick={undefined} issueUrl={undefined} />)
const issueButtons = screen.getAllByRole('button', { name: /Test Issue 1/i })
expect(issueButtons.length).toBeGreaterThan(0)
fireEvent.click(issueButtons[0])
expect(mockPush).not.toHaveBeenCalled()
})
})

describe('Empty State', () => {
Expand Down
43 changes: 43 additions & 0 deletions frontend/__tests__/unit/components/LabelList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { LabelList } from 'components/LabelList'

describe('LabelList', () => {
it('renders nothing when labels are empty', () => {
const { container } = render(<LabelList entityKey="test-1" labels={[]} />)
expect(container).toBeEmptyDOMElement()
})

it('renders nothing when labels are undefined', () => {
const { container } = render(<LabelList entityKey="test-2" labels={undefined} />)
expect(container).toBeEmptyDOMElement()
})

it('renders labels correctly', () => {
render(<LabelList entityKey="test-3" labels={['label1', 'label2']} />)
expect(screen.getByText('label1')).toBeInTheDocument()
expect(screen.getByText('label2')).toBeInTheDocument()
})

it('renders max visible labels and remaining count', () => {
render(<LabelList entityKey="test-4" labels={['1', '2', '3', '4', '5', '6']} maxVisible={5} />)
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.getByText('5')).toBeInTheDocument()
expect(screen.queryByText('6')).not.toBeInTheDocument()
expect(screen.getByText('+1 more')).toBeInTheDocument()
})

it('applies custom className', () => {
const { container } = render(
<LabelList entityKey="test-5" labels={['label']} className="custom-class" />
)
expect(container.firstChild).toHaveClass('custom-class')
})

it('renders all labels when maxVisible is greater than labels length', () => {
render(<LabelList entityKey="test-6" labels={['1', '2']} maxVisible={5} />)
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.getByText('2')).toBeInTheDocument()
expect(screen.queryByText(/\+\d+ more/)).not.toBeInTheDocument()
})
})
50 changes: 50 additions & 0 deletions frontend/__tests__/unit/components/LogoCarousel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,56 @@ describe('MovingLogos (LogoCarousel)', () => {
})
})

describe('Coverage Edge Cases', () => {
it('uses url for key generation when id is missing', () => {
const sponsor: Sponsor = {
id: undefined as unknown as string,
name: 'Name',
imageUrl: 'https://img.com',
url: 'https://url.com',
sponsorType: 'Gold',
}
render(<MovingLogos sponsors={[sponsor]} />)
expect(screen.getAllByTestId('sponsor-link')).toHaveLength(4)
})

it('uses name for key generation when id and url are missing', () => {
const sponsor: Sponsor = {
id: undefined as unknown as string,
name: 'Name',
imageUrl: 'https://img.com',
url: '',
sponsorType: 'Gold',
}
render(<MovingLogos sponsors={[sponsor]} />)
expect(screen.getAllByTestId('sponsor-link')).toHaveLength(4)
})

it('uses generic fallback for key generation when id, url, and name are missing', () => {
const sponsor: Sponsor = {
id: undefined as unknown as string,
name: '',
imageUrl: 'https://img.com',
url: '',
sponsorType: 'Gold',
}
render(<MovingLogos sponsors={[sponsor]} />)
expect(screen.getAllByTestId('sponsor-link')).toHaveLength(4)
})

it('renders generic "Sponsor" text when name and image are missing', () => {
const sponsor: Sponsor = {
id: '1',
name: '',
imageUrl: '',
url: 'https://url.com',
sponsorType: 'Gold',
}
render(<MovingLogos sponsors={[sponsor]} />)
expect(screen.getAllByText('Sponsor')).toHaveLength(2)
})
})

describe('Accessibility Roles and Labels', () => {
it('provides proper alt text for images', () => {
render(<MovingLogos sponsors={mockSponsors} />)
Expand Down
14 changes: 14 additions & 0 deletions frontend/__tests__/unit/components/MetricsCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,18 @@ describe('MetricsCard component', () => {
expect(screen.getByText(/Score:/).textContent).toContain('55')
expect(screen.getByRole('link')).toHaveAttribute('href', '/projects/dashboard/metrics/another')
})

it('handles undefined optional props using defaults', () => {
const metric = makeMetric({
score: undefined,
createdAt: undefined,
})
render(<MetricsCard metric={metric} />)

const scoreText = screen.getByText(/Score: 0/)
expect(scoreText).toBeInTheDocument()
expect(scoreText.closest('div')).toHaveClass('bg-red-500')

expect(screen.getByText('N/A')).toBeInTheDocument()
})
})
Loading