diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 31cf216e82..980ac54aa5 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -6,6 +6,7 @@ BOTTOMPADDING CCSP CISSP Cañón +Cyclonedx DRF GBP GFKs @@ -13,6 +14,7 @@ GSOC GTM Héllo Kateryna +Kimminich NETTACKER NOASSERTION NOSONAR @@ -43,6 +45,7 @@ arithmatex arkid15r askowasp bangbang +bjornkimminich bsky certbot collectstatic @@ -52,6 +55,7 @@ csrfguard csrfprotector csrftoken cva +cyclonedx demojize dismissable dsn diff --git a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx index 17511e18ad..13fdddbd8e 100644 --- a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx +++ b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx @@ -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() + + 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() - render() - 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() - render() + 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() + + const lastItem = screen.getByText('OWASP ZAP') + expect(lastItem).not.toHaveAttribute('href') + expect(lastItem.tagName).toBe('SPAN') + }) - render() + test('applies hover styles to clickable links', () => { + render() - 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() - render() + 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() - 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() - render() + expect(screen.getByText('Home')).toBeInTheDocument() + const separators = screen.queryByRole('separator') + expect(separators).not.toBeInTheDocument() + }) + + test('handles empty items array', () => { + const { container } = render() + + const breadcrumbList = container.querySelector('[data-slot="list"]') + expect(breadcrumbList?.children).toHaveLength(0) + }) + + test('applies correct wrapper styling', () => { + const { container } = render() + + const wrapper = container.querySelector('.mt-16') + expect(wrapper).toHaveClass('w-full', 'pt-4') + }) + + test('links have correct href attributes', () => { + render() 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') }) }) diff --git a/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx b/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx new file mode 100644 index 0000000000..5fe4b96d12 --- /dev/null +++ b/frontend/__tests__/unit/components/BreadCrumbsWrapper.test.tsx @@ -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() + expect(container.firstChild).toBeNull() + }) + + test('returns null for project detail pages', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + test('returns null for member detail pages', () => { + ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich') + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + test('returns null for chapter detail pages', () => { + ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore') + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + test('returns null for committee detail pages', () => { + ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach') + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + test('returns null for organization detail pages', () => { + ;(usePathname as jest.Mock).mockReturnValue('/organizations/cyclonedx') + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + test('returns null for nested repository pages', () => { + ;(usePathname as jest.Mock).mockReturnValue( + '/organizations/cyclonedx/repositories/cyclonedx-python' + ) + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + }) + + describe('Route Detection - Should Render', () => { + test('renders for projects list page', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects') + + render() + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Projects')).toBeInTheDocument() + }) + + test('renders for members list page', () => { + ;(usePathname as jest.Mock).mockReturnValue('/members') + + render() + expect(screen.getByText('Home')).toBeInTheDocument() + expect(screen.getByText('Members')).toBeInTheDocument() + }) + + test('renders for chapters list page', () => { + ;(usePathname as jest.Mock).mockReturnValue('/chapters') + + render() + 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() + expect(screen.getByText('About')).toBeInTheDocument() + }) + + test('replaces dashes with spaces and capitalizes', () => { + ;(usePathname as jest.Mock).mockReturnValue('/some-page') + + render() + expect(screen.getByText('Some page')).toBeInTheDocument() + }) + + test('handles multi-segment paths correctly', () => { + ;(usePathname as jest.Mock).mockReturnValue('/community/events') + + render() + 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() + + 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() + expect(screen.getByText('Projects')).toBeInTheDocument() + }) + + test('handles paths with special characters in segment names', () => { + ;(usePathname as jest.Mock).mockReturnValue('/test-page-name') + + render() + expect(screen.getByText('Test page name')).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/__tests__/unit/components/PageLayout.test.tsx b/frontend/__tests__/unit/components/PageLayout.test.tsx new file mode 100644 index 0000000000..7251942180 --- /dev/null +++ b/frontend/__tests__/unit/components/PageLayout.test.tsx @@ -0,0 +1,137 @@ +import { render, screen } from '@testing-library/react' +import { usePathname } from 'next/navigation' +import PageLayout from 'components/PageLayout' +import '@testing-library/jest-dom' + +jest.mock('next/navigation', () => ({ + usePathname: jest.fn(), +})) + +describe('PageLayout', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + test('renders children components', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + render( + +
Child Content
+
+ ) + + expect(screen.getByText('Child Content')).toBeInTheDocument() + }) + + test('renders breadcrumbs above children', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { container } = render( + +
Child Content
+
+ ) + + const breadcrumbNav = container.querySelector('nav') + const childContent = screen.getByText('Child Content') + + expect(breadcrumbNav?.compareDocumentPosition(childContent)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + }) + + describe('BreadcrumbData Prop Handling', () => { + test('passes projectName to breadcrumbs', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + render( + +
Content
+
+ ) + + expect(screen.getByText('OWASP ZAP')).toBeInTheDocument() + }) + + test('passes memberName to breadcrumbs', () => { + ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich') + + render( + +
Content
+
+ ) + + expect(screen.getByText('Björn Kimminich')).toBeInTheDocument() + }) + + test('passes chapterName to breadcrumbs', () => { + ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore') + + render( + +
Content
+
+ ) + + expect(screen.getByText('Bangalore Chapter')).toBeInTheDocument() + }) + + test('passes committeeName to breadcrumbs', () => { + ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach') + + render( + +
Content
+
+ ) + + expect(screen.getByText('Outreach Committee')).toBeInTheDocument() + }) + + test('passes orgName and repoName for repository pages', () => { + ;(usePathname as jest.Mock).mockReturnValue( + '/organizations/cyclonedx/repositories/cyclonedx-python' + ) + + render( + +
Content
+
+ ) + + expect(screen.getByText('CycloneDX BOM Standard')).toBeInTheDocument() + expect(screen.getByText('Cyclonedx Python')).toBeInTheDocument() + }) + + test('handles undefined breadcrumbData', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/juice-shop') + + render( + +
Content
+
+ ) + + expect(screen.getByText('Juice shop')).toBeInTheDocument() + }) + + test('handles partial breadcrumbData (wrong field for route type)', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + render( + +
Content
+
+ ) + + expect(screen.getByText('John Doe')).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts b/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts new file mode 100644 index 0000000000..ea04830dc5 --- /dev/null +++ b/frontend/__tests__/unit/hooks/useBreadcrumbs.test.ts @@ -0,0 +1,242 @@ +import { renderHook } from '@testing-library/react' +import { useBreadcrumbs } from 'hooks/useBreadcrumbs' +import { usePathname } from 'next/navigation' + +jest.mock('next/navigation', () => ({ + usePathname: jest.fn(), +})) + +describe('useBreadcrumbs', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Single-slug routes', () => { + test('generates breadcrumbs for project page with projectName', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' })) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Projects', path: '/projects' }, + { title: 'OWASP ZAP', path: '/projects/zap' }, + ]) + }) + + test('fallback to formatted slug if projectName not provided', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/juice-shop') + + const { result } = renderHook(() => useBreadcrumbs({})) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Projects', path: '/projects' }, + { title: 'Juice shop', path: '/projects/juice-shop' }, + ]) + }) + + test('generates breadcrumbs for member page with memberName', () => { + ;(usePathname as jest.Mock).mockReturnValue('/members/bjornkimminich') + + const { result } = renderHook(() => useBreadcrumbs({ memberName: 'Björn Kimminich' })) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Members', path: '/members' }, + { title: 'Björn Kimminich', path: '/members/bjornkimminich' }, + ]) + }) + + test('generates breadcrumbs for chapter page with chapterName', () => { + ;(usePathname as jest.Mock).mockReturnValue('/chapters/bangalore') + + const { result } = renderHook(() => useBreadcrumbs({ chapterName: 'Bangalore Chapter' })) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Chapters', path: '/chapters' }, + { title: 'Bangalore Chapter', path: '/chapters/bangalore' }, + ]) + }) + + test('generates breadcrumbs for committee page with committeeName', () => { + ;(usePathname as jest.Mock).mockReturnValue('/committees/outreach') + + const { result } = renderHook(() => useBreadcrumbs({ committeeName: 'Outreach Committee' })) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Committees', path: '/committees' }, + { title: 'Outreach Committee', path: '/committees/outreach' }, + ]) + }) + + test('generates breadcrumbs for organization page with orgName', () => { + ;(usePathname as jest.Mock).mockReturnValue('/organizations/cyclonedx') + + const { result } = renderHook(() => useBreadcrumbs({ orgName: 'CycloneDX BOM Standard' })) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Organizations', path: '/organizations' }, + { title: 'CycloneDX BOM Standard', path: '/organizations/cyclonedx' }, + ]) + }) + }) + + describe('Multi-slug routes (nested repositories)', () => { + test('detects nested repository route pattern', () => { + ;(usePathname as jest.Mock).mockReturnValue( + '/organizations/cyclonedx/repositories/cyclonedx-python' + ) + + const { result } = renderHook(() => + useBreadcrumbs({ + orgName: 'CycloneDX BOM Standard', + repoName: 'Cyclonedx Python', + }) + ) + + expect(result.current).toHaveLength(4) + }) + + test('generates correct breadcrumbs for repository page', () => { + ;(usePathname as jest.Mock).mockReturnValue( + '/organizations/cyclonedx/repositories/cyclonedx-python' + ) + + const { result } = renderHook(() => + useBreadcrumbs({ + orgName: 'CycloneDX BOM Standard', + repoName: 'Cyclonedx Python', + }) + ) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Organizations', path: '/organizations' }, + { + title: 'CycloneDX BOM Standard', + path: '/organizations/cyclonedx', + }, + { + title: 'Cyclonedx Python', + path: '/organizations/cyclonedx/repositories/cyclonedx-python', + }, + ]) + }) + + test('uses orgName from breadcrumbData', () => { + ;(usePathname as jest.Mock).mockReturnValue('/organizations/owasp-amass/repositories/amass') + + const { result } = renderHook(() => + useBreadcrumbs({ + orgName: 'OWASP Amass Project', + repoName: 'Amass', + }) + ) + + const orgBreadcrumb = result.current.find( + (item) => item.path === '/organizations/owasp-amass' + ) + expect(orgBreadcrumb?.title).toBe('OWASP Amass Project') + }) + + test('formats repo name by splitting dashes and capitalizing', () => { + ;(usePathname as jest.Mock).mockReturnValue( + '/organizations/cyclonedx/repositories/cyclonedx-maven-plugin' + ) + + const { result } = renderHook(() => useBreadcrumbs({})) + + const repoBreadcrumb = result.current[result.current.length - 1] + expect(repoBreadcrumb.title).toBe('Cyclonedx maven plugin') + }) + + test('fallback to formatted slugs if data not provided', () => { + ;(usePathname as jest.Mock).mockReturnValue('/organizations/some-org/repositories/some-repo') + + const { result } = renderHook(() => useBreadcrumbs({})) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Organizations', path: '/organizations' }, + { title: 'Some org', path: '/organizations/some-org' }, + { + title: 'Some repo', + path: '/organizations/some-org/repositories/some-repo', + }, + ]) + }) + }) + + describe('Path building', () => { + test('builds progressive paths correctly', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' })) + + expect(result.current[0].path).toBe('/') + expect(result.current[1].path).toBe('/projects') + expect(result.current[2].path).toBe('/projects/zap') + }) + + test('builds correct nested paths for repos', () => { + ;(usePathname as jest.Mock).mockReturnValue('/organizations/org/repositories/repo') + + const { result } = renderHook(() => + useBreadcrumbs({ orgName: 'Org Name', repoName: 'Repo Name' }) + ) + + expect(result.current[0].path).toBe('/') + expect(result.current[1].path).toBe('/organizations') + expect(result.current[2].path).toBe('/organizations/org') + expect(result.current[3].path).toBe('/organizations/org/repositories/repo') + }) + }) + + describe('Edge cases', () => { + test('handles undefined breadcrumbData', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/test') + + const { result } = renderHook(() => useBreadcrumbs(undefined)) + + expect(result.current).toEqual([ + { title: 'Home', path: '/' }, + { title: 'Projects', path: '/projects' }, + { title: 'Test', path: '/projects/test' }, + ]) + }) + + test('handles partial breadcrumbData (uses first matching field)', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { result } = renderHook(() => useBreadcrumbs({ memberName: 'John Doe' })) + + expect(result.current[2].title).toBe('John Doe') + }) + + test('handles paths with trailing slashes', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap/') + + const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' })) + + expect(result.current).toHaveLength(3) + expect(result.current[2].title).toBe('OWASP ZAP') + }) + + test('each breadcrumb item has title and path properties', () => { + ;(usePathname as jest.Mock).mockReturnValue('/projects/zap') + + const { result } = renderHook(() => useBreadcrumbs({ projectName: 'OWASP ZAP' })) + + result.current.forEach((item) => { + expect(item).toHaveProperty('title') + expect(item).toHaveProperty('path') + expect(typeof item.title).toBe('string') + expect(typeof item.path).toBe('string') + }) + }) + }) +}) diff --git a/frontend/__tests__/unit/pages/ChapterDetails.test.tsx b/frontend/__tests__/unit/pages/ChapterDetails.test.tsx index 978f55026a..96da372687 100644 --- a/frontend/__tests__/unit/pages/ChapterDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ChapterDetails.test.tsx @@ -28,6 +28,7 @@ jest.mock('next/navigation', () => ({ ...jest.requireActual('next/navigation'), useRouter: jest.fn(() => mockRouter), useParams: () => ({ chapterKey: 'test-chapter' }), + usePathname: jest.fn(() => '/chapters/test-chapter'), })) describe('chapterDetailsPage Component', () => { diff --git a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx index e27fff0286..a9c776505f 100644 --- a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx +++ b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx @@ -22,6 +22,7 @@ jest.mock('next/navigation', () => ({ ...jest.requireActual('next/navigation'), useRouter: jest.fn(() => mockRouter), useParams: () => ({ committeeKey: 'test-committee' }), + usePathname: jest.fn(() => '/committees/test-committee'), })) describe('CommitteeDetailsPage Component', () => { @@ -52,7 +53,7 @@ describe('CommitteeDetailsPage Component', () => { test('renders committee data correctly', async () => { render() await waitFor(() => { - expect(screen.getByText('Test Committee')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Test Committee' })).toBeInTheDocument() }) expect(screen.getByText('This is a test committee summary.')).toBeInTheDocument() expect(screen.getByText('Leader 1')).toBeInTheDocument() diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx index 1b1281fe65..1a4e27d98b 100644 --- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx +++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx @@ -28,6 +28,7 @@ jest.mock('next/navigation', () => ({ ...jest.requireActual('next/navigation'), useRouter: jest.fn(() => mockRouter), useParams: () => ({ repositoryKey: 'test-org' }), + usePathname: jest.fn(() => '/organizations/test-org'), })) const mockError = { @@ -70,7 +71,7 @@ describe('OrganizationDetailsPage', () => { render() await waitFor(() => { - expect(screen.getByText('Test Organization')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Test Organization' })).toBeInTheDocument() }) expect(screen.getByText('@test-org')).toBeInTheDocument() diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index b425c42838..706c56d3a3 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -10,6 +10,7 @@ import type { Contributor } from 'types/contributor' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' export default function ChapterDetailsPage() { const { chapterKey } = useParams<{ chapterKey: string }>() @@ -60,17 +61,19 @@ export default function ChapterDetailsPage() { }, ] return ( - + + + ) } diff --git a/frontend/src/app/committees/[committeeKey]/page.tsx b/frontend/src/app/committees/[committeeKey]/page.tsx index dea63a3afe..74d7ccd84a 100644 --- a/frontend/src/app/committees/[committeeKey]/page.tsx +++ b/frontend/src/app/committees/[committeeKey]/page.tsx @@ -17,6 +17,7 @@ import type { Contributor } from 'types/contributor' import { formatDate } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' export default function CommitteeDetailsPage() { const { committeeKey } = useParams<{ committeeKey: string }>() @@ -80,14 +81,16 @@ export default function CommitteeDetailsPage() { ] return ( - + + + ) } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 62b643fa3f..7b46dd8dae 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -6,7 +6,7 @@ import { Providers } from 'wrappers/provider' import { GTM_ID } from 'utils/env.client' import { IS_GITHUB_AUTH_ENABLED } from 'utils/env.server' import AutoScrollToTop from 'components/AutoScrollToTop' -import BreadCrumbs from 'components/BreadCrumbs' +import BreadCrumbsWrapper from 'components/BreadCrumbsWrapper' import Footer from 'components/Footer' import Header from 'components/Header' import ScrollToTop from 'components/ScrollToTop' @@ -74,7 +74,7 @@ export default function RootLayout({
- +
{children}