codegouvfr/react-dsfr

Utilisation avec `jest` et `@testing-library/react`

Closed this issue · 20 comments

Bonjour,

J'ai vu que le package était généré en ESM.

Ayant comme moteur de test jest, qui par défaut, ne supporte pas l'ESM, j'ai des soucis à faire tourner mes tests. Pour pallier à cela, j'ai essayé :

  1. d'utiliser plusieurs transformers différents pour transpile le code (babel-jest, @swc/jest, ts-jest avec comme presets ts-jest/presets/js-with-ts-esm)
  2. Essayer d'ignorer, le node_modules/@codegouvfr/react-dsfr, car j'ai vu que sur d'autres projet open-sourcec'était l'approche mise en place

Une des dernières configuration que j'ai essayé :

const nextJest = require("next/jest");

const createJestConfig = nextJest();

+ const esModules = ["@codegouvfr/react-dsfr"].join("|");

const customJestConfig = {
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterEnv: ["<rootDir>/test/jest.setup.js"],
  testMatch: ["**/__tests__/**/*?(*.)+(test|spec).[jt]s?(x)"],
  testPathIgnorePatterns: [
    "<rootDir>/.next/",
    "<rootDir>/node_modules/",
    "<rootDir>/cypress/",
  ],
  testTimeout: 20000,
  moduleNameMapper: {
    "^@sentry/nextjs$": "<rootDir>/test/mockSentry.js",
  },
+  preset: "ts-jest/presets/js-with-ts-esm",
+  transformIgnorePatterns: [`<rootDir>/node_modules/(?!(${esModules})/)`],
+  transform: {
+   "^.+\\.(js|jsx|ts|tsx)$": ['ts-jest'],
+ },
};

module.exports = createJestConfig(customJestConfig);

Recommendez-vous d'autres approches spécifiques pour les tests en @testing-library/react avec l'implem par défaut de next en jest ?

Dans le cas où je résous cela, je pourrai voir pour faire une PR sur la documentation afin d'avoir une section test.

Merci et encore gg pour la lib 👌

Hey @maxgfr,

Ca va bien?

Oui alors normalement pour jest tu n'a rien besoin de faire d'autre que:

image

Je l'ai mis dans la doc pour Create React App parce que c'est le framwork de test par default.

Si non la solution que je recommende fortement est Vitest: https://nextjs.org/docs/app/building-your-application/testing/vitest

Mais fait moi savoir si ça marche quand même avec Jest essaie avec just transformIgnorePatterns

Yes ça va et toi ?

