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
10 changes: 5 additions & 5 deletions frontend/__tests__/unit/components/AnchorTitle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('AnchorTitle Component', () => {

const titleElement = screen.getByText('Test')
expect(titleElement).toHaveClass('flex', 'items-center', 'text-2xl', 'font-semibold')
expect(titleElement).toHaveAttribute('id', 'anchor-title')
expect(titleElement).toHaveAttribute('data-anchor-title', 'true')
})

it('renders FontAwesome link icon', () => {
Expand Down Expand Up @@ -372,7 +372,7 @@ describe('AnchorTitle Component', () => {
render(<AnchorTitle title="Heading Test" />)

const titleElement = screen.getByText('Heading Test')
expect(titleElement).toHaveAttribute('id', 'anchor-title')
expect(titleElement).toHaveAttribute('data-anchor-title', 'true')
expect(titleElement).toHaveClass('text-2xl', 'font-semibold')

const container = document.getElementById('heading-test')
Expand Down Expand Up @@ -469,7 +469,7 @@ describe('AnchorTitle Component', () => {
fireEvent.click(link)

expect(mockScrollTo).toHaveBeenCalledWith({
top: 520,
top: 20,
behavior: 'smooth',
})

Expand Down Expand Up @@ -562,8 +562,8 @@ describe('AnchorTitle Component', () => {
fireEvent.click(link)
const secondCall = (mockScrollTo.mock.calls[1][0] as unknown as { top: number }).top

expect(firstCall).not.toBe(secondCall)
expect(Math.abs(firstCall - secondCall)).toBe(30)
expect(firstCall).toBe(secondCall)
expect(Math.abs(firstCall - secondCall)).toBe(0)

mockScrollTo.mockRestore()
mockGetElementById.mockRestore()
Expand Down
21 changes: 16 additions & 5 deletions frontend/__tests__/unit/components/CardDetailsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,18 @@ jest.mock('components/LeadersList', () => ({

jest.mock('components/MetricsScoreCircle', () => ({
__esModule: true,
default: ({ score, ...props }: { score: number; [key: string]: unknown }) => (
<div data-testid="metrics-score-circle" {...props}>
default: ({
score,
clickable,
onClick: _onClick,
...props
}: {
score: number
clickable?: boolean
onClick?: () => void
[key: string]: unknown
}) => (
<div data-testid="metrics-score-circle" role={clickable ? 'button' : undefined} {...props}>
Score: {score}
</div>
),
Expand Down Expand Up @@ -724,7 +734,7 @@ describe('CardDetailsPage', () => {
})

describe('Event Handling', () => {
it('renders clickable health metrics link', () => {
it('renders clickable health metrics button', () => {
render(
<CardDetailsPage
{...defaultProps}
Expand All @@ -733,8 +743,9 @@ describe('CardDetailsPage', () => {
/>
)

const healthLink = screen.getByRole('link')
expect(healthLink).toHaveAttribute('href', '#issues-trend')
const healthButton = screen.getByRole('button')
expect(healthButton).toBeInTheDocument()
expect(screen.getByTestId('metrics-score-circle')).toBeInTheDocument()
})

it('renders social links with correct hrefs and target attributes', () => {
Expand Down
170 changes: 167 additions & 3 deletions frontend/__tests__/unit/components/MetricsScoreCircle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'
import { render, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import '@testing-library/jest-dom'
import MetricsScoreCircle from 'components/MetricsScoreCircle'
Expand Down Expand Up @@ -172,14 +172,22 @@ describe('MetricsScoreCircle', () => {
})

// Test 9: Event handling - hover effects (visual testing through classes)
it('has hover effect classes applied', () => {
const { container } = render(<MetricsScoreCircle score={75} />)
it('has hover effect classes applied when clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={true} />)

// Check for hover-related classes
const hoverElement = container.querySelector('[class*="hover:"]')
expect(hoverElement).toBeInTheDocument()
})

it('does not have hover effect classes when not clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={false} />)

// Should not have hover-related classes
const hoverElement = container.querySelector('[class*="hover:"]')
expect(hoverElement).not.toBeInTheDocument()
})

// Test 10: Component integration test
it('integrates all features correctly for a low score', () => {
const { container } = render(<MetricsScoreCircle score={15} />)
Expand Down Expand Up @@ -218,4 +226,160 @@ describe('MetricsScoreCircle', () => {
'Current Project Health Score'
)
})

// Test 11: Click handling functionality
describe('click handling', () => {
it('calls onClick when clickable and onClick provided', () => {
const mockOnClick = jest.fn()
render(<MetricsScoreCircle score={75} clickable={true} onClick={mockOnClick} />)

const circleElement = screen.getByText('75').closest('.group')
if (circleElement) {
fireEvent.click(circleElement)
}

expect(mockOnClick).toHaveBeenCalledTimes(1)
})

it('does not call onClick when not clickable', () => {
const mockOnClick = jest.fn()
render(<MetricsScoreCircle score={75} clickable={false} onClick={mockOnClick} />)

const circleElement = screen.getByText('75').closest('.group')
if (circleElement) {
fireEvent.click(circleElement)
}

expect(mockOnClick).not.toHaveBeenCalled()
})

it('does not call onClick when no onClick provided', () => {
render(<MetricsScoreCircle score={75} clickable={true} />)

const circleElement = screen.getByText('75').closest('.group')
if (circleElement) {
fireEvent.click(circleElement)
}
// Should not throw any errors - test passes if no exception is thrown
expect(circleElement).toBeInTheDocument()
})

it('has cursor pointer when clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={true} />)

const clickableElement = container.querySelector('[class*="cursor-pointer"]')
expect(clickableElement).toBeInTheDocument()
})

it('does not have cursor pointer when not clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={false} />)

const clickableElement = container.querySelector('[class*="cursor-pointer"]')
expect(clickableElement).not.toBeInTheDocument()
})
})

// Test 12: Accessibility for clickable component
describe('accessibility for clickable component', () => {
it('has button role when clickable', () => {
render(<MetricsScoreCircle score={75} clickable={true} />)

const buttonElement = screen.getByRole('button')
expect(buttonElement).toBeInTheDocument()
})

it('does not have button role when not clickable', () => {
render(<MetricsScoreCircle score={75} clickable={false} />)

const buttonElement = screen.queryByRole('button')
expect(buttonElement).not.toBeInTheDocument()
})

it('has tabIndex when clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={true} />)

const clickableElement = container.querySelector('[tabindex="0"]')
expect(clickableElement).toBeInTheDocument()
})

it('does not have tabIndex when not clickable', () => {
const { container } = render(<MetricsScoreCircle score={75} clickable={false} />)

const clickableElement = container.querySelector('[tabindex]')
expect(clickableElement).not.toBeInTheDocument()
})

it('handles keyboard navigation when clickable', () => {
const mockOnClick = jest.fn()
render(<MetricsScoreCircle score={75} clickable={true} onClick={mockOnClick} />)

const buttonElement = screen.getByRole('button')

// Test Enter key
fireEvent.keyDown(buttonElement, { key: 'Enter' })
expect(mockOnClick).toHaveBeenCalledTimes(1)

// Test Space key
fireEvent.keyDown(buttonElement, { key: ' ' })
expect(mockOnClick).toHaveBeenCalledTimes(2)
})

it('does not handle keyboard navigation when not clickable', () => {
const mockOnClick = jest.fn()
render(<MetricsScoreCircle score={75} clickable={false} onClick={mockOnClick} />)

const circleElement = screen.getByText('75').closest('.group')
if (circleElement) {
fireEvent.keyDown(circleElement, { key: 'Enter' })
fireEvent.keyDown(circleElement, { key: ' ' })
}

expect(mockOnClick).not.toHaveBeenCalled()
})
})

// Test 13: Maintains existing functionality with new props
it('maintains all existing functionality when clickable is true', () => {
const { container } = render(<MetricsScoreCircle score={25} clickable={true} />)

// Should still have red styling
expect(container.querySelector('[class*="bg-red"]')).toBeInTheDocument()

// Should still have pulse animation
expect(container.querySelector('[class*="animate-pulse"]')).toBeInTheDocument()

// Should still display correct score
expect(screen.getByText('25')).toBeInTheDocument()

// Should still have tooltip
expect(screen.getByTestId('tooltip-wrapper')).toHaveAttribute(
'data-content',
'Current Project Health Score'
)

// Should have hover effects
expect(container.querySelector('[class*="hover:"]')).toBeInTheDocument()
})

it('maintains all existing functionality when clickable is false', () => {
const { container } = render(<MetricsScoreCircle score={25} clickable={false} />)

// Should still have red styling
expect(container.querySelector('[class*="bg-red"]')).toBeInTheDocument()

// Should still have pulse animation
expect(container.querySelector('[class*="animate-pulse"]')).toBeInTheDocument()

// Should still display correct score
expect(screen.getByText('25')).toBeInTheDocument()

// Should still have tooltip
expect(screen.getByTestId('tooltip-wrapper')).toHaveAttribute(
'data-content',
'Current Project Health Score'
)

// Should NOT have hover effects
expect(container.querySelector('[class*="hover:"]')).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ProjectHealthMetricsDetails: FC = () => {
/>
</div>
<div className="flex items-center gap-2">
<MetricsScoreCircle score={metricsLatest.score} />
<MetricsScoreCircle score={metricsLatest.score} clickable={false} />
<GeneralCompliantComponent
title={
metricsLatest.isFundingRequirementsCompliant
Expand Down
15 changes: 4 additions & 11 deletions frontend/src/components/AnchorTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useEffect, useCallback } from 'react'
import { scrollToAnchor, scrollToAnchorWithHistory } from 'utils/scrollToAnchor'
import slugify from 'utils/slugify'

interface AnchorTitleProps {
Expand All @@ -13,20 +14,12 @@ const AnchorTitle: React.FC<AnchorTitleProps> = ({ title }) => {
const href = `#${id}`

const scrollToElement = useCallback(() => {
const element = document.getElementById(id)
if (element) {
const headingHeight =
(element.querySelector('div#anchor-title') as HTMLElement)?.offsetHeight || 0
const yOffset = -headingHeight - 50
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset
window.scrollTo({ top: y, behavior: 'smooth' })
}
scrollToAnchor(id)
}, [id])

const handleClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
event.preventDefault()
scrollToElement()
window.history.pushState(null, '', href)
scrollToAnchorWithHistory(id)
}

useEffect(() => {
Expand All @@ -50,7 +43,7 @@ const AnchorTitle: React.FC<AnchorTitleProps> = ({ title }) => {
return (
<div id={id} className="relative">
<div className="group relative flex items-center">
<div className="flex items-center text-2xl font-semibold" id="anchor-title">
<div className="flex items-center text-2xl font-semibold" data-anchor-title="true">
{title}
</div>
<a
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import upperFirst from 'lodash/upperFirst'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import type { ExtendedSession } from 'types/auth'
import type { DetailsCardProps } from 'types/card'
import { IS_PROJECT_HEALTH_ENABLED } from 'utils/credentials'
import { scrollToAnchor } from 'utils/scrollToAnchor'
import { getSocialIcon } from 'utils/urlIconMappings'
import AnchorTitle from 'components/AnchorTitle'
import ChapterMapWrapper from 'components/ChapterMapWrapper'
Expand Down Expand Up @@ -96,9 +96,11 @@ const DetailsCard = ({
</button>
)}
{IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && (
<Link href="#issues-trend">
<MetricsScoreCircle score={healthMetricsData[0].score} />
</Link>
<MetricsScoreCircle
score={healthMetricsData[0].score}
clickable={true}
onClick={() => scrollToAnchor('issues-trend')}
/>
)}
</div>
{!isActive && (
Expand Down
Loading