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
11 changes: 7 additions & 4 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Quality (lint + typecheck)
name: Quality (lint + typecheck + unit tests)

# Garde-fous statiques du frontend. Introduits en amont de l'upgrade Next 16
# pour rendre visibles les regressions de typage et de lint.
#
# Etat actuel : NON BLOQUANT. Le depot demarre avec un passif d'erreurs de
# typage et de lint ; les etapes sont donc en `continue-on-error` le temps de
# resorber ce passif. Le passage en bloquant est le sujet de TASK-12.
# Etat actuel : typage et lint NON BLOQUANTS. Le depot demarre avec un passif
# d'erreurs ; ces deux etapes sont donc en `continue-on-error` le temps de
# resorber ce passif. Les tests unitaires, eux, sont bloquants. Le passage en bloquant est le sujet de TASK-12.

on:
push:
Expand Down Expand Up @@ -39,3 +39,6 @@ jobs:
- name: Lint
continue-on-error: true
run: pnpm --filter frontend run lint

- name: Unit tests
run: pnpm --filter frontend run test:unit
25 changes: 19 additions & 6 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,21 @@ pnpm run build-storybook
# Lancer les tests de composants (Vitest + navigateur, voir plus bas)
pnpm run test
pnpm run test:watch

# Lancer les tests unitaires (Node, sans navigateur)
pnpm run test:unit

