From f984ea8b3bde74dc497e4bfe0c00f0574cafa2f2 Mon Sep 17 00:00:00 2001 From: anirudhprmar Date: Mon, 15 Dec 2025 10:47:24 +0530 Subject: [PATCH 1/3] Refactored: removed animated counter --- frontend/__tests__/e2e/pages/About.spec.ts | 2 +- .../unit/components/AnimatedCounter.test.tsx | 295 ------------------ frontend/__tests__/unit/pages/Home.test.tsx | 33 +- frontend/src/app/about/page.tsx | 4 +- frontend/src/app/page.tsx | 6 +- frontend/src/components/AnimatedCounter.tsx | 35 --- 6 files changed, 17 insertions(+), 358 deletions(-) delete mode 100644 frontend/__tests__/unit/components/AnimatedCounter.test.tsx delete mode 100644 frontend/src/components/AnimatedCounter.tsx diff --git a/frontend/__tests__/e2e/pages/About.spec.ts b/frontend/__tests__/e2e/pages/About.spec.ts index f25f73f2fc..7fca8dfd21 100644 --- a/frontend/__tests__/e2e/pages/About.spec.ts +++ b/frontend/__tests__/e2e/pages/About.spec.ts @@ -77,7 +77,7 @@ test.describe('About Page', () => { } }) - test('displays animated counters with correct values', async ({ page }) => { + test('displays project statistics with correct values', async ({ page }) => { await expect(page.getByText('1.2K+Contributors')).toBeVisible() await expect(page.getByText('40+Open Issues')).toBeVisible() await expect(page.getByText('60+Forks')).toBeVisible() diff --git a/frontend/__tests__/unit/components/AnimatedCounter.test.tsx b/frontend/__tests__/unit/components/AnimatedCounter.test.tsx deleted file mode 100644 index b12061aa03..0000000000 --- a/frontend/__tests__/unit/components/AnimatedCounter.test.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { render, screen, act } from '@testing-library/react' -import '@testing-library/jest-dom' -import AnimatedCounter from 'components/AnimatedCounter' - -jest.useFakeTimers() - -// Patch for performance.now() in jsdom test env -beforeAll(() => { - if (typeof performance.now !== 'function') { - performance.now = jest.fn(() => Date.now()) - } -}) - -describe('AnimatedCounter', () => { - afterEach(() => { - jest.clearAllTimers() - }) - - describe('Renders successfully with minimal required props', () => { - it('renders correctly with initial count 0', () => { - render() - const counter = screen.getByText('0') - expect(counter).toBeInTheDocument() - }) - - it('renders with all props including className', () => { - render() - const element = screen.getByText('0') - expect(element).toHaveClass('test-class') - }) - }) - - describe('Prop-based behavior – different props affect output', () => { - it('renders with correct end value', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('applies custom className when provided', () => { - render() - const element = screen.getByText('0') - expect(element).toHaveClass('custom-counter') - }) - - it('renders without className when not provided', () => { - render() - const element = screen.getByText('0') - expect(element).not.toHaveAttribute('class') - }) - }) - - describe('State changes / internal logic', () => { - it('animates to the end value over duration', () => { - render() - - // Advance time by 2 seconds within act() - act(() => { - jest.advanceTimersByTime(2000) - }) - - const counter = screen.getByText('1K') - expect(counter).toBeInTheDocument() - }) - - it('updates count during animation', () => { - render() - - // Advance time by 1 second (halfway through animation) - act(() => { - jest.advanceTimersByTime(1000) - }) - - // Should show intermediate value - const displayedValue = Number.parseInt(screen.getByText(/\d+/).textContent || '0') - expect(displayedValue).toBeGreaterThan(0) - expect(displayedValue).toBeLessThanOrEqual(50) - }) - - it('stops at exact end value', () => { - render() - - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('75')).toBeInTheDocument() - }) - }) - - describe('Default values and fallbacks', () => { - it('handles zero end value', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles negative end value', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles very small duration', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles very large duration', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - }) - - describe('Text and content rendering', () => { - it('displays formatted numbers using millify', () => { - render() - - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('1.2K')).toBeInTheDocument() - }) - - it('renders as span element', () => { - render() - const element = screen.getByText('0') - expect(element.tagName).toBe('SPAN') - }) - }) - - describe('Handles edge cases and invalid inputs', () => { - it('handles decimal end values', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles very large end values', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles zero duration gracefully', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('handles negative duration gracefully', () => { - render() - expect(screen.getByText('0')).toBeInTheDocument() - }) - }) - - describe('DOM structure / classNames / styles', () => { - it('renders with correct HTML structure', () => { - render() - const element = screen.getByText('0') - expect(element).toBeInTheDocument() - expect(element.tagName).toBe('SPAN') - expect(element).toHaveClass('test-class') - }) - - it('applies multiple CSS classes when provided', () => { - render() - const element = screen.getByText('0') - expect(element).toHaveClass('class1', 'class2') - }) - - it('handles empty className string', () => { - render() - const element = screen.getByText('0') - expect(element).toHaveAttribute('class', '') - }) - }) - - describe('Animation behavior', () => { - it('calls requestAnimationFrame during animation', () => { - const requestAnimationFrameSpy = jest.spyOn(globalThis, 'requestAnimationFrame') - render() - - expect(requestAnimationFrameSpy).toHaveBeenCalled() - }) - - it('renders final value correctly', () => { - render() - - act(() => { - jest.advanceTimersByTime(100) - }) - - expect(screen.getByText('100')).toBeInTheDocument() - }) - - it('updates count correctly', () => { - render() - - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('10')).toBeInTheDocument() - }) - }) - - describe('Component lifecycle', () => { - it('re-initializes animation when props change', () => { - const { rerender } = render() - - // Wait for first animation to complete - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('50')).toBeInTheDocument() - - // Change props - rerender() - - // Should show the new end value after animation completes - act(() => { - jest.advanceTimersByTime(2000) - }) - - expect(screen.getByText('100')).toBeInTheDocument() - }) - - it('handles rapid prop changes gracefully', () => { - const { rerender } = render() - - // Rapidly change props - rerender() - rerender() - rerender() - - // Should not crash and should render - expect(screen.getByText('0')).toBeInTheDocument() - }) - }) - - describe('Accessibility considerations', () => { - it('renders content that can be read by screen readers', () => { - render() - const element = screen.getByText('0') - expect(element).toBeInTheDocument() - expect(element.textContent).toBeTruthy() - }) - - it('maintains semantic meaning of displayed numbers', () => { - render() - const element = screen.getByText('0') - expect(element).toBeInTheDocument() - // The number should be meaningful to screen readers - expect(element.textContent).toMatch(/\d+/) - }) - }) - - describe('Event handling and user interactions', () => { - it('responds to prop changes correctly', () => { - const { rerender } = render() - - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('100')).toBeInTheDocument() - - // Change end value - rerender() - - // Should show the new end value after animation completes - act(() => { - jest.advanceTimersByTime(1000) - }) - - expect(screen.getByText('200')).toBeInTheDocument() - }) - }) - - describe('Performance and optimization', () => { - it('does not cause infinite re-renders', () => { - const renderSpy = jest.fn() - const TestWrapper = () => { - renderSpy() - return - } - - render() - - act(() => { - jest.advanceTimersByTime(1000) - }) - - // Should not have excessive render calls - expect(renderSpy).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index 94e26deb09..f2d1759cc1 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -2,7 +2,6 @@ import { useQuery } from '@apollo/client/react' import { addToast } from '@heroui/toast' import { fireEvent, screen, waitFor } from '@testing-library/react' import { mockAlgoliaData, mockGraphQLData } from '@unit/data/mockHomeData' -import millify from 'millify' import { useRouter } from 'next/navigation' import { render } from 'wrappers/testUtil' import Home from 'app/page' @@ -181,17 +180,6 @@ describe('Home', () => { }) }) - test('renders AnimatedCounter components', async () => { - render() - - await waitFor(() => { - expect(screen.getByText('Active Projects')).toBeInTheDocument() - expect(screen.getByText('Contributors')).toBeInTheDocument() - expect(screen.getByText('Local Chapters')).toBeInTheDocument() - expect(screen.getByText('Countries')).toBeInTheDocument() - }) - }) - test('handles missing data gracefully', async () => { ;(useQuery as unknown as jest.Mock).mockReturnValue({ data: mockGraphQLData, @@ -271,18 +259,21 @@ describe('Home', () => { ] const stats = mockGraphQLData.statsOverview - await waitFor(() => { - for (const header of headers) { - expect(screen.getByText(header)).toBeInTheDocument() - } - }) + const statTexts = [ + stats.activeProjectsStats.toLocaleString('en-US'), + stats.activeChaptersStats.toLocaleString('en-US'), + stats.contributorsStats.toLocaleString('en-US'), + stats.countriesStats.toLocaleString('en-US'), + stats.slackWorkspaceStats.toLocaleString('en-US'), + ] - // Wait for animated counters to complete (2 seconds animation) - // Note: The "+" is rendered separately from the number, so we check for the number only await waitFor( () => { - for (const value of Object.values(stats)) { - expect(screen.getByText(millify(value), { exact: false })).toBeInTheDocument() + for (const stat of statTexts) { + expect(screen.getByText(stat)).toBeInTheDocument() + } + for (const header of headers) { + expect(screen.getByText(header)).toBeInTheDocument() } }, { timeout: 3000 } diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index 798b9c20bb..ec38eb103b 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -14,6 +14,7 @@ import { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Tooltip } from '@heroui/tooltip' import upperFirst from 'lodash/upperFirst' +import millify from 'millify' import Image from 'next/image' import Link from 'next/link' import { useEffect, useState } from 'react' @@ -34,7 +35,6 @@ import { projectStory, } from 'utils/aboutData' import AnchorTitle from 'components/AnchorTitle' -import AnimatedCounter from 'components/AnimatedCounter' import Leaders from 'components/Leaders' import Markdown from 'components/MarkdownWrapper' import SecondaryCard from 'components/SecondaryCard' @@ -299,7 +299,7 @@ const About = () => {
- + + {millify(Math.floor(stat.value / 10 || 0) * 10)}+
{stat.label}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 84c2301fed..a5f8ad210b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -18,6 +18,7 @@ import { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { addToast } from '@heroui/toast' import upperFirst from 'lodash/upperFirst' +import millify from 'millify' import Link from 'next/link' import { useEffect, useState } from 'react' import { fetchAlgoliaData } from 'server/fetchAlgoliaData' @@ -29,7 +30,6 @@ import type { MainPageData } from 'types/home' import { formatDate, formatDateRange } from 'utils/dateFormatter' import AnchorTitle from 'components/AnchorTitle' -import AnimatedCounter from 'components/AnimatedCounter' import CalendarButton from 'components/CalendarButton' import ChapterMapWrapper from 'components/ChapterMapWrapper' import LeadersList from 'components/LeadersList' @@ -376,9 +376,7 @@ export default function Home() { {counterData.map((stat) => (
-
- + -
+
{millify(stat.value)}+
{stat.label}
diff --git a/frontend/src/components/AnimatedCounter.tsx b/frontend/src/components/AnimatedCounter.tsx deleted file mode 100644 index 41f9139802..0000000000 --- a/frontend/src/components/AnimatedCounter.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import millify from 'millify' -import { useEffect, useRef, useState } from 'react' - -interface AnimatedCounterProps { - className?: string - duration: number - end: number -} - -export default function AnimatedCounter({ end, duration, className }: AnimatedCounterProps) { - const [count, setCount] = useState(0) - const countRef = useRef(count) - const startTime = useRef(Date.now()) - - useEffect(() => { - const animate = () => { - const now = Date.now() - const progress = Math.min((now - startTime.current) / (duration * 1000), 1) - const currentCount = Math.floor(progress * end) - - if (currentCount !== countRef.current) { - setCount(currentCount) - countRef.current = currentCount - } - - if (progress < 1) { - requestAnimationFrame(animate) - } - } - - requestAnimationFrame(animate) - }, [end, duration]) - - return {millify(count)} -} From 6cf95ed562be4e95276ab4519dc9b1669903287b Mon Sep 17 00:00:00 2001 From: anirudhprmar Date: Mon, 15 Dec 2025 11:06:58 +0530 Subject: [PATCH 2/3] updated code --- frontend/__tests__/unit/pages/Home.test.tsx | 14 ++++++++------ frontend/src/app/members/page.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index f2d1759cc1..2bed064321 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@apollo/client/react' import { addToast } from '@heroui/toast' import { fireEvent, screen, waitFor } from '@testing-library/react' import { mockAlgoliaData, mockGraphQLData } from '@unit/data/mockHomeData' +import millify from 'millify' import { useRouter } from 'next/navigation' import { render } from 'wrappers/testUtil' import Home from 'app/page' @@ -260,12 +261,13 @@ describe('Home', () => { const stats = mockGraphQLData.statsOverview const statTexts = [ - stats.activeProjectsStats.toLocaleString('en-US'), - stats.activeChaptersStats.toLocaleString('en-US'), - stats.contributorsStats.toLocaleString('en-US'), - stats.countriesStats.toLocaleString('en-US'), - stats.slackWorkspaceStats.toLocaleString('en-US'), - ] + millify(stats.activeProjectsStats) + '+', + millify(stats.activeChaptersStats) + '+', + millify(stats.contributorsStats) + '+', + millify(stats.countriesStats) + '+', + millify(stats.slackWorkspaceStats) + '+' + ]; + await waitFor( () => { diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 88ca788835..0763e36dcf 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -65,7 +65,7 @@ const UsersPage = () => { totalPages={totalPages} >
- {users && users.map((user) =>
{renderUserCard(user)}
)} + {users?.map((user) =>
{renderUserCard(user)}
)}
) From d9706609f2af2a47d3b9562568515b08deb2203d Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Mon, 15 Dec 2025 16:19:59 -0800 Subject: [PATCH 3/3] Update tests --- frontend/__tests__/unit/pages/Home.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index 2bed064321..3338d8c314 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -265,9 +265,8 @@ describe('Home', () => { millify(stats.activeChaptersStats) + '+', millify(stats.contributorsStats) + '+', millify(stats.countriesStats) + '+', - millify(stats.slackWorkspaceStats) + '+' - ]; - + millify(stats.slackWorkspaceStats) + '+', + ] await waitFor( () => {