Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
92 changes: 42 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,19 @@ jest.mock('utils/constants', () => ({
],
}))

jest.mock('utils/env.client', () => ({
// Create a mutable mock for environment variables
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
},
}))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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

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

afterEach(() => {
Expand Down Expand Up @@ -132,10 +147,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')
}
Comment thread
anurag2787 marked this conversation as resolved.

Expand Down Expand Up @@ -266,63 +283,31 @@ 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', () => {
// Set non-production environment with commit hash
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'
// Reset to defaults to avoid leaking state
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 +405,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
40 changes: 40 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,45 @@ 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} />)

// Verify LineCharts have 0s for data
const lineCharts = screen.getAllByTestId('LineChart')

// Issues Trend
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]) // openPullRequestsCount

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

// Forks Trend
const forksProps = JSON.parse(lineCharts[3].dataset.props || '{}')
expect(forksProps.series[0].data).toEqual([0]) // forksCount
})
})
})
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
16 changes: 16 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,20 @@ 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} />)

// Score defaults to 0, so it should be red (< 50)
const scoreText = screen.getByText(/Score: 0/)
expect(scoreText).toBeInTheDocument()
expect(scoreText.closest('div')).toHaveClass('bg-red-500')

// createdAt defaults to "N/A"
expect(screen.getByText('N/A')).toBeInTheDocument()
})
})
Loading