> ## Documentation Index
> Fetch the complete documentation index at: https://docs-dev-actions-modules-ga.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Unit Test an Action That Renders a Form

> Unit tests for a Post Login action that renders a marketing consent Form and stores the submitted answer, denying access when no answer is submitted, using Jest, Mocha, and Node.js Test Runner in JavaScript and TypeScript.

## Action

The following Post Login action renders a marketing consent Form to users who haven't answered it yet, then handles the submitted response on the following login attempt: denying access if the Form was skipped, or storing the consent choice in `user_metadata` otherwise.

<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';

    /**
    * Handler that will be called during the execution of a PostLogin flow.
    * Renders a custom Form asking for marketing consent when the user hasn't
    * answered it yet. `api.prompt.render` cannot be combined with
    * `api.redirect.sendUserTo` in the same execution - pick one per action.
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
    */
    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 },
      });
    };

    /**
    * Handler that will be called when the user submits the Form rendered by
    * onExecutePostLogin.
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
    */
    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';

    /**
    * Handler that will be called during the execution of a PostLogin flow.
    * Renders a custom Form asking for marketing consent when the user hasn't
    * answered it yet. `api.prompt.render` cannot be combined with
    * `api.redirect.sendUserTo` in the same execution - pick one per action.
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
    */
    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 },
      });
    };

    /**
    * Handler that will be called when the user submits the Form rendered by
    * onExecutePostLogin.
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
    */
    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>

## Unit Test

The unit tests mock the `event` and `api` objects to verify the Form renders only when consent hasn't been answered, and that both accepted and declined consent submissions are stored correctly while missing submissions are denied.

<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();
          });
        });

        ```

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

        ```

        ```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();
          });
        });

        ```

        ```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() (from @auth0/actions/*\/test) reads its target file from disk and
         * runs it via vm.compileFunction, so it never goes through ts-node/Vitest's own
         * TS transform. Action sources (and any actions:-registered modules) must be
         * transpiled to plain JS on disk first.
         */
        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 };
        }

        ```

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-jest",
          "version": "1.0.0",
          "description": "Actions TS",
          "main": "example.ts",
          "scripts": {
            "test": "jest"
          },
          "author": "John Doe",
          "license": "ISC",
          "devDependencies": {
            "@auth0/actions": "^0.32.0",
            "@types/jest": "^29.5.12",
            "@types/node": "22.14.0",
            "jest": "^29.7.0",
            "ts-jest": "^29.1.2",
            "typescript": "^5.9.2"
          }
        }

        ```

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

        ```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);
          });
        });

        ```

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

        ```

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

        ```

        ```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);
          });
        });

        ```

        ```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() (from @auth0/actions/*\/test) reads its target file from disk and
         * runs it via vm.compileFunction, so it never goes through ts-node/Vitest's own
         * TS transform. Action sources (and any actions:-registered modules) must be
         * transpiled to plain JS on disk first.
         */
        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 };
        }

        ```

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-mocha",
          "version": "1.0.0",
          "description": "",
          "license": "ISC",
          "author": "",
          "scripts": {
            "test": "NODE_OPTIONS=--no-experimental-strip-types mocha"
          },
          "devDependencies": {
            "@auth0/actions": "^0.32.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"
          }
        }

        ```

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

        ```

        ```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);
          });
        });

        ```

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-node-test",
          "version": "1.0.0",
          "description": "",
          "license": "ISC",
          "author": "",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "node --test src/*.spec.js"
          },
          "devDependencies": {
            "@auth0/actions": "^0.32.0"
          }
        }

        ```

        ```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);
          });
        });

        ```

        ```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() (from @auth0/actions/*\/test) reads its target file from disk and
         * runs it via vm.compileFunction, so it never goes through node's native TS type
         * stripping. Action sources (and any actions:-registered modules) must be
         * transpiled to plain JS on disk first.
         */
        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 };
        };

        ```

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-node-test",
          "version": "1.0.0",
          "description": "",
          "license": "ISC",
          "author": "",
          "scripts": {
            "test": "node --test src/*.test.ts"
          },
          "devDependencies": {
            "@auth0/actions": "^0.32.0",
            "@types/node": "22.14.0",
            "typescript": "^5.9.2"
          }
        }

        ```

        ```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>
