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

# フォームをレンダリングするアクションのユニットテスト

> マーケティング同意フォームをレンダリングして送信された回答を保存し、回答がない場合はアクセスを拒否する Post Login アクションのユニットテストを、JavaScript と TypeScript で Jest、Mocha、Node.js テストランナー を使って作成します。

## プロジェクト

各サンプルでは、アクションのソースコードを `src/` に、そのユニットテストを `test/` に分けて配置しており、JavaScript プロジェクトと TypeScript プロジェクトのいずれも同じ構造になっています。

> [tab] JavaScript
>
> **/**
>
> **src**
>
> **test**

> [tab] TypeScript
>
> **/**
>
> **src**
>
> **test**
>
> **test-utils**

## アクション

次の Post Login アクションは、マーケティング同意フォームにまだ回答していないユーザーに対してそのフォームを表示し、次回の login 試行時に送信された回答を処理します。フォームがスキップされた場合はアクセスを拒否し、それ以外の場合は同意の選択内容を `user_metadata` に保存します。

> [tab] 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';
>
> /**
> * PostLogin フローの実行中に呼び出される Handler です。
> * ユーザーがマーケティングの同意にまだ回答していない場合に、同意を求める
> * カスタム Form を表示します。`api.prompt.render` と
> * `api.redirect.sendUserTo` は同一の実行内で併用できません。アクションごとにどちらか一方を選んでください。
> *
> * @param {Event} event - ユーザーと、ログインが行われるコンテキストに関する詳細情報。
> * @param {PostLoginAPI} api - ログインの動作を変更するメソッドを提供するインターフェイス。
> */
> 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 },
>   });
> };
>
> /**
> * onExecutePostLogin で表示した Form をユーザーが送信したときに
> * 呼び出される Handler です。
> *
> * @param {Event} event - ユーザーと、ログインが行われるコンテキストに関する詳細情報。
> * @param {PostLoginAPI} api - ログインの動作を変更するメソッドを提供するインターフェイス。
> */
> 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] 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';
>
> /**
> * PostLogin フローの実行中に呼び出される Handler です。
> * ユーザーがマーケティングの同意にまだ回答していない場合、同意を求めるカスタム Form を
> * レンダリングします。`api.prompt.render` は同一の実行内で
> * `api.redirect.sendUserTo` と併用できません。アクションごとにどちらか一方を選んでください。
> *
> * @param {Event} event - ユーザーと、login が行われるコンテキストに関する詳細情報。
> * @param {PostLoginAPI} api - 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 },
>   });
> };
>
> /**
> * onExecutePostLogin がレンダリングした Form をユーザーが送信したときに
> * 呼び出される Handler です。
> *
> * @param {Event} event - ユーザーと、login が行われるコンテキストに関する詳細情報。
> * @param {PostLoginAPI} api - 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);
> };
>
> ```

## ユニットテスト

ユニットテストでは `event` と `api` オブジェクトをモックし、同意が未回答の場合にのみフォームが表示されること、承諾・拒否いずれの同意の送信も正しく保存され、送信内容が欠けている場合は拒否されることを検証します。

> [accordion] Jest
>
> > [tab] 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();
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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();
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルへコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test 提供）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction 経由で実行するため、ts-node や Vitest 自体の TS 変換を
> >  * 一切経由しません。したがって、アクションのソース（および actions: で登録された
> >  * モジュール）は、あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > }
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `jest.config.js` で、TypeScript をコンパイルするように Jest を設定します。
> >
> > ```js title="jest.config.js" theme={null}
> > module.exports = {
> >   preset: 'ts-jest',
> >   testEnvironment: 'node',
> > };
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```

> [accordion] Mocha
>
> > [tab] 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);
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `.mocharc.json` で Mocha を設定します。
> >
> > ```json title=".mocharc.json" theme={null}
> > {
> >   "spec": "src/**/*.spec.js"
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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);
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルへコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test 提供）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction で実行するため、ts-node や Vitest 自身の TS 変換を経由しません。
> >  * そのため、アクションのソース（および actions: で登録されたモジュール）は、
> >  * あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > }
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `.mocharc.json` で Mocha を設定します。
> >
> > ```json title=".mocharc.json" theme={null}
> > {
> >   "require": "ts-node/register",
> >   "extension": ["ts"],
> >   "spec": "src/**/*.test.ts"
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```

> [accordion] Node.js テストランナー
>
> > [tab] 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);
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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);
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルへコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test のもの）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction で実行するため、node ネイティブの TS 型除去を経由することはありません。
> >  * そのため、アクションのソース（および actions: で登録されたモジュール）は、
> >  * あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > };
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```
