diff --git a/frontend/src/app/__tests__/page.test.tsx b/frontend/src/app/__tests__/page.test.tsx index 03cb3a5b..39496e74 100644 --- a/frontend/src/app/__tests__/page.test.tsx +++ b/frontend/src/app/__tests__/page.test.tsx @@ -1,29 +1,24 @@ -import React, { Suspense } from 'react'; -import { render, screen } from '@testing-library/react'; -import { axe, toHaveNoViolations } from 'jest-axe'; -import { LoadingSpinner } from '../../components/LoadingSpinner'; +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import Home from '../page'; -expect.extend(toHaveNoViolations); +jest.mock('../../components/LandingPage', () => ({ + LandingPage: () =>
, +})); -describe('Home page loading fallback', () => { - it('LoadingSpinner fallback has role=status', () => { - render(); - expect(screen.getByRole('status')).toBeInTheDocument(); - }); +describe('Home page dynamic loading fallback', () => { + it('renders exactly one loading indicator with a consistent aria-label while the dynamic import resolves', async () => { + render(); - it('LoadingSpinner fallback has aria-live=polite', () => { - render(); - expect(screen.getByRole('status')).toHaveAttribute('aria-live', 'polite'); - }); + const indicators = screen.getAllByRole('status'); + expect(indicators).toHaveLength(1); + expect(indicators[0]).toHaveAttribute('aria-label', 'Loading page'); + expect(indicators[0]).toHaveAttribute('aria-live', 'polite'); - it('LoadingSpinner fallback has an accessible label', () => { - render(); - expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Loading page'); - }); + await waitFor(() => { + expect(screen.getByTestId('landing-page-stub')).toBeInTheDocument(); + }); - it('LoadingSpinner fallback has no axe violations', async () => { - const { container } = render(); - const results = await axe(container); - expect(results).toHaveNoViolations(); + expect(screen.queryAllByRole('status')).toHaveLength(0); }); }); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d61f8b3e..9b9830c4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,18 +1,14 @@ 'use client'; import dynamic from 'next/dynamic'; -import { Suspense } from 'react'; import { LoadingSpinner } from '../components/LoadingSpinner'; -// Dynamic import with code splitting +// Dynamic import with code splitting. With ssr: true, next/dynamic manages +// its own loading boundary, so no outer is needed here. const LandingPage = dynamic(() => import('../components/LandingPage').then(mod => ({ default: mod.LandingPage })), { - loading: () => , + loading: () => , ssr: true, }); export default function Home() { - return ( - }> - - - ); + return ; } diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx index 4b76c2ff..200a5e7c 100644 --- a/frontend/src/components/LandingPage.tsx +++ b/frontend/src/components/LandingPage.tsx @@ -6,6 +6,9 @@ import { api } from '../lib/api/client'; import { Statistics } from './Statistics'; import { ErrorBoundary } from './ErrorBoundary'; import { LoadingSpinner } from './LoadingSpinner'; +import { FeatureCard } from './landing/FeatureCard'; +import { Step } from './landing/Step'; +import { FooterColumn } from './landing/FooterColumn'; interface LandingPageProps { className?: string; @@ -61,6 +64,32 @@ export const LandingPage: React.FC = ({ className }) => { } }; + const features = [ + { icon: '/icons/decentralized.svg', title: t('features.decentralized.title'), description: t('features.decentralized.description') }, + { icon: '/icons/secure.svg', title: t('features.secure.title'), description: t('features.secure.description') }, + { icon: '/icons/fast.svg', title: t('features.fast.title'), description: t('features.fast.description') }, + ]; + + const steps = [ + { title: t('howItWorks.step1.title'), description: t('howItWorks.step1.description') }, + { title: t('howItWorks.step2.title'), description: t('howItWorks.step2.description') }, + { title: t('howItWorks.step3.title'), description: t('howItWorks.step3.description') }, + { title: t('howItWorks.step4.title'), description: t('howItWorks.step4.description') }, + ]; + + const footerColumns = [ + { heading: t('footer.title'), headingLevel: 'h2' as const, tagline: t('footer.tagline') }, + { heading: t('footer.linksHeading'), links: [ + { href: '/docs', label: t('footer.documentation') }, + { href: '/github', label: t('footer.github') }, + { href: '/discord', label: t('footer.discord') }, + ] }, + { heading: t('footer.legalHeading'), links: [ + { href: '/privacy', label: t('footer.privacy') }, + { href: '/terms', label: t('footer.terms') }, + ] }, + ]; + return (
{/* Skip to main content link */} @@ -237,47 +266,9 @@ export const LandingPage: React.FC = ({ className }) => {

{t('features.heading')}

-
- -

{t('features.decentralized.title')}

-

- {t('features.decentralized.description')} -

-
- -
- -

{t('features.secure.title')}

-

- {t('features.secure.description')} -

-
- -
- -

{t('features.fast.title')}

-

- {t('features.fast.description')} -

-
+ {features.map((feature) => ( + + ))}
@@ -286,22 +277,9 @@ export const LandingPage: React.FC = ({ className }) => {

{t('howItWorks.heading')}

    -
  1. -

    {t('howItWorks.step1.title')}

    -

    {t('howItWorks.step1.description')}

    -
  2. -
  3. -

    {t('howItWorks.step2.title')}

    -

    {t('howItWorks.step2.description')}

    -
  4. -
  5. -

    {t('howItWorks.step3.title')}

    -

    {t('howItWorks.step3.description')}

    -
  6. -
  7. -

    {t('howItWorks.step4.title')}

    -

    {t('howItWorks.step4.description')}

    -
  8. + {steps.map((step) => ( + + ))}
@@ -320,27 +298,9 @@ export const LandingPage: React.FC = ({ className }) => { {/* Footer */}