Skip to content
Closed
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
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')

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