diff --git a/frontend/__tests__/e2e/pages/About.spec.ts b/frontend/__tests__/e2e/pages/About.spec.ts
index 9b968b4dd5..56dd64ddfc 100644
--- a/frontend/__tests__/e2e/pages/About.spec.ts
+++ b/frontend/__tests__/e2e/pages/About.spec.ts
@@ -71,8 +71,19 @@ test.describe('About Page', () => {
test('loads roadmap items correctly', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Roadmap' })).toBeVisible()
+
+ await expect(page.getByText('Contribution Hub title')).toBeVisible()
+
+ const roadmapSection = page
+ .locator('div')
+ .filter({ has: page.getByRole('heading', { name: 'Roadmap' }) })
+ .filter({ has: page.getByRole('button', { name: 'Show more' }) })
+ .last()
+
+ await roadmapSection.getByRole('button', { name: 'Show more' }).click()
+
for (const milestone of mockAboutData.project.recentMilestones) {
- await expect(page.getByText(milestone.title)).toBeVisible()
+ await expect(page.getByText(milestone.title, { exact: true })).toBeVisible()
await expect(page.getByText(milestone.body)).toBeVisible()
}
})
@@ -99,7 +110,18 @@ test.describe('About Page', () => {
})
test('renders project history timeline section', async ({ page }) => {
- await expect(page.getByText('Project Timeline')).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Project Timeline' })).toBeVisible()
+
+ await expect(page.getByText('OWASP Nest Logo Introduction')).toBeVisible()
+
+ const timelineSection = page
+ .locator('div')
+ .filter({ has: page.getByRole('heading', { name: 'Project Timeline' }) })
+ .filter({ has: page.getByRole('button', { name: 'Show more' }) })
+ .last()
+
+ await timelineSection.getByRole('button', { name: 'Show more' }).click()
+
await expect(page.getByText('Project Inception')).toBeVisible()
})
})
diff --git a/frontend/__tests__/mockData/mockAboutData.ts b/frontend/__tests__/mockData/mockAboutData.ts
index a2056cdce5..47c329613d 100644
--- a/frontend/__tests__/mockData/mockAboutData.ts
+++ b/frontend/__tests__/mockData/mockAboutData.ts
@@ -23,6 +23,18 @@ export const mockAboutData = {
url: 'http/github.com/milestones/10',
progress: 80,
},
+ {
+ title: 'Milestone 4',
+ body: 'Milestone 4 Idea',
+ url: 'http/github.com/milestones/11',
+ progress: 20,
+ },
+ {
+ title: 'Milestone 5',
+ body: 'Milestone 5 Idea',
+ url: 'http/github.com/milestones/12',
+ progress: 40,
+ },
],
},
topContributors: Array.from({ length: 15 }, (_, i) => ({
diff --git a/frontend/__tests__/unit/pages/About.test.tsx b/frontend/__tests__/unit/pages/About.test.tsx
index 0d9cee5263..9af466b734 100644
--- a/frontend/__tests__/unit/pages/About.test.tsx
+++ b/frontend/__tests__/unit/pages/About.test.tsx
@@ -70,6 +70,11 @@ jest.mock('utils/aboutData', () => ({
projectTimeline: [
{ title: 'Timeline Event 1', description: 'Timeline description 1', year: '2023' },
{ title: 'Timeline Event 2', description: 'Timeline description 2', year: '2024' },
+ { title: 'Timeline Event 3', description: 'Timeline description 3', year: '2025' },
+ { title: 'Timeline Event 4', description: 'Timeline description 4', year: '2026' },
+ { title: 'Timeline Event 5', description: 'Timeline description 5', year: '2027' },
+ { title: 'Timeline Event 6', description: 'Timeline description 6', year: '2028' },
+ { title: 'Timeline Event 7', description: 'Timeline description 7', year: '2029' },
],
technologies: [
{
@@ -275,15 +280,33 @@ describe('About Component', () => {
render()
})
- const projectHistorySection = screen.getByText('Our Story').closest('div')
- expect(projectHistorySection).toBeInTheDocument()
-
- expect(screen.getByText('Timeline Event 1')).toBeInTheDocument()
+ expect(screen.getByText('Project Timeline')).toBeInTheDocument()
+ expect(screen.getByText('Timeline Event 7')).toBeInTheDocument()
expect(screen.getByText('Timeline Event 2')).toBeInTheDocument()
- expect(screen.getByText('Timeline description 1')).toBeInTheDocument()
- expect(screen.getByText('Timeline description 2')).toBeInTheDocument()
- expect(screen.getByText('2023')).toBeInTheDocument()
+ expect(screen.queryByText('Timeline Event 1')).not.toBeInTheDocument()
+ expect(screen.getByText('2029')).toBeInTheDocument()
+ expect(screen.queryByText('2023')).not.toBeInTheDocument()
expect(screen.getByText('2024')).toBeInTheDocument()
+
+ expect(screen.getByText('Timeline Event 7')).toBeInTheDocument()
+ expect(screen.queryByText('Timeline Event 1')).not.toBeInTheDocument()
+
+ const timelineSection = screen.getByText('Project Timeline').closest('h2')?.parentElement
+ if (!timelineSection) throw new Error('Could not find Timeline section')
+
+ const showMoreButton = within(timelineSection).getByRole('button', { name: /Show more/i })
+ fireEvent.click(showMoreButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Timeline Event 1')).toBeInTheDocument()
+ })
+
+ const showLessButton = within(timelineSection).getByRole('button', { name: /Show less/i })
+ fireEvent.click(showLessButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Timeline Event 1')).not.toBeInTheDocument()
+ })
})
test('renders leaders section with three leaders', async () => {
@@ -327,7 +350,10 @@ describe('About Component', () => {
expect(screen.queryByText('Contributor 13')).not.toBeInTheDocument()
})
- const showMoreButton = screen.getByRole('button', { name: /Show more/i })
+ const wallOfFameSection = screen.getByText('Wall of Fame').closest('h2')?.parentElement
+ if (!wallOfFameSection) throw new Error('Could not find Wall of Fame section')
+
+ const showMoreButton = within(wallOfFameSection).getByRole('button', { name: /Show more/i })
fireEvent.click(showMoreButton)
await waitFor(() => {
@@ -336,7 +362,7 @@ describe('About Component', () => {
expect(screen.getByText('Contributor 15')).toBeInTheDocument()
})
- const showLessButton = screen.getByRole('button', { name: /Show less/i })
+ const showLessButton = within(wallOfFameSection).getByRole('button', { name: /Show less/i })
fireEvent.click(showLessButton)
await waitFor(() => {
@@ -387,17 +413,28 @@ describe('About Component', () => {
const roadmapSection = screen.getByRole('heading', { name: /Roadmap/ }).closest('div')
expect(roadmapSection).toBeInTheDocument()
- const roadmapData = mockAboutData.project.recentMilestones
- const links = within(roadmapSection)
- .getAllByRole('link')
- .filter((link) => link.getAttribute('href') !== '#roadmap')
-
- for (let i = 0; i < roadmapData.length; i++) {
- const milestone = [...roadmapData].sort((a, b) => (a.title > b.title ? 1 : -1))[i]
- expect(screen.getByText(milestone.title)).toBeInTheDocument()
- expect(screen.getByText(milestone.body)).toBeInTheDocument()
- expect(links[i].getAttribute('href')).toBe(milestone.url)
- }
+ const roadmapData = [...mockAboutData.project.recentMilestones].sort((a, b) =>
+ a.title > b.title ? 1 : -1
+ )
+
+ expect(screen.getByText(roadmapData[0].title)).toBeInTheDocument()
+ expect(screen.getByText(roadmapData[1].title)).toBeInTheDocument()
+ expect(screen.getByText(roadmapData[2].title)).toBeInTheDocument()
+ expect(screen.queryByText(roadmapData[3].title)).not.toBeInTheDocument()
+
+ const showMoreButtonRef = within(roadmapSection).getByRole('button', { name: /Show more/i })
+ fireEvent.click(showMoreButtonRef)
+
+ await waitFor(() => {
+ expect(screen.getByText(roadmapData[3].title)).toBeInTheDocument()
+ })
+
+ const showLessButtonRef = within(roadmapSection).getByRole('button', { name: /Show less/i })
+ fireEvent.click(showLessButtonRef)
+
+ await waitFor(() => {
+ expect(screen.queryByText(roadmapData[3].title)).not.toBeInTheDocument()
+ })
})
test('renders project stats cards correctly', async () => {
@@ -643,10 +680,9 @@ describe('About Component', () => {
render()
})
expect(screen.getByText('Project Timeline')).toBeInTheDocument()
- expect(screen.getByText('Timeline Event 1')).toBeInTheDocument()
+ expect(screen.getByText('Timeline Event 7')).toBeInTheDocument()
expect(screen.getByText('Timeline Event 2')).toBeInTheDocument()
- expect(screen.getByText('Timeline description 1')).toBeInTheDocument()
- expect(screen.getByText('Timeline description 2')).toBeInTheDocument()
+ expect(screen.queryByText('Timeline Event 1')).not.toBeInTheDocument()
})
test('triggers toaster error when GraphQL request fails for a leader', async () => {
diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx
index aafe8282db..122c96b8e6 100644
--- a/frontend/src/app/about/page.tsx
+++ b/frontend/src/app/about/page.tsx
@@ -5,7 +5,7 @@ import upperFirst from 'lodash/upperFirst'
import millify from 'millify'
import Image from 'next/image'
import Link from 'next/link'
-import { useEffect } from 'react'
+import { useState, useEffect } from 'react'
import { FaMapSigns, FaTools } from 'react-icons/fa'
import { FaCircleCheck, FaClock, FaScroll, FaBullseye, FaUser, FaUsersGear } from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
@@ -28,6 +28,7 @@ import AnchorTitle from 'components/AnchorTitle'
import Leaders from 'components/Leaders'
import Markdown from 'components/MarkdownWrapper'
import SecondaryCard from 'components/SecondaryCard'
+import ShowMoreButton from 'components/ShowMoreButton'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import TopContributorsList from 'components/TopContributorsList'
@@ -37,6 +38,9 @@ const leaders = {
mamicidal: 'CISSP',
}
+const PROJECT_LIMIT = 6
+const MILESTONE_LIMIT = 3
+
const projectKey = 'nest'
const getMilestoneStatus = (progress: number): string => {
@@ -87,6 +91,9 @@ const About = () => {
const projectMetadata = projectMetadataResponse?.project
const topContributors = topContributorsResponse?.topContributors
+ const [showAllRoadmap, setShowAllRoadmap] = useState(false)
+ const [showAllTimeline, setShowAllTimeline] = useState(false)
+
useEffect(() => {
if (projectMetadataRequestError) {
handleAppError(projectMetadataRequestError)
@@ -198,45 +205,56 @@ const About = () => {
{projectMetadata.recentMilestones.length > 0 && (
}>
-
- {[...projectMetadata.recentMilestones]
+ {(() => {
+ const filteredMilestones = [...projectMetadata.recentMilestones]
.filter((milestone) => milestone.state !== 'closed')
.sort((a, b) => (a.title > b.title ? 1 : -1))
- .map((milestone, index) => (
-
-
-
-
+
+ {filteredMilestones
+ .slice(0, showAllRoadmap ? filteredMilestones.length : MILESTONE_LIMIT)
+ .map((milestone, index) => (
+
-
- {milestone.title}
-
-
-
-
-
-
-
-
-
{milestone.body}
-
+
+
+
+
+ {milestone.title}
+
+
+
+
+
+
+
+
+
{milestone.body}
+
+
+ ))}
- ))}
-
+ {filteredMilestones.length > MILESTONE_LIMIT && (
+
setShowAllRoadmap(!showAllRoadmap)} />
+ )}
+ >
+ )
+ })()}
)}
}>
@@ -250,23 +268,31 @@ const About = () => {
}>
- {[...projectTimeline].reverse().map((milestone, index) => (
-
- {index !== projectTimeline.length - 1 && (
-
- )}
-
-
-
{milestone.title}
-
{milestone.year}
-
{milestone.description}
+ {(() => {
+ const visibleTimeline = [...projectTimeline]
+ .reverse()
+ .slice(0, showAllTimeline ? projectTimeline.length : PROJECT_LIMIT)
+ return visibleTimeline.map((milestone, index) => (
+
+ {index !== visibleTimeline.length - 1 && (
+
+ )}
+
+
+
{milestone.title}
+
{milestone.year}
+
{milestone.description}
+
-
- ))}
+ ))
+ })()}
+ {projectTimeline.length > PROJECT_LIMIT && (
+
setShowAllTimeline(!showAllTimeline)} />
+ )}