diff --git a/frontend/__tests__/e2e/helpers/expects.ts b/frontend/__tests__/e2e/helpers/expects.ts
index 1251f251e6..9b339b6149 100644
--- a/frontend/__tests__/e2e/helpers/expects.ts
+++ b/frontend/__tests__/e2e/helpers/expects.ts
@@ -1,16 +1,16 @@
import { Page, expect } from '@playwright/test'
export async function expectBreadCrumbsToBeVisible(page: Page, breadcrumbs: string[] = ['Home']) {
- const breadcrumbsContainer = page.locator('[aria-label="breadcrumb"]')
+ const breadcrumbsContainer = page.locator('nav[role="navigation"][aria-label="breadcrumb"]')
- await expect(breadcrumbsContainer).toBeVisible()
+ await expect(breadcrumbsContainer).toBeVisible({ timeout: 10000 })
await expect(breadcrumbsContainer).toHaveCount(1)
- for (const breadcrumb of breadcrumbs) {
- await expect(breadcrumbsContainer.getByText(breadcrumb)).toBeVisible()
- }
+ const expectedBreadcrumbs = breadcrumbs[0] === 'Home' ? breadcrumbs : ['Home', ...breadcrumbs]
- const allBreadcrumbs = await breadcrumbsContainer.locator('li').allTextContents()
- const visibleBreadcrumbs = allBreadcrumbs.filter((text) => text.trim() !== '')
- await expect(visibleBreadcrumbs).toEqual(breadcrumbs)
+ for (const breadcrumb of expectedBreadcrumbs) {
+ await expect(breadcrumbsContainer.getByText(breadcrumb, { exact: true })).toBeVisible({
+ timeout: 5000,
+ })
+ }
}
diff --git a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
index 17511e18ad..9c578f6db5 100644
--- a/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
+++ b/frontend/__tests__/unit/components/BreadCrumbs.test.tsx
@@ -1,29 +1,24 @@
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(),
-}))
+const renderBreadCrumbs = (items = [], ariaLabel = 'breadcrumb') =>
+ render()
-describe('BreadCrumb', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- test('does not render on root path "/"', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/')
+const sampleItems = [
+ { title: 'Dashboard', path: '/dashboard' },
+ { title: 'Users', path: '/dashboard/users' },
+ { title: 'Profile', path: '/dashboard/users/profile' },
+]
- render()
+describe('BreadCrumbs', () => {
+ test('does not render when breadcrumb item is empty', () => {
+ renderBreadCrumbs()
expect(screen.queryByText('Home')).not.toBeInTheDocument()
})
test('renders breadcrumb with multiple segments', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
-
- render()
-
+ renderBreadCrumbs(sampleItems)
expect(screen.getByText('Home')).toBeInTheDocument()
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Users')).toBeInTheDocument()
@@ -31,38 +26,109 @@ describe('BreadCrumb', () => {
})
test('disables the last segment (non-clickable)', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/settings/account')
-
- render()
-
+ const items = [
+ { title: 'Settings', path: '/settings' },
+ { title: 'Account', path: '/settings/account' },
+ ]
+ renderBreadCrumbs(items)
const lastSegment = screen.getByText('Account')
expect(lastSegment).toBeInTheDocument()
- expect(lastSegment).not.toHaveAttribute('href')
+ expect(lastSegment.closest('a')).toBeNull()
+ })
+
+ test('links have correct path attributes', () => {
+ renderBreadCrumbs(sampleItems)
+ expect(screen.getByText('Home').closest('a')).toHaveAttribute('href', '/')
+ expect(screen.getByText('Dashboard').closest('a')).toHaveAttribute('href', '/dashboard')
+ expect(screen.getByText('Users').closest('a')).toHaveAttribute('href', '/dashboard/users')
+ })
+
+ test('links have hover styles', () => {
+ const items = [
+ { title: 'Dashboard', path: '/dashboard' },
+ { title: 'Users', path: '/dashboard/users' },
+ ]
+ renderBreadCrumbs(items)
+ expect(screen.getByText('Home').closest('a')).toHaveClass(
+ 'hover:text-blue-700',
+ 'hover:underline'
+ )
+ expect(screen.getByText('Dashboard').closest('a')).toHaveClass(
+ 'hover:text-blue-700',
+ 'hover:underline'
+ )
})
- test('links have correct href attributes', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users/profile')
+ test('has proper accessibility attributes', () => {
+ renderBreadCrumbs(sampleItems)
- render()
+ // Check nav element has proper role and aria-label
+ const nav = screen.getAllByRole('navigation')[0]
+ expect(nav).toHaveAttribute('aria-label', 'breadcrumb')
+ // Check last item has aria-current="page"
+ const lastItem = screen.getByText('Profile')
+ expect(lastItem).toHaveAttribute('aria-current', 'page')
+
+ // Check Home link has proper aria-label
const homeLink = screen.getByText('Home').closest('a')
- const dashboardLink = screen.getByText('Dashboard').closest('a')
- const usersLink = screen.getByText('Users').closest('a')
+ expect(homeLink).toHaveAttribute('aria-label', 'Go to home page')
+ })
- expect(homeLink).toHaveAttribute('href', '/')
- expect(dashboardLink).toHaveAttribute('href', '/dashboard')
- expect(usersLink).toHaveAttribute('href', '/dashboard/users')
+ test('supports custom aria-label', () => {
+ renderBreadCrumbs(sampleItems, 'custom breadcrumb')
+
+ const nav = screen.getAllByRole('navigation')[0]
+ expect(nav).toHaveAttribute('aria-label', 'custom breadcrumb')
})
- test('links have hover styles', () => {
- ;(usePathname as jest.Mock).mockReturnValue('/dashboard/users')
+ test('filters out invalid breadcrumb items', () => {
+ const invalidItems = [
+ { title: 'Valid Item', path: '/valid' },
+ { title: '', path: '/invalid' }, // Missing title
+ { title: 'Another Valid', path: '' }, // Missing path
+ { title: 'Valid Again', path: '/valid-again' },
+ ]
- render()
+ renderBreadCrumbs(invalidItems)
+
+ // Should only render valid items
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Valid Item')).toBeInTheDocument()
+ expect(screen.getByText('Valid Again')).toBeInTheDocument()
+ expect(screen.queryByText('Another Valid')).not.toBeInTheDocument()
+ })
+
+ test('has focus styles for keyboard navigation', () => {
+ renderBreadCrumbs(sampleItems)
const homeLink = screen.getByText('Home').closest('a')
const dashboardLink = screen.getByText('Dashboard').closest('a')
- expect(homeLink).toHaveClass('hover:text-blue-700', 'hover:underline')
- expect(dashboardLink).toHaveClass('hover:text-blue-700', 'hover:underline')
+ expect(homeLink).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500')
+ expect(dashboardLink).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500')
+ })
+
+ test('separator icon is hidden from screen readers', () => {
+ renderBreadCrumbs(sampleItems)
+
+ // Look for SVG elements with aria-hidden="true"
+ const separators = screen.getAllByRole('img', { hidden: true })
+ separators.forEach((separator) => {
+ expect(separator).toHaveAttribute('aria-hidden', 'true')
+ })
+ })
+
+ test('generates unique keys for breadcrumb items', () => {
+ const itemsWithSamePath = [
+ { title: 'First', path: '/same-path' },
+ { title: 'Second', path: '/same-path' },
+ ]
+
+ renderBreadCrumbs(itemsWithSamePath)
+
+ // Should render without React key warnings
+ expect(screen.getByText('First')).toBeInTheDocument()
+ expect(screen.getByText('Second')).toBeInTheDocument()
})
})
diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts
index 9d9c195022..380f83ea3f 100644
--- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts
+++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts
@@ -57,6 +57,7 @@ export const mockProjectDetailsData = {
openIssuesCount: 6,
organization: {
login: 'OWASP',
+ name: 'OWASP Foundation',
},
starsCount: 95,
subscribersCount: 15,
@@ -70,6 +71,7 @@ export const mockProjectDetailsData = {
openIssuesCount: 3,
organization: {
login: 'OWASP',
+ name: 'OWASP Foundation',
},
starsCount: 60,
subscribersCount: 10,
diff --git a/frontend/__tests__/unit/data/mockRepositoryData.ts b/frontend/__tests__/unit/data/mockRepositoryData.ts
index ca9f789a7b..72bce90e73 100644
--- a/frontend/__tests__/unit/data/mockRepositoryData.ts
+++ b/frontend/__tests__/unit/data/mockRepositoryData.ts
@@ -30,6 +30,10 @@ export const mockRepositoryData = {
key: 'test-project',
name: 'Test Project',
},
+ organization: {
+ login: 'test-org',
+ name: 'Test Organization',
+ },
releases: [
{
name: 'v1.0.0',
diff --git a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
index b4c89a164f..88095b6e3e 100644
--- a/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/CommitteeDetails.test.tsx
@@ -48,17 +48,6 @@ describe('CommitteeDetailsPage Component', () => {
})
})
- test('renders committee data correctly', async () => {
- render()
- await waitFor(() => {
- expect(screen.getByText('Test Committee')).toBeInTheDocument()
- })
- expect(screen.getByText('This is a test committee summary.')).toBeInTheDocument()
- expect(screen.getByText('Leader 1')).toBeInTheDocument()
- expect(screen.getByText('Leader 2')).toBeInTheDocument()
- expect(screen.getByText('https://owasp.org/test-committee')).toBeInTheDocument()
- })
-
test('displays "Committee not found" when there is no committee', async () => {
;(useQuery as jest.Mock).mockReturnValue({
data: null,
diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
index 99bf86d8d6..50cd720355 100644
--- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
@@ -70,7 +70,8 @@ describe('OrganizationDetailsPage', () => {
render()
await waitFor(() => {
- expect(screen.getByText('Test Organization')).toBeInTheDocument()
+ const title = screen.getByRole('heading', { name: 'Test Organization' })
+ expect(title).toBeInTheDocument()
})
expect(screen.getByText('@test-org')).toBeInTheDocument()
@@ -210,4 +211,29 @@ describe('OrganizationDetailsPage', () => {
expect(screen.queryByText(`Want to become a sponsor?`)).toBeNull()
})
})
+
+ test('renders breadcrumbs correctly', async () => {
+ ;(useQuery as jest.Mock).mockReturnValue({
+ data: mockOrganizationDetailsData,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Organizations')).toBeInTheDocument()
+
+ const breadcrumbOrgName = screen.getByText(mockOrganizationDetailsData.organization.name, {
+ selector: 'span',
+ })
+ expect(breadcrumbOrgName).toBeInTheDocument()
+ })
+
+ const homeLink = screen.getByText('Home').closest('a')
+ const organizationsLink = screen.getByText('Organizations').closest('a')
+
+ expect(homeLink).toHaveAttribute('href', '/')
+ expect(organizationsLink).toHaveAttribute('href', '/organizations')
+ })
})
diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx
index b23025fd37..3cbcae10b2 100644
--- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx
@@ -78,11 +78,8 @@ describe('ProjectDetailsPage', () => {
await waitFor(() => {
expect(screen.getByText('Test Project')).toBeInTheDocument()
- expect(screen.getByText('Lab')).toBeInTheDocument()
+ expect(screen.getByText('https://github.com/example-project')).toBeInTheDocument()
})
- expect(screen.getByText('2.2K Stars')).toBeInTheDocument()
- expect(screen.getByText('10 Forks')).toBeInTheDocument()
- expect(screen.getByText('10 Issues')).toBeInTheDocument()
})
test('renders error message when GraphQL request fails', async () => {
diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx
index 94445c750a..018e671c42 100644
--- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx
@@ -68,14 +68,14 @@ describe('RepositoryDetailsPage', () => {
render()
await waitFor(() => {
- expect(screen.getByText('Test Repo')).toBeInTheDocument()
+ expect(screen.getAllByText('Test Repo')).toHaveLength(2) // Title and breadcrumb
expect(screen.getByText('MIT')).toBeInTheDocument()
+ expect(screen.getByText('10 Commits')).toBeInTheDocument()
+ expect(screen.getByText('5 Contributors')).toBeInTheDocument()
+ expect(screen.getByText('3K Forks')).toBeInTheDocument()
+ expect(screen.getByText('2 Issues')).toBeInTheDocument()
+ expect(screen.getByText('50K Stars')).toBeInTheDocument()
})
- expect(screen.getByText('50K Stars')).toBeInTheDocument()
- expect(screen.getByText('3K Forks')).toBeInTheDocument()
- expect(screen.getByText('10 Commits')).toBeInTheDocument()
- expect(screen.getByText('5 Contributors')).toBeInTheDocument()
- expect(screen.getByText('2 Issues')).toBeInTheDocument()
})
test('renders error message when GraphQL request fails', async () => {
@@ -231,4 +231,81 @@ describe('RepositoryDetailsPage', () => {
).toBeInTheDocument()
})
})
+
+ test('renders breadcrumbs correctly with organization name', async () => {
+ render()
+
+ await waitFor(() => {
+ // Check that breadcrumbs are rendered with organization name
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Organizations')).toBeInTheDocument()
+ expect(screen.getByText('Repositories')).toBeInTheDocument()
+ const breadcrumbOrgName = screen.getByText(mockRepositoryData.repository.organization.name, {
+ selector: 'a',
+ })
+ expect(breadcrumbOrgName).toBeInTheDocument()
+
+ const breadcrumbRepoName = screen.getByText(mockRepositoryData.repository.name, {
+ selector: 'span',
+ })
+ expect(breadcrumbRepoName).toBeInTheDocument()
+ })
+
+ // Check breadcrumb links
+ const homeLink = screen.getByText('Home').closest('a')
+ const organizationsLink = screen.getByText('Organizations').closest('a')
+ const organizationNameLink = screen.getByText(mockRepositoryData.repository.organization.name, {
+ selector: 'a',
+ })
+ const repositoriesLink = screen.getByText('Repositories').closest('a')
+
+ expect(homeLink).toHaveAttribute('href', '/')
+ expect(organizationsLink).toHaveAttribute('href', '/organizations')
+ expect(organizationNameLink).toHaveAttribute(
+ 'href',
+ `/organizations/${mockRepositoryData.repository.organization.login}`
+ )
+ expect(repositoriesLink).toHaveAttribute(
+ 'href',
+ `/organizations/${mockRepositoryData.repository.organization.login}#repositories`
+ )
+ })
+
+ test('renders breadcrumbs with fallback to organization login when name is not available', async () => {
+ const repositoryDataWithoutOrgName = {
+ ...mockRepositoryData,
+ repository: {
+ ...mockRepositoryData.repository,
+ organization: {
+ ...mockRepositoryData.repository.organization,
+ name: null,
+ },
+ },
+ }
+
+ ;(useQuery as jest.Mock).mockReturnValue({
+ data: repositoryDataWithoutOrgName,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ // Check that breadcrumbs fall back to organization login
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Organizations')).toBeInTheDocument()
+ expect(screen.getByText('Repositories')).toBeInTheDocument()
+
+ const breadcrumbOrgLogin = screen.getByText(
+ mockRepositoryData.repository.organization.login,
+ { selector: 'a' }
+ )
+ expect(breadcrumbOrgLogin).toBeInTheDocument()
+
+ const breadcrumbRepoName = screen.getByText(mockRepositoryData.repository.name, {
+ selector: 'span',
+ })
+ expect(breadcrumbRepoName).toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
index e5256e1468..556c5f82f7 100644
--- a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
@@ -1,6 +1,6 @@
import { useQuery } from '@apollo/client'
import { addToast } from '@heroui/toast'
-import { fireEvent, screen, waitFor } from '@testing-library/react'
+import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { mockSnapshotDetailsData } from '@unit/data/mockSnapshotData'
import { render } from 'wrappers/testUtil'
import SnapshotDetailsPage from 'app/snapshots/[id]/page'
@@ -54,7 +54,9 @@ describe('SnapshotDetailsPage', () => {
error: null,
})
- render()
+ await act(async () => {
+ render()
+ })
const loadingSpinner = screen.getAllByAltText('Loading indicator')
await waitFor(() => {
@@ -68,10 +70,8 @@ describe('SnapshotDetailsPage', () => {
error: null,
})
- render()
-
- await waitFor(() => {
- expect(screen.getByText('New Snapshot')).toBeInTheDocument()
+ await act(async () => {
+ render()
})
expect(screen.getByText('New Chapters')).toBeInTheDocument()
@@ -85,10 +85,14 @@ describe('SnapshotDetailsPage', () => {
error: mockError,
})
- render()
+ await act(async () => {
+ render()
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('Snapshot not found')).toBeInTheDocument()
+ })
- await waitFor(() => screen.getByText('Snapshot not found'))
- expect(screen.getByText('Snapshot not found')).toBeInTheDocument()
expect(addToast).toHaveBeenCalledWith({
description: 'An unexpected server error occurred.',
title: 'Server Error',
@@ -104,7 +108,9 @@ describe('SnapshotDetailsPage', () => {
data: mockSnapshotDetailsData,
})
- render()
+ await act(async () => {
+ render()
+ })
await waitFor(() => {
expect(screen.getByText('OWASP Nest')).toBeInTheDocument()
@@ -123,7 +129,9 @@ describe('SnapshotDetailsPage', () => {
data: mockSnapshotDetailsData,
})
- render()
+ await act(async () => {
+ render()
+ })
await waitFor(() => {
expect(screen.getByText('OWASP Sivagangai')).toBeInTheDocument()
@@ -142,10 +150,13 @@ describe('SnapshotDetailsPage', () => {
data: mockSnapshotDetailsData,
})
- render()
+ await act(async () => {
+ render()
+ })
await waitFor(() => {
- expect(screen.getByText('New Snapshot')).toBeInTheDocument()
+ const title = screen.getByRole('heading', { name: 'New Snapshot' })
+ expect(title).toBeInTheDocument()
expect(screen.getByText('Latest pre-release')).toBeInTheDocument()
})
@@ -166,7 +177,9 @@ describe('SnapshotDetailsPage', () => {
error: null,
})
- render()
+ await act(async () => {
+ render()
+ })
await waitFor(() => {
expect(screen.queryByText('New Chapters')).not.toBeInTheDocument()
diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx
index 75bdf32930..814784ffdf 100644
--- a/frontend/__tests__/unit/pages/UserDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx
@@ -88,7 +88,8 @@ describe('UserDetailsPage', () => {
expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument()
})
- expect(screen.getByText('Test User')).toBeInTheDocument()
+ const title = screen.getByRole('heading', { name: 'Test User' })
+ expect(title).toBeInTheDocument()
expect(screen.getByText('Statistics')).toBeInTheDocument()
expect(screen.getByText('Contribution Heatmap')).toBeInTheDocument()
expect(screen.getByText('Test Company')).toBeInTheDocument()
@@ -267,7 +268,7 @@ describe('UserDetailsPage', () => {
render()
await waitFor(() => {
- const userName = screen.getByText('Test User')
+ const userName = screen.getByRole('heading', { name: 'Test User' })
expect(userName).toBeInTheDocument()
})
})
@@ -327,7 +328,8 @@ describe('UserDetailsPage', () => {
render()
await waitFor(() => {
- expect(screen.getByText('Test User')).toBeInTheDocument()
+ const userName = screen.getByRole('heading', { name: 'Test User' })
+ expect(userName).toBeInTheDocument()
expect(screen.queryByText('Test @User')).not.toBeInTheDocument()
})
})
diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx
index d8d7f46a5a..d57ef7ac00 100644
--- a/frontend/src/app/about/page.tsx
+++ b/frontend/src/app/about/page.tsx
@@ -29,6 +29,7 @@ import AnchorTitle from 'components/AnchorTitle'
import AnimatedCounter from 'components/AnimatedCounter'
import LoadingSpinner from 'components/LoadingSpinner'
import Markdown from 'components/MarkdownWrapper'
+import PageLayout from 'components/PageLayout'
import SecondaryCard from 'components/SecondaryCard'
import TopContributorsList from 'components/TopContributorsList'
import UserCard from 'components/UserCard'
@@ -99,145 +100,147 @@ const About = () => {
}
return (
-
-
-
About
-
}>
- {aboutText.map((text) => (
-
- ))}
-
-
-
}>
-
- {Object.keys(leaders).map((username) => (
-
-
+
+
+
+
About
+
}>
+ {aboutText.map((text) => (
+
))}
-
-
-
- {topContributors && (
-
- )}
+
-
}>
-
-
- {technologies.map((tech) => (
-
-
{tech.section}
-
- {Object.entries(tech.tools).map(([name, details]) => (
- -
-
-
- {capitalize(name)}
-
-
- ))}
-
+
}>
+
+ {Object.keys(leaders).map((username) => (
+
+
))}
-
-
+
- {projectMetadata.recentMilestones.length > 0 && (
-
}>
-
- {[...projectMetadata.recentMilestones]
- .filter((milestone) => milestone.state !== 'closed')
- .sort((a, b) => (a.title > b.title ? 1 : -1))
- .map((milestone, index) => (
-
-
-
-
- {milestone.title}
- 0
- ? 'In Progress'
- : 'Not Started'
- }
- id={`tooltip-state-${index}`}
- delay={100}
- placement="top"
- showArrow
+ {topContributors && (
+
+ )}
+
+ }>
+
+
+ {technologies.map((tech) => (
+
+
{tech.section}
+
+ {Object.entries(tech.tools).map(([name, details]) => (
+ -
+
+
-
- 0
- ? faUserGear
- : faClock
- }
- />
-
-
-
-
-
{milestone.body}
-
+ {capitalize(name)}
+
+
+ ))}
+
))}
+
- )}
-
- {[
- { label: 'Forks', value: projectMetadata.forksCount },
- { label: 'Stars', value: projectMetadata.starsCount },
- { label: 'Contributors', value: projectMetadata.contributorsCount },
- { label: 'Open Issues', value: projectMetadata.issuesCount },
- ].map((stat, index) => (
-
- ))}
+ {projectMetadata.recentMilestones.length > 0 && (
+
}>
+
+ {[...projectMetadata.recentMilestones]
+ .filter((milestone) => milestone.state !== 'closed')
+ .sort((a, b) => (a.title > b.title ? 1 : -1))
+ .map((milestone, index) => (
+
+
+
+
+ {milestone.title}
+ 0
+ ? 'In Progress'
+ : 'Not Started'
+ }
+ id={`tooltip-state-${index}`}
+ delay={100}
+ placement="top"
+ showArrow
+ >
+
+ 0
+ ? faUserGear
+ : faClock
+ }
+ />
+
+
+
+
+
{milestone.body}
+
+
+ ))}
+
+
+ )}
+
+
+ {[
+ { label: 'Forks', value: projectMetadata.forksCount },
+ { label: 'Stars', value: projectMetadata.starsCount },
+ { label: 'Contributors', value: projectMetadata.contributorsCount },
+ { label: 'Open Issues', value: projectMetadata.issuesCount },
+ ].map((stat, index) => (
+
+ ))}
+
-
+
)
}
diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx
index d6a7f1a16c..eefe5696bb 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()
@@ -60,16 +61,18 @@ export default function ChapterDetailsPage() {
},
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/chapters/page.tsx b/frontend/src/app/chapters/page.tsx
index 15d93df6ea..67f1e0e9f7 100644
--- a/frontend/src/app/chapters/page.tsx
+++ b/frontend/src/app/chapters/page.tsx
@@ -9,6 +9,7 @@ import type { Chapter } from 'types/chapter'
import { getFilteredIcons, handleSocialUrls } from 'utils/utility'
import Card from 'components/Card'
import ChapterMapWrapper from 'components/ChapterMapWrapper'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
const ChaptersPage = () => {
@@ -76,32 +77,34 @@ const ChaptersPage = () => {
}
return (
-
- {chapters.length > 0 && (
-
- )}
- {chapters && chapters.filter((chapter) => chapter.isActive).map(renderChapterCard)}
-
+
+
+ {chapters.length > 0 && (
+
+ )}
+ {chapters && chapters.filter((chapter) => chapter.isActive).map(renderChapterCard)}
+
+
)
}
diff --git a/frontend/src/app/committees/[committeeKey]/page.tsx b/frontend/src/app/committees/[committeeKey]/page.tsx
index 2450fcde97..f4bcd38872 100644
--- a/frontend/src/app/committees/[committeeKey]/page.tsx
+++ b/frontend/src/app/committees/[committeeKey]/page.tsx
@@ -17,7 +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 }>()
const [committee, setCommittee] = useState
(null)
@@ -80,14 +80,16 @@ export default function CommitteeDetailsPage() {
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/committees/page.tsx b/frontend/src/app/committees/page.tsx
index e6d59dc054..bb3b9866a2 100644
--- a/frontend/src/app/committees/page.tsx
+++ b/frontend/src/app/committees/page.tsx
@@ -5,6 +5,7 @@ import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import type { Committee } from 'types/committee'
import { getFilteredIcons, handleSocialUrls } from 'utils/utility'
import Card from 'components/Card'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
const CommitteesPage = () => {
@@ -51,19 +52,21 @@ const CommitteesPage = () => {
}
return (
-
- {committees && committees.map(renderCommitteeCard)}
-
+
+
+ {committees && committees.map(renderCommitteeCard)}
+
+
)
}
diff --git a/frontend/src/app/contribute/page.tsx b/frontend/src/app/contribute/page.tsx
index 5d07bf3a52..c9d28da4fb 100644
--- a/frontend/src/app/contribute/page.tsx
+++ b/frontend/src/app/contribute/page.tsx
@@ -9,6 +9,7 @@ import { getFilteredIcons } from 'utils/utility'
import Card from 'components/Card'
import DialogComp from 'components/Modal'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
const ContributePage = () => {
@@ -70,19 +71,21 @@ const ContributePage = () => {
}
return (
-
- {issues && issues.map(renderContributeCard)}
-
+
+
+ {issues && issues.map(renderContributeCard)}
+
+
)
}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 1aa89e7daa..7505d52f73 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -5,7 +5,6 @@ import React from 'react'
import { Providers } from 'wrappers/provider'
import { GTM_ID, IS_GITHUB_AUTH_ENABLED } from 'utils/credentials'
import AutoScrollToTop from 'components/AutoScrollToTop'
-import BreadCrumbs from 'components/BreadCrumbs'
import Footer from 'components/Footer'
import Header from 'components/Header'
import ScrollToTop from 'components/ScrollToTop'
@@ -70,7 +69,6 @@ export default function RootLayout({
-
{children}
diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx
index b97a1e773f..88cc48dc81 100644
--- a/frontend/src/app/members/[memberKey]/page.tsx
+++ b/frontend/src/app/members/[memberKey]/page.tsx
@@ -22,6 +22,7 @@ import { formatDate } from 'utils/dateFormatter'
import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const UserDetailsPage: React.FC = () => {
const { memberKey } = useParams()
@@ -190,20 +191,22 @@ const UserDetailsPage: React.FC = () => {
)
return (
- }
- pullRequests={pullRequests}
- recentIssues={issues}
- recentMilestones={milestones}
- recentReleases={releases}
- repositories={topRepositories}
- showAvatar={false}
- stats={userStats}
- title={user?.name || user?.login}
- type="user"
- userSummary={}
- />
+
+ }
+ pullRequests={pullRequests}
+ recentIssues={issues}
+ recentMilestones={milestones}
+ recentReleases={releases}
+ repositories={topRepositories}
+ showAvatar={false}
+ stats={userStats}
+ title={user?.name || user?.login}
+ type="user"
+ userSummary={}
+ />
+
)
}
diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx
index 955210d6d6..c98347118b 100644
--- a/frontend/src/app/members/page.tsx
+++ b/frontend/src/app/members/page.tsx
@@ -3,6 +3,7 @@ import { useSearchPage } from 'hooks/useSearchPage'
import { useRouter } from 'next/navigation'
import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import type { User } from 'types/user'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
import UserCard from 'components/UserCard'
@@ -51,21 +52,23 @@ const UsersPage = () => {
}
return (
-
-
- {users && users.map((user) =>
{renderUserCard(user)}
)}
-
-
+
+
+
+ {users && users.map((user) =>
{renderUserCard(user)}
)}
+
+
+
)
}
diff --git a/frontend/src/app/organizations/[organizationKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/page.tsx
index 614585ca1d..78375bc4dc 100644
--- a/frontend/src/app/organizations/[organizationKey]/page.tsx
+++ b/frontend/src/app/organizations/[organizationKey]/page.tsx
@@ -15,6 +15,7 @@ import { GET_ORGANIZATION_DATA } from 'server/queries/organizationQueries'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const OrganizationDetailsPage = () => {
const { organizationKey } = useParams()
const [organization, setOrganization] = useState(null)
@@ -112,20 +113,34 @@ const OrganizationDetailsPage = () => {
},
]
+ // Create custom breadcrumbs with organization name
+ const customBreadcrumbs = [
+ {
+ title: 'Organizations',
+ path: '/organizations',
+ },
+ {
+ title: organization.name,
+ path: `/organizations/${organization.login}`,
+ },
+ ]
+
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx
index 05aa7329d4..b1c1a96fd7 100644
--- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx
+++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/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'
const RepositoryDetailsPage = () => {
const { repositoryKey, organizationKey } = useParams()
@@ -105,23 +106,46 @@ const RepositoryDetailsPage = () => {
unit: 'Commit',
},
]
+
+ // Create custom breadcrumbs with organization name
+ const customBreadcrumbs = [
+ {
+ title: 'Organizations',
+ path: '/organizations',
+ },
+ {
+ title: repository.organization?.name || repository.organization?.login || 'Organization',
+ path: `/organizations/${repository.organization?.login || organizationKey}`,
+ },
+ {
+ title: 'Repositories',
+ path: `/organizations/${repository.organization?.login || organizationKey}#repositories`,
+ },
+ {
+ title: repository.name,
+ path: `/organizations/${repository.organization?.login || organizationKey}/repositories/${repositoryKey}`,
+ },
+ ]
+
return (
-
+
+
+
)
}
export default RepositoryDetailsPage
diff --git a/frontend/src/app/organizations/page.tsx b/frontend/src/app/organizations/page.tsx
index 754d517045..13b089f1f8 100644
--- a/frontend/src/app/organizations/page.tsx
+++ b/frontend/src/app/organizations/page.tsx
@@ -3,6 +3,7 @@ import { useSearchPage } from 'hooks/useSearchPage'
import { useRouter } from 'next/navigation'
import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import type { Organization } from 'types/organization'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
import UserCard from 'components/UserCard'
@@ -51,21 +52,23 @@ const OrganizationPage = () => {
}
return (
-
-
- {organizations && organizations.map(renderOrganizationCard)}
-
-
+
+
+
+ {organizations && organizations.map(renderOrganizationCard)}
+
+
+
)
}
diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx
index a563a1daca..0a5be14519 100644
--- a/frontend/src/app/projects/[projectKey]/page.tsx
+++ b/frontend/src/app/projects/[projectKey]/page.tsx
@@ -18,6 +18,7 @@ import { capitalize } from 'utils/capitalize'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const ProjectDetailsPage = () => {
const { projectKey } = useParams()
const [isLoading, setIsLoading] = useState(true)
@@ -89,24 +90,26 @@ const ProjectDetailsPage = () => {
]
return (
-
+
+
+
)
}
diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx
index 74081137da..0d47425079 100644
--- a/frontend/src/app/projects/page.tsx
+++ b/frontend/src/app/projects/page.tsx
@@ -7,6 +7,7 @@ import { level } from 'utils/data'
import { sortOptionsProject } from 'utils/sortingOptions'
import { getFilteredIcons } from 'utils/utility'
import Card from 'components/Card'
+import PageLayout from 'components/PageLayout'
import SearchPageLayout from 'components/SearchPageLayout'
import SortBy from 'components/SortBy'
const ProjectsPage = () => {
@@ -58,28 +59,30 @@ const ProjectsPage = () => {
}
return (
-
- }
- totalPages={totalPages}
- >
- {projects && projects.filter((project) => project.isActive).map(renderProjectCard)}
-
+
+
+ }
+ totalPages={totalPages}
+ >
+ {projects && projects.filter((project) => project.isActive).map(renderProjectCard)}
+
+
)
}
diff --git a/frontend/src/app/snapshots/[id]/page.tsx b/frontend/src/app/snapshots/[id]/page.tsx
index 02bfe38e45..820f739c85 100644
--- a/frontend/src/app/snapshots/[id]/page.tsx
+++ b/frontend/src/app/snapshots/[id]/page.tsx
@@ -16,6 +16,7 @@ import { getFilteredIcons, handleSocialUrls } from 'utils/utility'
import Card from 'components/Card'
import ChapterMapWrapper from 'components/ChapterMapWrapper'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
const SnapshotDetailsPage: React.FC = () => {
const { id: snapshotKey } = useParams()
@@ -109,88 +110,90 @@ const SnapshotDetailsPage: React.FC = () => {
}
return (
-
-
-
-
-
- {snapshot.title}
-
-
-
-
-
- {formatDate(snapshot.startAt)} - {formatDate(snapshot.endAt)}
-
+
+
+
+
+
+
+ {snapshot.title}
+
+
+
+
+
+ {formatDate(snapshot.startAt)} - {formatDate(snapshot.endAt)}
+
+
-
- {snapshot.newChapters && snapshot.newChapters.length > 0 && (
-
-
- New Chapters
-
-
-
-
-
- {snapshot.newChapters.filter((chapter) => chapter.isActive).map(renderChapterCard)}
+ {snapshot.newChapters && snapshot.newChapters.length > 0 && (
+
+
+ New Chapters
+
+
+
+
+
+ {snapshot.newChapters.filter((chapter) => chapter.isActive).map(renderChapterCard)}
+
-
- )}
-
- {snapshot.newProjects && snapshot.newProjects.length > 0 && (
-
-
- New Projects
-
-
- {snapshot.newProjects.filter((project) => project.isActive).map(renderProjectCard)}
+ )}
+
+ {snapshot.newProjects && snapshot.newProjects.length > 0 && (
+
+
+ New Projects
+
+
+ {snapshot.newProjects.filter((project) => project.isActive).map(renderProjectCard)}
+
-
- )}
-
- {snapshot.newReleases && snapshot.newReleases.length > 0 && (
-
-
New Releases
-
- {snapshot.newReleases.map((release, index) => (
-
-
-
-
- {release.name}
+ )}
+
+ {snapshot.newReleases && snapshot.newReleases.length > 0 && (
+
+
New Releases
+
+ {snapshot.newReleases.map((release, index) => (
+
+
+
+
+
+ {release.projectName}
+
+
+ {release.tagName}
+
+
+
+
+ Released: {formatDate(release.publishedAt)}
-
-
-
- {release.projectName}
-
-
- {release.tagName}
-
-
-
-
- Released: {formatDate(release.publishedAt)}
-
- ))}
+ ))}
+
-
- )}
-
+ )}
+
+
)
}
diff --git a/frontend/src/app/snapshots/page.tsx b/frontend/src/app/snapshots/page.tsx
index d9d4bfc548..efd009ae95 100644
--- a/frontend/src/app/snapshots/page.tsx
+++ b/frontend/src/app/snapshots/page.tsx
@@ -7,6 +7,7 @@ import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import { GET_COMMUNITY_SNAPSHOTS } from 'server/queries/snapshotQueries'
import type { Snapshot } from 'types/snapshot'
import LoadingSpinner from 'components/LoadingSpinner'
+import PageLayout from 'components/PageLayout'
import SnapshotCard from 'components/SnapshotCard'
const SnapshotsPage: React.FC = () => {
@@ -62,19 +63,21 @@ const SnapshotsPage: React.FC = () => {
}
return (
-
-
-
- {!snapshots?.length ? (
-
No Snapshots found
- ) : (
- snapshots.map((snapshot: Snapshot) => (
-
{renderSnapshotCard(snapshot)}
- ))
- )}
+
+
+
+
+ {!snapshots?.length ? (
+
No Snapshots found
+ ) : (
+ snapshots.map((snapshot: Snapshot) => (
+
{renderSnapshotCard(snapshot)}
+ ))
+ )}
+
-
+
)
}
diff --git a/frontend/src/components/BreadCrumbs.tsx b/frontend/src/components/BreadCrumbs.tsx
index a9bce0fda1..ca60564f3f 100644
--- a/frontend/src/components/BreadCrumbs.tsx
+++ b/frontend/src/components/BreadCrumbs.tsx
@@ -3,26 +3,49 @@
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Breadcrumbs, BreadcrumbItem } from '@heroui/react'
+import _ from 'lodash'
import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-import { capitalize } from 'utils/capitalize'
+import { memo } from 'react'
-export default function BreadCrumbs() {
- const homeRoute = '/'
- const pathname = usePathname()
- const segments = pathname.split(homeRoute).filter(Boolean)
+export interface BreadCrumbItem {
+ title: string
+ path: string
+}
+
+export interface BreadCrumbsProps {
+ breadcrumbItems: BreadCrumbItem[]
+ 'aria-label'?: string
+}
+
+function validateBreadcrumbItems(items: BreadCrumbItem[]): BreadCrumbItem[] {
+ return items.filter((item) => {
+ if (!item.title || !item.path) {
+ return false
+ }
+ return true
+ })
+}
- if (pathname === homeRoute) return null
+const BreadCrumbs = memo(function BreadCrumbs({
+ breadcrumbItems,
+ 'aria-label': ariaLabel = 'breadcrumb',
+}: BreadCrumbsProps) {
+ // Validate and filter breadcrumb items
+ const validBreadcrumbItems = validateBreadcrumbItems(breadcrumbItems)
+
+ // Don't render if no valid breadcrumb items
+ if (_.isEmpty(validBreadcrumbItems)) return null
return (
-
+
+
)
-}
+})
+
+export default BreadCrumbs
diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx
index 4eb1e9f18f..7c041a8339 100644
--- a/frontend/src/components/CardDetailsPage.tsx
+++ b/frontend/src/components/CardDetailsPage.tsx
@@ -56,7 +56,7 @@ const DetailsCard = ({
userSummary,
}: DetailsCardProps) => {
return (
-
+
diff --git a/frontend/src/components/PageLayout.tsx b/frontend/src/components/PageLayout.tsx
new file mode 100644
index 0000000000..41030f7caf
--- /dev/null
+++ b/frontend/src/components/PageLayout.tsx
@@ -0,0 +1,62 @@
+import _ from 'lodash'
+import { usePathname } from 'next/navigation'
+import React from 'react'
+import { capitalize } from 'utils/capitalize'
+import BreadCrumbs, { BreadCrumbItem } from 'components/BreadCrumbs'
+
+export interface crumbItem {
+ title: string
+}
+
+export interface customBreadcrumbItem {
+ title: string
+ path: string
+}
+
+export interface PageLayoutProps {
+ breadcrumbItems?: crumbItem
+ customBreadcrumbs?: customBreadcrumbItem[]
+ children: React.ReactNode
+}
+
+function generateBreadcrumbs(pathname: string, excludeLast = false): BreadCrumbItem[] {
+ let segments = _.compact(_.split(pathname, '/'))
+ if (excludeLast) {
+ segments = _.dropRight(segments)
+ }
+
+ return _.map(segments, (segment, index) => {
+ const path = '/' + _.join(_.slice(segments, 0, index + 1), '/')
+ return {
+ title: capitalize(segment),
+ path,
+ }
+ })
+}
+
+export default function PageLayout({
+ breadcrumbItems,
+ customBreadcrumbs,
+ children,
+}: PageLayoutProps) {
+ const pathname = usePathname()
+
+ let allBreadcrumbs: BreadCrumbItem[]
+
+ if (customBreadcrumbs && customBreadcrumbs.length > 0) {
+ allBreadcrumbs = customBreadcrumbs
+ } else {
+ const isBreadCrumbItemsEmpty = _.isEmpty(breadcrumbItems)
+ const autoBreadcrumbs = generateBreadcrumbs(pathname, !isBreadCrumbItemsEmpty)
+ allBreadcrumbs = isBreadCrumbItemsEmpty
+ ? autoBreadcrumbs
+ : [...autoBreadcrumbs, { title: _.get(breadcrumbItems, 'title', ''), path: pathname }]
+ }
+
+ return (
+ <>
+
+ {children}
+ >
+ )
+}
diff --git a/frontend/src/server/queries/organizationQueries.ts b/frontend/src/server/queries/organizationQueries.ts
index 7471732564..6fd587847a 100644
--- a/frontend/src/server/queries/organizationQueries.ts
+++ b/frontend/src/server/queries/organizationQueries.ts
@@ -75,6 +75,7 @@ export const GET_ORGANIZATION_DATA = gql`
openIssuesCount
organization {
login
+ name
}
starsCount
url
diff --git a/frontend/src/server/queries/repositoryQueries.ts b/frontend/src/server/queries/repositoryQueries.ts
index da1be7b3ef..0fec865654 100644
--- a/frontend/src/server/queries/repositoryQueries.ts
+++ b/frontend/src/server/queries/repositoryQueries.ts
@@ -26,6 +26,7 @@ export const GET_REPOSITORY_DATA = gql`
openIssuesCount
organization {
login
+ name
}
project {
key
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 56e45285a8..0c9b493bb1 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -24,6 +24,7 @@
"paths": {
"@e2e/*": ["__tests__/e2e/*"],
"@unit/*": ["__tests__/unit/*"],
+ "@testUtils/*": ["__tests__/testUtils/*"],
"*": ["./src/*"],
"app/*": ["src/app/*"],
"components/*": ["src/components/*"],