Skip to content
4 changes: 4 additions & 0 deletions cspell/custom-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ BOTTOMPADDING
CCSP
CISSP
Cañón
Cyclonedx
DRF
GBP
GFKs
GSOC
GTM
Héllo
Kateryna
Kimminich
NETTACKER
NOASSERTION
NOSONAR
Expand Down Expand Up @@ -43,6 +45,7 @@ arithmatex
arkid15r
askowasp
bangbang
bjornkimminich
bsky
certbot
collectstatic
Expand All @@ -52,6 +55,7 @@ csrfguard
csrfprotector
csrftoken
cva
cyclonedx
demojize
dismissable
dsn
Expand Down
110 changes: 70 additions & 40 deletions frontend/__tests__/unit/components/BreadCrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,98 @@
import { render, screen } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import BreadCrumbs from 'components/BreadCrumbs'
import BreadCrumbRenderer from 'components/BreadCrumbs'
import '@testing-library/jest-dom'

jest.mock('next/navigation', () => ({
usePathname: jest.fn(),
}))
describe('BreadCrumbRenderer', () => {
const mockItems = [
{ title: 'Home', path: '/' },
{ title: 'Projects', path: '/projects' },
{ title: 'OWASP ZAP', path: '/projects/zap' },
]

describe('BreadCrumb', () => {
afterEach(() => {
jest.clearAllMocks()
test('renders all breadcrumb items', () => {
render(<BreadCrumbRenderer items={mockItems} />)

expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Projects')).toBeInTheDocument()
expect(screen.getByText('OWASP ZAP')).toBeInTheDocument()
})

test('does not render on root path "/"', () => {
;(usePathname as jest.Mock).mockReturnValue('/')
test('renders navigation element with correct aria-label', () => {
render(<BreadCrumbRenderer items={mockItems} />)

render(<BreadCrumbs />)
expect(screen.queryByText('Home')).not.toBeInTheDocument()
const nav = screen.getByRole('navigation')
expect(nav).toHaveAttribute('aria-label', 'breadcrumb')
})

test('renders breadcrumb with multiple segments', () => {
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
test('renders clickable links for non-last items', () => {
render(<BreadCrumbRenderer items={mockItems} />)

render(<BreadCrumbs />)
const homeLink = screen.getByText('Home').closest('a')
const projectsLink = screen.getByText('Projects').closest('a')

expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Users')).toBeInTheDocument()
expect(screen.getByText('Profile')).toBeInTheDocument()
expect(homeLink).toHaveAttribute('href', '/')
expect(projectsLink).toHaveAttribute('href', '/projects')
})

test('disables the last segment (non-clickable)', () => {
;(usePathname as jest.Mock).mockReturnValue('/settings/account')
test('disables the last item (non-clickable)', () => {
render(<BreadCrumbRenderer items={mockItems} />)

const lastItem = screen.getByText('OWASP ZAP')
expect(lastItem).not.toHaveAttribute('href')
expect(lastItem.tagName).toBe('SPAN')
})

render(<BreadCrumbs />)
test('applies hover styles to clickable links', () => {
render(<BreadCrumbRenderer items={mockItems} />)

const lastSegment = screen.getByText('Account')
expect(lastSegment).toBeInTheDocument()
expect(lastSegment).not.toHaveAttribute('href')
const homeLink = screen.getByText('Home').closest('a')
expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline')
})

test('links have correct href attributes', () => {
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
test('applies disabled styling to last breadcrumb', () => {
render(<BreadCrumbRenderer items={mockItems} />)

render(<BreadCrumbs />)
const lastItem = screen.getByText('OWASP ZAP')
expect(lastItem).toHaveClass('cursor-default', 'font-semibold')
})

const homeLink = screen.getByText('Home').closest('a')
const dashboardLink = screen.getByText('Dashboard').closest('a')
const usersLink = screen.getByText('Users').closest('a')
test('renders chevron separators between items', () => {
const { container } = render(<BreadCrumbRenderer items={mockItems} />)

expect(homeLink).toHaveAttribute('href', '/')
expect(dashboardLink).toHaveAttribute('href', '/dashboard')
expect(usersLink).toHaveAttribute('href', '/dashboard/users')
const separators = container.querySelectorAll('[data-slot="separator"]')
expect(separators).toHaveLength(2)
})

test('links have hover styles', () => {
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users')
test('handles single item (home only)', () => {
const singleItem = [{ title: 'Home', path: '/' }]
render(<BreadCrumbRenderer items={singleItem} />)

render(<BreadCrumbs />)
expect(screen.getByText('Home')).toBeInTheDocument()
const separators = screen.queryByRole('separator')
expect(separators).not.toBeInTheDocument()
})

test('handles empty items array', () => {
const { container } = render(<BreadCrumbRenderer items={[]} />)

const breadcrumbList = container.querySelector('[data-slot="list"]')
expect(breadcrumbList?.children).toHaveLength(0)
})

test('applies correct wrapper styling', () => {
const { container } = render(<BreadCrumbRenderer items={mockItems} />)

const wrapper = container.querySelector('.mt-16')
expect(wrapper).toHaveClass('w-full', 'pt-4')
})

test('links have correct href attributes', () => {
render(<BreadCrumbRenderer items={mockItems} />)

const homeLink = screen.getByText('Home').closest('a')
const dashboardLink = screen.getByText('Dashboard').closest('a')
const projectsLink = screen.getByText('Projects').closest('a')

expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline')
expect(dashboardLink).toHaveClass('hover:text-blue-700', 'hover:underline')
expect(homeLink).toHaveAttribute('href', '/')
expect(projectsLink).toHaveAttribute('href', '/projects')
})
})
148 changes: 148 additions & 0 deletions frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { render, screen } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import BreadCrumbsWrapper from 'components/BreadCrumbsWrapper'
import '@testing-library/jest-dom'

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

describe('BreadCrumbsWrapper', () => {
afterEach(() => {
jest.clearAllMocks()
})

describe('Route Detection - Should Hide', () => {
test('returns null on home page', () => {
;(usePathname as jest.Mock).mockReturnValue('/')

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for project detail pages', () => {
;(usePathname as jest.Mock).mockReturnValue('/projects/zap')

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for member detail pages', () => {
;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use users/project names that we already added to cspell to not add new items? Or just some fake ones, that would be even better.


const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for chapter detail pages', () => {
;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore')

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for committee detail pages', () => {
;(usePathname as jest.Mock).mockReturnValue('/committees/outreach')

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for organization detail pages', () => {
;(usePathname as jest.Mock).mockReturnValue('/organizations/cyclonedx')

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})

test('returns null for nested repository pages', () => {
;(usePathname as jest.Mock).mockReturnValue(
'/organizations/cyclonedx/repositories/cyclonedx-python'
)

const { container } = render(<BreadCrumbsWrapper />)
expect(container.firstChild).toBeNull()
})
})

describe('Route Detection - Should Render', () => {
test('renders for projects list page', () => {
;(usePathname as jest.Mock).mockReturnValue('/projects')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Projects')).toBeInTheDocument()
})

test('renders for members list page', () => {
;(usePathname as jest.Mock).mockReturnValue('/members')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Members')).toBeInTheDocument()
})

test('renders for chapters list page', () => {
;(usePathname as jest.Mock).mockReturnValue('/chapters')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Chapters')).toBeInTheDocument()
})
})

describe('Auto-Generation Logic', () => {
test('capitalizes single-word segments', () => {
;(usePathname as jest.Mock).mockReturnValue('/about')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('About')).toBeInTheDocument()
})

test('replaces dashes with spaces and capitalizes', () => {
;(usePathname as jest.Mock).mockReturnValue('/some-page')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Some page')).toBeInTheDocument()
})

test('handles multi-segment paths correctly', () => {
;(usePathname as jest.Mock).mockReturnValue('/community/events')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Community')).toBeInTheDocument()
expect(screen.getByText('Events')).toBeInTheDocument()
})

test('builds progressive paths for links', () => {
;(usePathname as jest.Mock).mockReturnValue('/community/events/conferences')

render(<BreadCrumbsWrapper />)

const homeLink = screen.getByText('Home').closest('a')
const communityLink = screen.getByText('Community').closest('a')
const eventsLink = screen.getByText('Events').closest('a')

expect(homeLink).toHaveAttribute('href', '/')
expect(communityLink).toHaveAttribute('href', '/community')
expect(eventsLink).toHaveAttribute('href', '/community/events')
})
})

describe('Edge Cases', () => {
test('handles trailing slashes', () => {
;(usePathname as jest.Mock).mockReturnValue('/projects/')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Projects')).toBeInTheDocument()
})

test('handles paths with special characters in segment names', () => {
;(usePathname as jest.Mock).mockReturnValue('/test-page-name')

render(<BreadCrumbsWrapper />)
expect(screen.getByText('Test page name')).toBeInTheDocument()
})
})
})
Loading
Loading