# Lancer les deux
pnpm run test:all
```

#### Tests unitaires

`pnpm run test:unit` exécute les fichiers `src/**/*.test.ts` en environnement Node, sans navigateur :
quelques centaines de millisecondes, là où les tests de composants demandent une bonne minute. C'est
le bon endroit pour vérifier de la logique pure, par exemple la décision de rendu prise face à une
réponse Strapi (`src/lib/cms-outcome.ts`).

#### Tests de composants

Les stories servent de tests : chaque story est rendue dans un vrai navigateur (Vitest +
Expand Down Expand Up @@ -101,20 +114,20 @@ Voici les étapes à suivre:

```typescript
export default async function Homepage() {
const { data, error } = await client.GET('/home-page');
if (error) {
return <div>Error</div>;
}
const result = await client.GET('/home-page');
const data = requireCmsData<HomepageData>(result, 'singleton', '/home-page');

return (
<div>
Title:
{data.data?.title}
{data.title}
</div>
);
}
```

3. Passer le résultat par `requireCmsData` (`src/lib/cms-guard.ts`), jamais `data` directement : le client ne lève pas d'exception, il rend `{ data, error }`. La garde rend le contenu, répond 404 (`'detail'` sans entrée) ou lève une erreur journalisée `[cms] <adresse> : <statut>` qui produit un 500. Les pages dont seules les métadonnées viennent du CMS (charte, FAQ, CGU) restent tolérantes.

> [!NOTE]
> Avec le client fetch type-safe, les routes sont typées automatiquement ainsi que les paramètres et les données de réponse.

Expand All @@ -128,4 +141,4 @@ Rien pour le moment

- `fragile` : les redirections du CMS sont figées au `build` (`next.config.mjs`). L'image Docker de la CI est construite sans `STRAPI_API_TOKEN` → elle part sans aucune redirection, avec un simple avertissement.
- `not done` : `docker-compose.yml` vise une cible `frontend` absente du Dockerfile (étapes `base`, `builder`, `runner`) et fournit `STRAPI_URL` au lieu de `STRAPI_API_URL`.
- `not done` : 141 erreurs TypeScript et 43 erreurs ESLint restent ouvertes ; `ignoreBuildErrors` est actif et le workflow `quality.yml` est en `continue-on-error`.
- `not done` : 141 erreurs TypeScript et 41 erreurs ESLint restent ouvertes ; `ignoreBuildErrors` est actif et le workflow `quality.yml` est en `continue-on-error`.
8 changes: 8 additions & 0 deletions frontend/messages/fr/error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"error": {
"title": "Contenu momentanément indisponible",
"description": "Cette page n'a pas pu charger son contenu. La panne vient de notre côté, pas de votre navigateur.",
"retry": "Réessayer",
"backToHome": "Retour à l'accueil"
}
}
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "vitest run --project storybook",
"test:unit": "vitest run --project unit",
"test:all": "vitest run",
"test:watch": "vitest --project storybook",
"typecheck": "tsc --noEmit"
},
Expand Down
11 changes: 4 additions & 7 deletions frontend/src/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';
import AboutPage from './about';

export async function generateMetadata(
props: { params: Promise<{ locale: string }> },
) {
const { locale } = await props.params;

Check warning on line 9 in frontend/src/app/[locale]/about/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-quality

'locale' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 9 in frontend/src/app/[locale]/about/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-quality

'locale' is assigned a value but never used. Allowed unused vars must match /^_/u

const { data } = await fetchAboutPageData();

Expand Down Expand Up @@ -74,12 +75,8 @@


export default async function Page() {
const { data, error } = await fetchAboutPageData();
const result = await fetchAboutPageData();
const data = requireCmsData<AboutPageData>(result, 'singleton', '/about');

if (!data?.data) {
console.error(error);
return null;
}

return <AboutPage data={data.data} />;
return <AboutPage data={data} />;
};
14 changes: 5 additions & 9 deletions frontend/src/app/[locale]/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import ArticlePage from './article';
import { getMarkdownContent } from '@/lib/markdown';

Expand Down Expand Up @@ -48,14 +49,9 @@ export async function generateMetadata(

export default async function Page(props: { params: Promise<{ slug: string }> }) {
const { slug } = await props.params;
const { data } = await fetchBlogPageData(slug);

if (!data?.data || !data.data.length) {
return null;
}

const blogPageData = data.data[0] as BlogPageData;
const contentHtml = await getMarkdownContent(blogPageData.content)
const result = await fetchBlogPageData(slug);
const [blogPageData] = requireCmsData<BlogPageData[]>(result, 'detail', `/blogs?slug=${slug}`);
const contentHtml = await getMarkdownContent(blogPageData.content);

return <ArticlePage blog={{...blogPageData, contentHtml}} />;
return <ArticlePage blog={{ ...blogPageData, contentHtml }} />;
};
9 changes: 6 additions & 3 deletions frontend/src/app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import BlogPage from './blog';
import client from '@/lib/strapi-client';
import { cmsUnavailable, requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -52,10 +53,12 @@ export type BlogsPageMeta = NonNullable<NonNullable<BlogsPageResponse["meta"]>["

export default async function Page() {
const response = await fetchBlogsPageData();
const data = requireCmsData<BlogsPageData>(response, 'singleton', '/resources');
const pagination = response.data?.meta?.pagination;

if (!response?.data || !response?.data?.meta?.pagination) {
return null;
if (!pagination) {
throw cmsUnavailable('/resources', 'pagination absente de la reponse');
}

return <BlogPage data={response.data?.data} pagination={response.data.meta.pagination} />;
return <BlogPage data={data} pagination={pagination} />;
};
12 changes: 6 additions & 6 deletions frontend/src/app/[locale]/climate-and-biodiversity/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import ClimatePage from './climate';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -92,12 +93,11 @@ export type ThematicPageData = NonNullable<NonNullable<Awaited<ReturnType<typeof
export type ThematicsData = NonNullable<NonNullable<Awaited<ReturnType<typeof fetchThematics>>["data"]>["data"]>;

export default async function Page() {
const { data } = await fetchThematicPageData();
const { data: thematicsData } = await fetchThematics();
const themeResult = await fetchThematicPageData();
const thematicsResult = await fetchThematics();

if (!data?.data || !thematicsData?.data) {
return null;
}
const data = requireCmsData<ThematicPageData>(themeResult, 'singleton', '/climate-and-biodiversity');
const thematicsData = requireCmsData<ThematicsData>(thematicsResult, 'singleton', '/thematics');

return <ClimatePage data={data.data} thematicsData={thematicsData.data} />;
return <ClimatePage data={data} thematicsData={thematicsData} />;
};
12 changes: 6 additions & 6 deletions frontend/src/app/[locale]/democracy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import DemocracyPage from './democracy';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -70,12 +71,11 @@ export type ThematicPageData = NonNullable<NonNullable<Awaited<ReturnType<typeof
export type ThematicsData = NonNullable<NonNullable<Awaited<ReturnType<typeof fetchThematics>>["data"]>["data"]>;

export default async function Page() {
const { data } = await fetchThematicPageData();
const { data: thematicsData } = await fetchThematics();
const themeResult = await fetchThematicPageData();
const thematicsResult = await fetchThematics();

if (!data?.data || !thematicsData?.data) {
return null;
}
const data = requireCmsData<ThematicPageData>(themeResult, 'singleton', '/democracy');
const thematicsData = requireCmsData<ThematicsData>(thematicsResult, 'singleton', '/thematics');

return <DemocracyPage data={data.data} thematicsData={thematicsData.data} />;
return <DemocracyPage data={data} thematicsData={thematicsData} />;
};
12 changes: 5 additions & 7 deletions frontend/src/app/[locale]/donations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import DonationsPage from './donation';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';
import { getMarkdownContent } from '@/lib/markdown';

Expand Down Expand Up @@ -48,12 +49,9 @@ async function fetchDonationData() {
export type DonationsData = NonNullable<NonNullable<Awaited<ReturnType<typeof fetchDonationData>>["data"]>["data"]>;

export default async function Page() {
const { data } = await fetchDonationData();

if (!data?.data) {
return null;
}
const result = await fetchDonationData();
const data = requireCmsData<DonationsData>(result, 'singleton', '/donation');

const introduction_text = await getMarkdownContent(data.data.introduction_text);
return <DonationsPage data={{ ...data.data, introduction_text }} />;
const introduction_text = await getMarkdownContent(data.introduction_text);
return <DonationsPage data={{ ...data, introduction_text }} />;
};
42 changes: 42 additions & 0 deletions frontend/src/app/[locale]/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Button, Title } from '@/components';

export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const t = useTranslations('error');

useEffect(() => {
// Le serveur a deja journalise la cause avec l'adresse appelee. Ici, c'est
// la console du navigateur qui parle, utile pendant le developpement.
console.error(error);
}, [error]);

return (
<div className="container my-lg pt-lg">
<div className="max-w-2xl mx-auto text-center">
<Title variant="medium" className="mb-xs">
{t('title')}
</Title>
<p className="lead mb-md">
{t('description')}
</p>
<div className="flex flex-wrap items-center justify-center gap-sm">
<Button variant="primary" color="black" onClick={() => reset()}>
{t('retry')}
</Button>
<Button href="/" variant="tertiary" color="black" hasArrow={false}>
{t('backToHome')}
</Button>
</div>
</div>
</div>
);
}
9 changes: 6 additions & 3 deletions frontend/src/app/[locale]/events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import EventsPage from './events';
import client from '@/lib/strapi-client';
import { cmsUnavailable, requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -52,10 +53,12 @@ export default async function Page(
const pageSize = 6;

const response = await fetchEventPageData(page, pageSize);
const data = requireCmsData<EventsPageData>(response, 'singleton', '/events');
const pagination = response.data?.meta?.pagination;

if (!response?.data || !response?.data.meta?.pagination) {
return null;
if (!pagination) {
throw cmsUnavailable('/events', 'pagination absente de la reponse');
}

return <EventsPage data={response.data?.data} pagination={response.data.meta.pagination} currentPage={page} />;
return <EventsPage data={data} pagination={pagination} currentPage={page} />;
};
10 changes: 4 additions & 6 deletions frontend/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Homepage from './home';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -73,11 +74,8 @@ async function fetchHomepageData() {
export type HomepageData = NonNullable<NonNullable<Awaited<ReturnType<typeof fetchHomepageData>>["data"]>["data"]>;

export default async function Page() {
const { data } = await fetchHomepageData();
const result = await fetchHomepageData();
const data = requireCmsData<HomepageData>(result, 'singleton', '/home-page');

if (!data?.data) {
return null;
}

return <Homepage data={data.data} />;
return <Homepage data={data} />;
}
10 changes: 4 additions & 6 deletions frontend/src/app/[locale]/positions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import PositionsPage from './positions';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { generateMetadataFromSeo } from '@/lib/utils';

export async function generateMetadata(
Expand Down Expand Up @@ -80,11 +81,8 @@ async function fetchPositionPageData() {
export type PositionsPageData = NonNullable<NonNullable<Awaited<ReturnType<typeof fetchPositionPageData>>["data"]>["data"]>;

export default async function Page() {
const { data } = await fetchPositionPageData();

if (!data?.data) {
return null;
}
const result = await fetchPositionPageData();
const data = requireCmsData<PositionsPageData>(result, 'singleton', '/position');

return <PositionsPage data={data.data} />;
return <PositionsPage data={data} />;
};
11 changes: 4 additions & 7 deletions frontend/src/app/[locale]/projects/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import ProjectDetailPage from './projectDetail';
import client from '@/lib/strapi-client';
import { requireCmsData } from '@/lib/cms-guard';
import { getMarkdownContent } from '@/lib/markdown';

async function fetchProjectPageData(slug: string) {
Expand Down Expand Up @@ -82,16 +83,12 @@ export async function generateMetadata(

export default async function Page(props: { params: Promise<{ slug: string }> }) {
const { slug } = await props.params;
const { data } = await fetchProjectPageData(slug);

if (!data?.data || !data.data.length) {
return null;
}
const result = await fetchProjectPageData(slug);
const [projectData] = requireCmsData<ProjectPageData[]>(result, 'detail', `/projects?slug=${slug}`);

const projectData = data.data[0] as ProjectPageData;
const context = await getMarkdownContent(projectData.context);
const long_description = await getMarkdownContent(projectData.long_description);
const delivrable = await getMarkdownContent(projectData.delivrable);

return <ProjectDetailPage project={{...projectData, context, long_description, delivrable}} />;
return <ProjectDetailPage project={{ ...projectData, context, long_description, delivrable }} />;
};
Loading
Loading