Ouais j'avais essayé en première intention, malheureusement, ça bloque toujours avec ce genre d'erreur :

 FAIL  src/modules/layout/__tests__/Footer.test.tsx
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    /Users/max/Documents/Code/code-du-travail-numerique/node_modules/@codegouvfr/react-dsfr/Footer.js:12
    import React, { memo, forwardRef } from "react";
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      10 |     <FooterDsfr
      11 |       brandTop={<BrandTop />}
    > 12 |       homeLinkProps={homeLinksProps}
         |                 ^
      13 |       accessibility="partially compliant"
      14 |       linkList={[
      15 |         {

      at Runtime.createScriptFromCode (../../node_modules/jest-runtime/build/index.js:1505:14)
      at Object.<anonymous> (src/modules/layout/Footer.tsx:12:17)
      at Object.<anonymous> (src/modules/layout/__tests__/Footer.test.tsx:8:17)

ou

    Details:

    /Users/max/Documents/Code/code-du-travail-numerique/node_modules/@codegouvfr/react-dsfr/fr/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export * from "./colors";
                                                                                      ^^^^^^

    SyntaxError: Unexpected token 'export'

      10 |
      11 | export const Stats = (props: StatsProps) => (
    > 12 |   <div className={fr.cx("fr-grid-row")}>
         |                    ^
      13 |     <div className={fr.cx("fr-my-4w", "fr-col-12")}>
      14 |       <h1 className={fr.cx("fr-mt-0")}>Statistiques d&apos;utilisation</h1>
      15 |       <div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>

      at Runtime.createScriptFromCode (../../node_modules/jest-runtime/build/index.js:1505:14)
      at Object.<anonymous> (src/modules/stats/index.tsx:12:20)
      at Object.<anonymous> (src/modules/stats/__tests__/Stats.test.tsx:8:11)

en utilisant cette configuration :

const nextJest = require("next/jest");

const createJestConfig = nextJest();

const customJestConfig = {
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterEnv: ["<rootDir>/test/jest.setup.js"],
  testMatch: ["**/__tests__/**/*?(*.)+(test|spec).[jt]s?(x)"],
  testPathIgnorePatterns: [
    "<rootDir>/.next/",
    "<rootDir>/node_modules/",
    "<rootDir>/cypress/",
  ],
  testTimeout: 20000,
  moduleNameMapper: {
    "^@sentry/nextjs$": "<rootDir>/test/mockSentry.js",
  },
  transformIgnorePatterns: [
    "<rootDir>/node_modules/(?!@codegouvfr/react-dsfr/)",
//ou "node_modules/(?!@codegouvfr/react-dsfr/)"
  ],
};

module.exports = createJestConfig(customJestConfig);

Et oui, utiliser vitest semble être la solution, mais bon ça va nous faire migrer des centaines de tests ahah

A mon avis c'est une histoire de monorepo de de path qui n'es pas resolved.
Là en fait c'est juste que transformIgnorePatterns il est pas bon...
Vous êtes avec lerna c'est ça?

Essaie de run jest en mode debug pour voir pourquoi le pattern ne marche pas. Je ne connais pas Lerna je ne sais pas trop ou sont installer les dependances mais en tout cas je suis assez sur que c'est ça, il faut juste le bon pattern.

Ca déjà c'est normal que ça ne marche pas: "<rootDir>/node_modules/(?!@codegouvfr/react-dsfr/)",

Ton root dir là in pointe vers packages/code-du-travail-frontend mais @codegouv/react-dsfr il est installer dans les node_modules a la racine du monorepo

Essaie "../../node_modules/(?!@codegouvfr/react-dsfr/)",

J'ai aussi l'impression que ça vient de lerna, j'ai testé plusieurs chemins possibles mais sans succès :

transformIgnorePatterns: [
    "<rootDir>/packages/code-du-travail-frontend/node_modules/(?!@codegouvfr/react-dsfr/)",
    "<rootDir>/node_modules/(?!@codegouvfr/react-dsfr/)",
    "/node_modules/(?!@codegouvfr/react-dsfr/)",
    "node_modules/(?!@codegouvfr/react-dsfr/)",
    "../../node_modules/(?!@codegouvfr/react-dsfr/)",
  ],

En parralèlle, en passant par vitest, ça fonctionne niquel donc, y'a moyen que je fasse la migration

Normalement <rootDir> pointe vers la racine du projet où sont installés les libs, donc ça devrait marcher... Très bizarre, que la config ne soit pas prise en compte

J'ai même essayé la path de base /Users/max/Documents/Code/code-du-travail-numerique/node_modules/@codegouvfr ahah. Comme si l'override de la config next/jest ne prend pas en compte ce paramètre

Je ne suis pas sûr que ça joue mais ma configuration typescript est la suivante :

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "forceConsistentCasingInFileNames": true,
    "strictNullChecks": true,
    "downlevelIteration": true,
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules", "cypress"]
}

L'output de next est au build en esnext et bundler.

Ça fait mal le fait avec les frameworks de tests aient leur propre transpilation, ce qui peut différer de l'output général. D'autant plus que ça soit tellement abusé que next par défaut utilise jest, et non vitest vu que jest ne supporte pas les packages esm par défaut...

D'ailleurs, j'ai vu que tu n'as pas ajouté le transpilePackage dans la documentation du app router :
Screenshot 2024-09-05 at 10 33 24

Dans tous les cas, vu que vitest fonctionne niquel, ça vient de la configuration de jest même si, comme ils indiquent By default "node_modules" folder is ignored by transformers.

En soit, le soucis vient de jest et non du code source, donc je pense on peut cloturer l'issue, hormis si tu vois d'autres choses qui peuvent sembler utile

L'alternative vitest fonctionne niquel.

A voir, ceux qui sont en monorepo et/ou utilisent jest pour avoir leur feedback

D'ailleurs en regardant le code source de next/jest, je suis tombé sur ça qui est intéressant : https://github.com/vercel/next.js/blob/canary/packages/next/src/build/jest/jest.ts#L105

Ils utilisent les transpilePackages qu'ils passent ici : https://github.com/vercel/next.js/blob/canary/packages/next/src/build/jest/jest.ts#L172

Donc en théorie, on aurait même pas besoin de passer sur jest le transformIgnorePattern sur next.js

Normalement pointe vers la racine du projet où sont installés les libs, donc ça devrait marcher...

Ben non, si j'en crois votre setup ça pointe vers src/packages/code-du-travail-frontend.
Du moins si j'en crois votre moduleNameMapper:

image

En plus la notrion de "dossier ou les libs sont installer est assez vague... il y a plein de configuration.

C'est vrai que c'est un peu le bordel avec lerna la manière dont sont installés les dépendances....

Sinon, oui t'as totalement raison, le <rootDir>, c'est bien le . par défaut, donc le <rootDir>/node_modules/(?!@codegouvfr/react-dsfr/), ne peut pas marcher, car les dépendances sont situés à ../../ du <rootDir>

Au fait, j'ai remarqué que tu n'as pas ajouté le transpilePackage dans la documentation du App Router.

Je comprends que ce soit contraignant pour vous. Next.js peut vraiment être un casse-tête pour les mainteneurs de librairies. J'aimerais pouvoir simplifier les choses ça m'embète de vous faire toucher votre config, mais je n'ai malheureusement pas trouvé de solution viable.
De manière générale je suis assez frustré que mettre en place React DSFR dans un project Next App Dir ne soit pas plus simple. En particulier les fait qu'il y ai plein de petit fichier a crée et que qu'il faillent installer sass...
Mais bon une fois que c'est setup au moins ça marche. J'essairais de simplifier le setup au fur et a mesur des avancée de Next.

C'est dommage si vous n'arrivez pas à faire fonctionner ça avec Jest. J'aurais vraiment aimé fournir une solution adaptée pour les autres équipes.
Je ne peux malheureusement pas livrer react-dsfr en CommonJS. Je fais de mon mieux pour rendre les choses plus simples, mais dans le cas spécifique de react-dsfr, des contraintes techniques m'obligent à le publier exclusivement en ESM.

Salut @garronej

En creusant de mon côté sur l'implémentation avec next, j'ai vu que tu pouvais paramètrer le folder, à priori avec cette configuration pour mon jest.config.js :

const nextJest = require("next/jest");

+ const createJestConfig = nextJest({
+ dir: "./",
+ });

/** @type {import('jest').Config} */
const customJestConfig = {
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterEnv: ["<rootDir>/test/jest.setup.js"],
  testMatch: ["**/__tests__/**/*?(*.)+(test|spec).[jt]s?(x)"],
  testPathIgnorePatterns: [
    "<rootDir>/.next/",
    "<rootDir>/node_modules/",
    "<rootDir>/cypress/",
  ],
  testTimeout: 20000,
  moduleNameMapper: {
    "^@sentry/nextjs$": "<rootDir>/test/mockSentry.js",
  },
};

module.exports = createJestConfig(customJestConfig);

Et pour ma config next.config.mjs, celle-ci :

const nextConfig = {
  poweredByHeader: false,
  compiler: {
    reactRemoveProperties:
      process.env.NEXT_PUBLIC_APP_ENV === "production"
        ? { properties: ["data-testid"] }
        : false,
    styledComponents: true,
  },
  eslint: {
    ignoreDuringBuilds: true,
  },
  staticPageGenerationTimeout: 60 * 5, // 5 minutes
  experimental: {
    instrumentationHook: true,
  },
  webpack: (config) => {
    config.module.rules.push({
      test: /\.woff2$/,
      type: "asset/resource",
    });
    return config;
  },
+ transpilePackages: ["@codegouvfr/react-dsfr"],
};

Ça semble fonctionner. Par défaut, next/jest va transpiler @codegouvfr/react-dsfr même pour jest, comme on l'a vu plus haut.

Merci pour le retour, je close l'issue, see you ;)

Même si c'est très bizarre que par défaut ce ne soit pas : dir: "./" dans la configuration. Dans tous les cas, si des gens en monorepo ont des soucis, cette issue pourront les aider

Ah genial je vais l'ajouter a la documentation!