> ## Documentation Index
> Fetch the complete documentation index at: https://auth0.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test unitaire d'une action qui affiche un formulaire

> Tests unitaires pour une action Post Login qui affiche un formulaire de consentement marketing et enregistre la réponse soumise, en refusant l'accès lorsqu'aucune réponse n'est fournie, à l'aide de Jest, Mocha et du Node.js Test Runner, en JavaScript et en TypeScript.

<h2 id="project">
  Projet
</h2>

Dans chaque exemple, le code source de l'action se trouve sous `src/` et ses tests unitaires sous `test/`, selon la même structure pour les projets JavaScript et TypeScript.

<Tabs>
  <Tab title="JavaScript">
    <Tree>
      <Tree.Folder name="/" defaultOpen>
        <Tree.Folder name="src" />

        <Tree.Folder name="test" />
      </Tree.Folder>
    </Tree>
  </Tab>

  <Tab title="TypeScript">
    <Tree>
      <Tree.Folder name="/" defaultOpen>
        <Tree.Folder name="src" />

        <Tree.Folder name="test">
          <Tree.Folder name="test-utils" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree>
  </Tab>
</Tabs>

<h2 id="action">
  Action
</h2>

L'action Post Login suivante affiche un formulaire de consentement marketing aux utilisateurs qui n'y ont pas encore répondu, puis traite la réponse soumise lors de la tentative de connexion suivante : elle refuse l'accès si le formulaire a été ignoré, sinon elle enregistre le choix de consentement dans `user_metadata`.

<Tabs>
  <Tab title="JavaScript">
    ```js title="mock-form-render.js" theme={null}
    /** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */

    const CONSENT_FORM_ID = 'marketing-consent-form';

    /**
    * Gestionnaire appelé pendant l’exécution d’un flux PostLogin.
    * Affiche un formulaire personnalisé demandant le consentement à des communications marketing si l’utilisateur n’a pas
    * encore répondu. `api.prompt.render` ne peut pas être combiné à
    * `api.redirect.sendUserTo` dans la même exécution : utilisez-en un seul par action.
    *
    * @param {Event} event - Informations sur l’utilisateur et le contexte dans lequel il s’authentifie.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onExecutePostLogin = async (event, api) => {
      const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';

      if (hasAnsweredConsent) {
        return;
      }

      api.prompt.render(CONSENT_FORM_ID, {
        vars: { firstName: event.user.given_name ?? event.user.name },
      });
    };

    /**
    * Gestionnaire appelé lorsque l’utilisateur soumet le formulaire affiché par
    * onExecutePostLogin.
    *
    * @param {Event} event - Informations sur l’utilisateur et le contexte dans lequel il s’authentifie.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onContinuePostLogin = async (event, api) => {
      const consent = event.prompt?.fields?.consent;

      if (typeof consent !== 'boolean') {
        api.access.deny('Marketing consent response was not submitted.');
        return;
      }

      api.user.setUserMetadata('marketing_consent', consent);
    };

    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts title="mock-form-render.ts" theme={null}
    import type { Event, PostLoginAPI } from '@auth0/actions/post-login/v3';

    const CONSENT_FORM_ID = 'marketing-consent-form';

    /**
    * Gestionnaire appelé lors de l'exécution d'un flux PostLogin.
    * Affiche un Form personnalisé demandant le consentement marketing lorsque
    * l'utilisateur n'y a pas encore répondu. `api.prompt.render` ne peut pas être
    * combiné à `api.redirect.sendUserTo` dans une même exécution – choisissez-en
    * un seul par action.
    *
    * @param {Event} event - Détails sur l'utilisateur et le contexte dans lequel il se connecte.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onExecutePostLogin = async (event: Event, api: PostLoginAPI) => {
      const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';

      if (hasAnsweredConsent) {
        return;
      }

      api.prompt.render(CONSENT_FORM_ID, {
        vars: { firstName: event.user.given_name ?? event.user.name },
      });
    };

    /**
    * Gestionnaire appelé lorsque l'utilisateur soumet le Form affiché par
    * onExecutePostLogin.
    *
    * @param {Event} event - Détails sur l'utilisateur et le contexte dans lequel il se connecte.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onContinuePostLogin = async (event: Event, api: PostLoginAPI) => {
      const consent = event.prompt?.fields?.consent;

      if (typeof consent !== 'boolean') {
        api.access.deny('Marketing consent response was not submitted.');
        return;
      }

      api.user.setUserMetadata('marketing_consent', consent);
    };

    ```
  </Tab>
