-
-
Notifications
You must be signed in to change notification settings - Fork 286
refactor: update breadcrumbs component to support dynamic title rende… #2573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Kunal1522
wants to merge
9
commits into
OWASP:main
Choose a base branch
from
Kunal1522:feature/dynamic-breadcrumbs-with-page-titles
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7a1353d
refactor: update breadcrumbs component to support dynamic title rende…
Kunal1522 5b00579
fix: use aria-current for last breadcrumb and consistent repo name fo…
Kunal1522 ef1cfb8
fix: use aria-current for last breadcrumb and consistent repo name fo…
Kunal1522 5f70589
consistent repo name formatting
Kunal1522 0e96f26
consistent repo name formatting
Kunal1522 cd1417d
Merge branch 'main' into feature/dynamic-breadcrumbs-with-page-titles
Kunal1522 bafe0e1
Merge branch 'main' into feature/dynamic-breadcrumbs-with-page-titles
kasya b431914
Fix key for repositoryCard
kasya 536119e
Run make-check
kasya File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 70 additions & 40 deletions
110
frontend/__tests__/unit/components/BreadCrumbs.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
148
frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| }) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.