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

export async function expectBreadCrumbsToBeVisible(page: Page, breadcrumbs: string[] = ['Home']) {
const breadcrumbsContainer = page.locator('[aria-label="breadcrumb"]')

await expect(breadcrumbsContainer).toBeVisible()
await expect(breadcrumbsContainer).toHaveCount(1)

for (const breadcrumb of breadcrumbs) {
await expect(breadcrumbsContainer.getByText(breadcrumb)).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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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 { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects'
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 expectBreadCrumbsToBeVisible(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
70 changes: 70 additions & 0 deletions frontend/src/components/BreadCrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'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'

export default function BreadCrumbs() {
const homeRoute = '/'
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={homeRoute}
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 = capitalize(segment).replace(/-/g, ' ')
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>
)
}