Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions frontend/__tests__/e2e/helpers/breadCrumbsHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Page, expect } from '@playwright/test'

export async function expectBreadcrumbVisible(page: Page, items: string[] = ['Home']) {
const breadcrumb = page.locator('[aria-label="breadcrumb"]')
await expect(breadcrumb).toBeVisible()
for (const item of items) {
await expect(breadcrumb.getByText(item)).toBeVisible()
}
}
5 changes: 5 additions & 0 deletions frontend/__tests__/e2e/pages/About.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockAboutData } from '@unit/data/mockAboutData'

Expand Down Expand Up @@ -84,4 +85,8 @@ test.describe('About Page', () => {
await newPage.waitForLoadState()
expect(newPage.url()).toContain('/members/')
})

test('breadcrumb renders correct segments on /about', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'About'])
})
})
5 changes: 5 additions & 0 deletions frontend/__tests__/e2e/pages/Chapters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockChapterData } from '@unit/data/mockChapterData'

Expand Down Expand Up @@ -54,4 +55,8 @@ test.describe('Chapters Page', () => {
await contributeButton.click()
await expect(page).toHaveURL('chapters/chapter_1')
})

test('breadcrumb renders correct segments on /chapters', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Chapters'])
})
})
5 changes: 5 additions & 0 deletions frontend/__tests__/e2e/pages/Committees.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockCommitteeData } from '@unit/data/mockCommitteeData'

Expand Down Expand Up @@ -54,4 +55,8 @@ test.describe('Committees Page', () => {
await contributeButton.click()
await expect(page).toHaveURL('/committees/committee_1')
})

test('breadcrumb renders correct segments on /committees', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Committees'])
})
})
4 changes: 4 additions & 0 deletions frontend/__tests__/e2e/pages/Contribute.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockContributeData } from '@unit/data/mockContributeData'

Expand Down Expand Up @@ -71,4 +72,7 @@ test.describe('Contribute Page', () => {
await CloseButton.click()
await expect(contributeButton).toBeVisible()
})
test('breadcrumb renders correct segments on /contribute', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Contribute'])
})
})
5 changes: 5 additions & 0 deletions frontend/__tests__/e2e/pages/Organizations.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockOrganizationData } from '@unit/data/mockOrganizationData'

Expand Down Expand Up @@ -40,4 +41,8 @@ test.describe('Organization Page', () => {
await expect(page.getByText('1k')).toBeVisible()
await expect(page.getByText('1.5k')).toBeVisible()
})

test('breadcrumb renders correct segments on /organizations', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Organizations'])
})
})
4 changes: 4 additions & 0 deletions frontend/__tests__/e2e/pages/Projects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import mockProjectData from '@unit/data/mockProjectData'

Expand Down Expand Up @@ -53,4 +54,7 @@ test.describe('Projects Page', () => {
await contributeButton.click()
await expect(page).toHaveURL('projects/project_1')
})
test('breadcrumb renders correct segments on /projects', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Projects'])
})
})
4 changes: 4 additions & 0 deletions frontend/__tests__/e2e/pages/Users.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectBreadcrumbVisible } from '@e2e/helpers/breadCrumbsHelper'
import { test, expect } from '@playwright/test'
import { mockUserData } from '@unit/data/mockUserData'

Expand Down Expand Up @@ -59,4 +60,7 @@ test.describe('Users Page', () => {
await expect(page.getByText('1k')).toBeVisible()
await expect(page.getByText('2k')).toBeVisible()
})
test('breadcrumb renders correct segments on /members', async ({ page }) => {
await expectBreadcrumbVisible(page, ['Home', 'Members'])
})
})
42 changes: 42 additions & 0 deletions frontend/__tests__/unit/components/BreadCrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react'
import { usePathname } from 'next/navigation'
import BreadCrumbs from 'components/BreadCrumbs'
import '@testing-library/jest-dom'

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

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

test('does not render on root path "/"', () => {
;(usePathname as jest.Mock).mockReturnValue('/')

render(<BreadCrumbs />)
expect(screen.queryByText('Home')).not.toBeInTheDocument()
})

test('renders breadcrumb with multiple segments', () => {
;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')

render(<BreadCrumbs />)

expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Users')).toBeInTheDocument()
expect(screen.getByText('Profile')).toBeInTheDocument()
})

test('disables the last segment (non-clickable)', () => {
;(usePathname as jest.Mock).mockReturnValue('/settings/account')

render(<BreadCrumbs />)

const lastSegment = screen.getByText('Account')
expect(lastSegment).toBeInTheDocument()
expect(lastSegment).not.toHaveAttribute('href')
})
})
2 changes: 2 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import React from 'react'
import { Providers } from 'wrappers/provider'
import BreadCrumbs from 'components/BreadCrumbs'
import Footer from 'components/Footer'

import Header from 'components/Header'
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function RootLayout({
>
<Providers>
<Header />
<BreadCrumbs />
{children}
<Footer />
<ScrollToTop />
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/components/BreadCrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client'

import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Breadcrumbs, BreadcrumbItem } from '@heroui/react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { capitalize } from 'utils/capitalize'
import { homeRoute } from 'utils/constants'

const capitalizeRoute = (str: string) => capitalize(str).replace(/-/g, ' ')

export default function BreadCrumbs() {
const pathname = usePathname()
const segments = pathname.split(homeRoute).filter(Boolean)

if (pathname === homeRoute) return null

return (
<div className="absolute bottom-2 left-0 top-1 mt-16 w-full py-2">
<div className="w-full px-8 sm:px-8 md:px-8 lg:px-8">
<Breadcrumbs
aria-label="breadcrumb"
separator={
<FontAwesomeIcon
icon={faChevronRight}
className="mx-1 text-xs text-gray-400 dark:text-gray-500"
/>
}
className="text-gray-800 dark:text-gray-200"
itemClasses={{
base: 'transition-colors duration-200',
item: 'text-sm font-medium',
separator: 'flex items-center',
}}
>
<BreadcrumbItem>
<Link href="/" className="hover:text-blue-700 hover:underline dark:text-blue-400">
Home
</Link>
</BreadcrumbItem>

{segments.map((segment, index) => {
const href = homeRoute + segments.slice(0, index + 1).join(homeRoute)
const label = capitalizeRoute(segment)
const isLast = index === segments.length - 1

return (
<BreadcrumbItem key={href} isDisabled={isLast}>
{isLast ? (
<span className="cursor-default font-semibold text-gray-600 dark:text-gray-300">
{label}
</span>
) : (
<Link
href={href}
className="hover:text-blue-700 hover:underline dark:text-blue-400"
>
{label}
</Link>
)}
</BreadcrumbItem>
)
})}
</Breadcrumbs>
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions frontend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,5 @@ export const themeToggleTooltip = {
}

export const desktopViewMinWidth = 768

export const homeRoute = '/'