From 67ad977a63a2979603f9274873441185dbd37a53 Mon Sep 17 00:00:00 2001 From: waterWang Date: Sun, 2 Aug 2026 13:13:28 +0800 Subject: [PATCH] fix: surface useLandingStats error and loading state in WhyChooseUs section (Closes #878) The WhyChooseUs component only destructured `display` from `useLandingStats()`, silently ignoring `isLoading` and `error`. Changes: - Destructure isLoading and error alongside display - Show animate-pulse skeleton placeholders while stats load - Show error fallback with alert role when stats fail to load - Add test cases for loading, error, and success states Fixes #878 --- .../landing/pages/LandingPage.test.tsx | 62 +++++++++++- src/features/landing/pages/LandingPage.tsx | 99 +++++++++++-------- 2 files changed, 117 insertions(+), 44 deletions(-) diff --git a/src/features/landing/pages/LandingPage.test.tsx b/src/features/landing/pages/LandingPage.test.tsx index b8fc8110..6c5335df 100644 --- a/src/features/landing/pages/LandingPage.test.tsx +++ b/src/features/landing/pages/LandingPage.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react' import { LandingPage } from './LandingPage' import { MemoryRouter } from 'react-router-dom' import { I18nProvider } from '../../../shared/i18n' +import { useLandingStats } from '../../../shared/hooks/useLandingStats' // --------------------------------------------------------------------------- // Mocks @@ -16,15 +17,18 @@ vi.mock('../../../shared/contexts/AuthContext', () => ({ useAuth: () => ({ isAuthenticated: false, logout: vi.fn() }), })) -vi.mock('../../../shared/hooks/useLandingStats', () => ({ - useLandingStats: () => ({ +vi.mock('../../../shared/hooks/useLandingStats', () => { + const mockFn = vi.fn(() => ({ display: { activeProjects: '1,234', contributors: '5,678', grantsDistributed: '$2.1M', }, - }), -})) + isLoading: false, + error: null, + })) + return { useLandingStats: mockFn } +}) vi.mock('../../../shared/utils/logger', () => ({ logger: { @@ -171,3 +175,53 @@ describe('ImageWithFallback security', () => { expect(images.length).toBe(3) }) }) + +describe('WhyChooseUs stats states', () => { + beforeEach(() => { + vi.mocked(useLandingStats).mockImplementation(() => ({ + stats: {} as any, + display: { activeProjects: '1,234', contributors: '5,678', grantsDistributed: '$2.1M' }, + isLoading: false, + error: null, + })) + }) + + it('shows skeleton pulse placeholders while stats are loading', () => { + vi.mocked(useLandingStats).mockImplementation(() => ({ + stats: null, + display: { activeProjects: '—', contributors: '—', grantsDistributed: '—' }, + isLoading: true, + error: null, + })) + + renderWithRouter() + // The stats section (WhyChooseUs) shows 2 pulse placeholders for Active Users and Projects Funded + // Hero also uses useLandingStats but renders SkeletonLoader, not animate-pulse + const whyChooseUs = document.getElementById('why-choose-us') + const pulses = whyChooseUs?.querySelectorAll('.animate-pulse') ?? [] + expect(pulses.length).toBe(2) + }) + + it('shows error fallback when stats fail to load', () => { + vi.mocked(useLandingStats).mockImplementation(() => ({ + stats: null, + display: { activeProjects: '—', contributors: '—', grantsDistributed: '—' }, + isLoading: false, + error: 'Network error', + })) + + renderWithRouter() + expect(screen.getByRole('alert')).toBeInTheDocument() + expect(screen.getByText(/unable to load live statistics/i)).toBeInTheDocument() + }) + + it('shows formatted stats when data loads successfully', () => { + renderWithRouter() + // Hero section also renders the same stats values, so use getAllByText + const contributors = screen.getAllByText('5,678') + expect(contributors.length).toBeGreaterThanOrEqual(1) + + const projects = screen.getAllByText('1,234') + expect(projects.length).toBeGreaterThanOrEqual(1) + }) +}) diff --git a/src/features/landing/pages/LandingPage.tsx b/src/features/landing/pages/LandingPage.tsx index f8902058..a32940d6 100644 --- a/src/features/landing/pages/LandingPage.tsx +++ b/src/features/landing/pages/LandingPage.tsx @@ -258,7 +258,7 @@ function HowItWorks() { function WhyChooseUs() { const { theme } = useTheme() - const { display } = useLandingStats() + const { display, isLoading, error } = useLandingStats() const benefits = [ 'Verified and vetted projects from trusted organizations', @@ -337,50 +337,69 @@ function WhyChooseUs() { : 'bg-white/[0.15] border-white/25' }`} > -
- {[ - { - icon: TrendingUp, - label: 'Growing Ecosystem', - value: '+45%', - }, - { - icon: Users, - label: 'Active Users', - value: display.contributors, - }, - { - icon: Award, - label: 'Projects Funded', - value: display.activeProjects, - }, - ].map((item, index) => ( -
-
- + {error ? ( +
+ ⚠️ +

+ Unable to load live statistics. Please try again later. +

+
+ ) : ( +
+ {[ + { + icon: TrendingUp, + label: 'Growing Ecosystem', + value: '+45%', + }, + { + icon: Users, + label: 'Active Users', + value: isLoading ? ( + + ) : ( + display.contributors + ), + }, + { + icon: Award, + label: 'Projects Funded', + value: isLoading ? ( + + ) : ( + display.activeProjects + ), + }, + ].map((item, index) => ( +
+
+ + + {item.label} + +
- {item.label} + {item.value}
- - {item.value} - -
- ))} -
+ ))} +
+ )}