Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
26 changes: 24 additions & 2 deletions frontend/__tests__/e2e/pages/About.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
Expand All @@ -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()
})
})
12 changes: 12 additions & 0 deletions frontend/__tests__/mockData/mockAboutData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
82 changes: 59 additions & 23 deletions frontend/__tests__/unit/pages/About.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -275,15 +280,33 @@ describe('About Component', () => {
render(<About />)
})

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 () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -643,10 +680,9 @@ describe('About Component', () => {
render(<About />)
})
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()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('triggers toaster error when GraphQL request fails for a leader', async () => {
Expand Down
48 changes: 32 additions & 16 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -87,6 +88,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)
Expand Down Expand Up @@ -202,6 +206,7 @@ const About = () => {
{[...projectMetadata.recentMilestones]
.filter((milestone) => milestone.state !== 'closed')
.sort((a, b) => (a.title > b.title ? 1 : -1))
.slice(0, showAllRoadmap ? projectMetadata.recentMilestones.length : 3)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
.map((milestone, index) => (
<div
key={milestone.url || milestone.title || index}
Expand Down Expand Up @@ -237,6 +242,9 @@ const About = () => {
</div>
))}
</div>
{projectMetadata.recentMilestones.length > 3 && (
<ShowMoreButton onToggle={() => setShowAllRoadmap(!showAllRoadmap)} />
)}
</SecondaryCard>
)}
<SecondaryCard icon={FaScroll} title={<AnchorTitle title="Our Story" />}>
Expand All @@ -250,23 +258,31 @@ const About = () => {
</SecondaryCard>
<SecondaryCard icon={FaClock} title={<AnchorTitle title="Project Timeline" />}>
<div className="space-y-6">
{[...projectTimeline].reverse().map((milestone, index) => (
<div key={`${milestone.year}-${milestone.title}`} className="relative pl-10">
{index !== projectTimeline.length - 1 && (
<div className="absolute top-5 left-[5px] h-full w-0.5 bg-gray-400"></div>
)}
<div
aria-hidden="true"
className="absolute top-2.5 left-0 h-3 w-3 rounded-full bg-gray-400"
></div>
<div>
<h3 className="text-lg font-semibold text-blue-400">{milestone.title}</h3>
<h4 className="mb-1 font-medium text-gray-400">{milestone.year}</h4>
<p className="text-gray-600 dark:text-gray-300">{milestone.description}</p>
{(() => {
const visibleTimeline = [...projectTimeline]
.reverse()
.slice(0, showAllTimeline ? projectTimeline.length : 6)
return visibleTimeline.map((milestone, index) => (
<div key={`${milestone.year}-${milestone.title}`} className="relative pl-10">
{index !== visibleTimeline.length - 1 && (
<div className="absolute top-5 left-[5px] h-full w-0.5 bg-gray-400"></div>
)}
<div
aria-hidden="true"
className="absolute top-2.5 left-0 h-3 w-3 rounded-full bg-gray-400"
></div>
<div>
<h3 className="text-lg font-semibold text-blue-400">{milestone.title}</h3>
<h4 className="mb-1 font-medium text-gray-400">{milestone.year}</h4>
<p className="text-gray-600 dark:text-gray-300">{milestone.description}</p>
</div>
</div>
</div>
))}
))
})()}
</div>
{projectTimeline.length > 6 && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic var

<ShowMoreButton onToggle={() => setShowAllTimeline(!showAllTimeline)} />
)}
</SecondaryCard>

<div className="grid gap-0 md:grid-cols-4 md:gap-6">
Expand Down