</Tabs>

<h2 id="unit-test">
  Unit Test
</h2>

Les tests unitaires simulent les objets `event` et `api` afin de vérifier que le formulaire s'affiche uniquement tant qu'aucune réponse de consentement n'a été donnée, et que les soumissions de consentement, qu'elles soient acceptées ou refusées, sont enregistrées correctement, tandis que les soumissions manquantes sont rejetées.

<AccordionGroup>
  <Accordion title="Jest">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-form-render.spec.js" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.js');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            jest.resetAllMocks();
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            jest.spyOn(api.prompt, 'render');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            });
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).not.toHaveBeenCalled();
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).not.toHaveBeenCalled();
          });
        });

        describe('onContinuePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            jest.resetAllMocks();
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            jest.spyOn(api.access, 'deny');
            jest.spyOn(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
            expect(api.user.setUserMetadata).not.toHaveBeenCalled();
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
            expect(api.access.deny).not.toHaveBeenCalled();
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
            expect(api.access.deny).not.toHaveBeenCalled();
          });
        });

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-jest",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Jest",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "jest"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "jest": "^30.4.2"
          },
          "jest": {
            "testEnvironment": "node"
          }
        }

        ```

        Configurez `jsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-form-render.test.ts" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');
        const { compileActionModules } = require('./test-utils/load-compiled-action');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.ts');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            jest.resetAllMocks();
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            jest.spyOn(api.prompt, 'render');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            });
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).not.toHaveBeenCalled();
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            expect(api.prompt.render).not.toHaveBeenCalled();
          });
        });

        describe('onContinuePostLogin', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            jest.resetAllMocks();
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            jest.spyOn(api.access, 'deny');
            jest.spyOn(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
            expect(api.user.setUserMetadata).not.toHaveBeenCalled();
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
            expect(api.access.deny).not.toHaveBeenCalled();
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
            expect(api.access.deny).not.toHaveBeenCalled();
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        import * as fs from 'fs';
        import * as os from 'os';
        import * as path from 'path';
        import * as ts from 'typescript';

        export interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute au moyen de vm.compileFunction; il ne passe donc jamais par la
         * transformation TS de ts-node/Vitest. Les sources d'actions (et tout module
         * enregistré avec actions:) doivent d'abord être transpilées en JS ordinaire sur le disque.
         */
        export function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        }

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-jest",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Jest and TypeScript",
          "main": "example.ts",
          "scripts": {
            "test": "jest"
          },
          "author": "Auth0",
          "license": "MIT",
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/jest": "^29.5.12",
            "@types/node": "22.14.0",
            "jest": "^29.7.0",
            "ts-jest": "^29.1.2",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez Jest pour qu'il compile le TypeScript à l'aide de `jest.config.js` :

        ```js title="jest.config.js" theme={null}
        module.exports = {
          preset: 'ts-jest',
          testEnvironment: 'node',
        };
        ```

        Configurez `tsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Mocha">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-form-render.spec.js" theme={null}
        const sinon = require('sinon');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.js');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            sinon.spy(api.prompt, 'render');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            });
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.notCalled(api.prompt.render);
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.notCalled(api.prompt.render);
          });
        });

        describe('onContinuePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            sinon.spy(api.access, 'deny');
            sinon.spy(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
            sinon.assert.notCalled(api.user.setUserMetadata);
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
            sinon.assert.notCalled(api.access.deny);
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
            sinon.assert.notCalled(api.access.deny);
          });
        });

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-mocha",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Mocha",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "mocha"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "chai": "^4.5.0",
            "mocha": "^11.0.0",
            "sinon": "^19.0.0"
          }
        }

        ```

        Configurez Mocha à l'aide de `.mocharc.json` :

        ```json title=".mocharc.json" theme={null}
        {
          "spec": "src/**/*.spec.js"
        }

        ```

        Configurez `jsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-form-render.test.ts" theme={null}
        import * as path from 'path';
        import sinon from 'sinon';
        import { compileActionModules } from './test-utils/load-compiled-action';

        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.ts');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            sinon.spy(api.prompt, 'render');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            });
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.notCalled(api.prompt.render);
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            sinon.assert.notCalled(api.prompt.render);
          });
        });

        describe('onContinuePostLogin', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            sinon.spy(api.access, 'deny');
            sinon.spy(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
            sinon.assert.notCalled(api.user.setUserMetadata);
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
            sinon.assert.notCalled(api.access.deny);
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
            sinon.assert.notCalled(api.access.deny);
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        import * as fs from 'fs';
        import * as os from 'os';
        import * as path from 'path';
        import * as ts from 'typescript';

        export interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute au moyen de vm.compileFunction : il ne passe donc jamais par la transformation
         * TS propre à ts-node/Vitest. Les sources des actions (et tout module enregistré avec
         * actions:) doivent d'abord être transpilées en JS pur sur le disque.
         */
        export function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        }

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-mocha",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Mocha and TypeScript",
          "license": "MIT",
          "author": "Auth0",
          "scripts": {
            "test": "NODE_OPTIONS=--no-experimental-strip-types mocha"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/chai": "^4.3.16",
            "@types/mocha": "^10.0.6",
            "@types/node": "22.14.0",
            "@types/sinon": "^17.0.3",
            "chai": "^4.5.0",
            "mocha": "^11.0.0",
            "sinon": "^19.0.0",
            "ts-node": "^10.9.2",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez Mocha à l'aide de `.mocharc.json` :

        ```json title=".mocharc.json" theme={null}
        {
          "require": "ts-node/register",
          "extension": ["ts"],
          "spec": "src/**/*.test.ts"
        }

        ```

        Configurez `tsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Node.js Test Runner">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-form-render.spec.js" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.js');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            mock.method(api.prompt, 'render');
          });

          afterEach(() => {
            mock.reset();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            }]);
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            assert.strictEqual(api.prompt.render.mock.calls.length, 0);
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            assert.strictEqual(api.prompt.render.mock.calls.length, 0);
          });
        });

        describe('onContinuePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            mock.method(api.access, 'deny');
            mock.method(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            mock.reset();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
            assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });
        });

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-node-test",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using the Node.js built-in test runner",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "node --test src/*.spec.js"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0"
          }
        }

        ```

        Configurez `jsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-form-render.test.ts" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');
        const { compileActionModules } = require('./test-utils/load-compiled-action.ts');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-form-render.ts');
        const CONSENT_FORM_ID = 'marketing-consent-form';

        describe('onExecutePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            mock.method(api.prompt, 'render');
          });

          afterEach(() => {
            mock.reset();
          });

          it('renders the consent form when the user has not answered yet', async () => {
            await loader.execute('onExecutePostLogin', event, api);

            assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
              vars: { firstName: event.user.given_name },
            }]);
          });

          it('skips rendering when the user has already answered', async () => {
            event.user.user_metadata.marketing_consent = true;

            await loader.execute('onExecutePostLogin', event, api);

            assert.strictEqual(api.prompt.render.mock.calls.length, 0);
          });

          it('skips rendering when the user has already declined', async () => {
            event.user.user_metadata.marketing_consent = false;

            await loader.execute('onExecutePostLogin', event, api);

            assert.strictEqual(api.prompt.render.mock.calls.length, 0);
          });
        });

        describe('onContinuePostLogin', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            mock.method(api.access, 'deny');
            mock.method(api.user, 'setUserMetadata');
          });

          afterEach(() => {
            mock.reset();
          });

          it('denies access when the form response is missing', async () => {
            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
            assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
          });

          it('stores an accepted consent answer submitted by the form', async () => {
            event.prompt.fields.consent = true;

            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });

          it('stores a declined consent answer submitted by the form', async () => {
            event.prompt.fields.consent = false;

            await loader.execute('onContinuePostLogin', event, api);

            assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        const fs = require('fs');
        const os = require('os');
        const path = require('path');
        const ts = require('typescript');

        interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute avec vm.compileFunction; il ne passe donc jamais par le retrait
         * natif des types TS de node. Les sources d'actions (et tout module enregistré
         * avec actions:) doivent d'abord être transpilées en JS pur sur le disque.
         */
        exports.compileActionModules = function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m: ModuleToCompile) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        };

        ```

        Ajoutez les dépendances de test au fichier `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-node-test",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using the Node.js built-in test runner and TypeScript",
          "license": "MIT",
          "author": "Auth0",
          "scripts": {
            "test": "node --test src/*.test.ts"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/node": "22.14.0",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez `tsconfig.json` pour que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>
