Skip to content
Open
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
62 changes: 58 additions & 4 deletions src/features/landing/pages/LandingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {
Expand Down Expand Up @@ -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(<LandingPage />)
// 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(<LandingPage />)
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.getByText(/unable to load live statistics/i)).toBeInTheDocument()
})

it('shows formatted stats when data loads successfully', () => {
renderWithRouter(<LandingPage />)
// 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)
})
})
99 changes: 59 additions & 40 deletions src/features/landing/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -337,50 +337,69 @@ function WhyChooseUs() {
: 'bg-white/[0.15] border-white/25'
}`}
>
<div className="grid grid-cols-1 gap-6">
{[
{
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) => (
<div
key={index}
className={`flex items-center justify-between p-4 rounded-[16px] ${
theme === 'dark' ? 'bg-white/[0.05]' : 'bg-white/[0.1]'
}`}
>
<div className="flex items-center space-x-3">
<item.icon className="w-5 h-5 text-[#c9983a]" />
{error ? (
<div className="flex flex-col items-center justify-center py-8 text-center" role="alert">
<span className="text-4xl mb-3">⚠️</span>
<p className={`text-sm transition-colors ${
theme === 'dark' ? 'text-[#b8a898]' : 'text-[#7a6b5a]'
}`}>
Unable to load live statistics. Please try again later.
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-6">
{[
{
icon: TrendingUp,
label: 'Growing Ecosystem',
value: '+45%',
},
{
icon: Users,
label: 'Active Users',
value: isLoading ? (
<span className="inline-block w-16 h-5 rounded bg-current opacity-20 animate-pulse" />
) : (
display.contributors
),
},
{
icon: Award,
label: 'Projects Funded',
value: isLoading ? (
<span className="inline-block w-16 h-5 rounded bg-current opacity-20 animate-pulse" />
) : (
display.activeProjects
),
},
].map((item, index) => (
<div
key={index}
className={`flex items-center justify-between p-4 rounded-[16px] ${
theme === 'dark' ? 'bg-white/[0.05]' : 'bg-white/[0.1]'
}`}
>
<div className="flex items-center space-x-3">
<item.icon className="w-5 h-5 text-[#c9983a]" />
<span
className={`transition-colors ${
theme === 'dark' ? 'text-[#b8a898]' : 'text-[#7a6b5a]'
}`}
>
{item.label}
</span>
</div>
<span
className={`transition-colors ${
theme === 'dark' ? 'text-[#b8a898]' : 'text-[#7a6b5a]'
className={`font-semibold transition-colors ${
theme === 'dark' ? 'text-[#e8dfd0]' : 'text-[#2d2820]'
}`}
>
{item.label}
{item.value}
</span>
</div>
<span
className={`font-semibold transition-colors ${
theme === 'dark' ? 'text-[#e8dfd0]' : 'text-[#2d2820]'
}`}
>
{item.value}
</span>
</div>
))}
</div>
))}
</div>
)}
</div>
</div>
</div>
Expand Down
Loading