Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
128 changes: 77 additions & 51 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 All @@ -37,6 +38,9 @@ const leaders = {
mamicidal: 'CISSP',
}

const PROJECT_LIMIT = 6
const MILESTONE_LIMIT = 3

const projectKey = 'nest'

const getMilestoneStatus = (progress: number): string => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -198,45 +205,56 @@ const About = () => {

{projectMetadata.recentMilestones.length > 0 && (
<SecondaryCard icon={FaMapSigns} title={<AnchorTitle title="Roadmap" />}>
<div className="grid gap-4">
{[...projectMetadata.recentMilestones]
{(() => {
const filteredMilestones = [...projectMetadata.recentMilestones]
.filter((milestone) => milestone.state !== 'closed')
.sort((a, b) => (a.title > b.title ? 1 : -1))
.map((milestone, index) => (
<div
key={milestone.url || milestone.title || index}
className="flex items-center gap-4 overflow-hidden rounded-lg bg-gray-200 p-6 dark:bg-gray-700"
>
<div className="flex-1">
<div className="relative">
<Link
href={milestone.url}
target="_blank"
rel="noopener noreferrer"
className="block"
return (
<>
<div className="grid gap-4">
{filteredMilestones
.slice(0, showAllRoadmap ? filteredMilestones.length : MILESTONE_LIMIT)
.map((milestone, index) => (
<div
key={milestone.url || milestone.title || index}
className="flex items-center gap-4 overflow-hidden rounded-lg bg-gray-200 p-6 dark:bg-gray-700"
>
<h3 className="mb-2 pr-8 text-xl font-semibold text-blue-400">
{milestone.title}
</h3>
</Link>
<Tooltip
closeDelay={100}
content={getMilestoneStatus(milestone.progress)}
id={`tooltip-state-${index}`}
delay={100}
placement="top"
showArrow
>
<span className="absolute top-0 right-0 text-xl text-gray-400">
<IconWrapper icon={getMilestoneIcon(milestone.progress)} />
</span>
</Tooltip>
</div>
<p className="text-gray-600 dark:text-gray-300">{milestone.body}</p>
</div>
<div className="flex-1">
<div className="relative">
<Link
href={milestone.url}
target="_blank"
rel="noopener noreferrer"
className="block"
>
<h3 className="mb-2 pr-8 text-xl font-semibold text-blue-400">
{milestone.title}
</h3>
</Link>
<Tooltip
closeDelay={100}
content={getMilestoneStatus(milestone.progress)}
id={`tooltip-state-${index}`}
delay={100}
placement="top"
showArrow
>
<span className="absolute top-0 right-0 text-xl text-gray-400">
<IconWrapper icon={getMilestoneIcon(milestone.progress)} />
</span>
</Tooltip>
</div>
<p className="text-gray-600 dark:text-gray-300">{milestone.body}</p>
</div>
</div>
))}
</div>
))}
</div>
{filteredMilestones.length > MILESTONE_LIMIT && (
<ShowMoreButton onToggle={() => setShowAllRoadmap(!showAllRoadmap)} />
)}
Comment thread
HarshitVerma109 marked this conversation as resolved.
</>
)
})()}
</SecondaryCard>
)}
<SecondaryCard icon={FaScroll} title={<AnchorTitle title="Our Story" />}>
Expand All @@ -250,23 +268,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 : PROJECT_LIMIT)
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 > PROJECT_LIMIT && (
<ShowMoreButton onToggle={() => setShowAllTimeline(!showAllTimeline)} />
)}
</SecondaryCard>

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