Records HTTP interactions exercised through @web/mocks from Web Test Runner tests and Storybook stories (WDS or Vite builder), generating OpenAPI specs (default) or HAR files.

View on GithubNew to Storybook?Get started

web-mocks-recorder

Records HTTP interactions exercised through @web/mocks and, by default, generates an OpenAPI 3 spec for each producer. An optional HAR 1.2 file is also available when har: true. Web Test Runner (WTR) and Storybook are the supported sources. Pact generation and publication come later and will reuse the model in src/model.js.

Install

npm install --save-dev web-mocks-recorder

Usage (WTR)

web-test-runner.config.mjs

import { mockPlugin } from '@web/mocks/plugins.js';
import { recorderPlugin } from 'web-mocks-recorder/plugin.js';

export default {
  nodeResolve: true,
  plugins: [mockPlugin(), recorderPlugin({ outputDir: '.interactions', pathSegments: 1, openApi: true, har: false })],
};

pathSegments (default 1) controls how many leading URL path segments are folded into each producer's grouping key — see Output. openApi (default true) controls whether an OpenAPI 3 document is generated for every producer — see OpenAPI specs. har (default false) controls whether a HAR 1.2 file is also written per producer — see HAR files.

Test file (one line added; the rest of the test is unchanged):

import { registerMockRoutes, worker } from '@web/mocks/browser.js';
import { recordInteractions } from 'web-mocks-recorder/wtr.js';

recordInteractions(worker);

Call it at the test file's top level — not inside a hook. recordInteractions() registers its own before/afterEach hooks, so calling it from inside e.g. a before() hook means those hooks are never scheduled. That setup fails completely silently: tests pass, no warning is logged, and no HAR file is written.

Opt-in switch: recording runs only with WEB_MOCKS_RECORDER=1 (or recorderPlugin({ enabled: true })). When disabled, no listener is attached, nothing is written and no directory is created.

Splitting by producer: interactions recorded within a single test file are not all merged into one document. They are grouped by producer — the request URL's hostname plus its first pathSegments path segments (default 1) — so requests made to different backends/APIs (or different top-level routes on the same backend) land in separate OpenAPI specs (and HAR files, when enabled). The port is ignored when grouping, since dev servers often get a randomly assigned port. Tune the grouping granularity with recorderPlugin({ pathSegments: 2 }) (0 groups by hostname only); unparsable URLs fall into a single unknown bucket rather than being dropped.

Usage (Storybook)

Prerequisites: a Storybook using either the @web/storybook-builder (for example @web/storybook-framework-web-components, with @web/storybook-addon-mocks) or the Vite builder (for example @storybook/web-components-vite, registering mocks with registerMockRoutes from @web/mocks/browser.js). The addon registers its dev-server middleware through wdsFinal for WDS and viteFinal for Vite; the same configuration works for both.

.storybook/main.js

export default {
  addons: [
    '@web/storybook-addon-mocks',
    {
      name: 'web-mocks-recorder/storybook',
      options: { outputDir: '.interactions', pathSegments: 1, openApi: true, har: false },
    },
  ],
};

The plain string form 'web-mocks-recorder/storybook' also works (defaults apply).

  • On by default: registering the addon is the opt-in, so recording starts with storybook dev. Disable it with enabled: false or WEB_MOCKS_RECORDER=0 storybook dev. (WTR remains opt-in via WEB_MOCKS_RECORDER=1.)
  • No story changes needed: the existing parameters.mocks are recorded while you interact with the Storybook UI. Each interaction is attributed to the story that was rendered when the mocked response happened.
  • Flushing: files are written about 500 ms after the last interaction, when you switch to a story in another stories file, and on page unload.
  • Output: <outputDir>/storybook/<stories file path>.<producer>.openapi.json, plus a .har file when har: true. Producer grouping, pathSegments, openApi and har behave as in WTR. HAR files additionally carry log._storiesFile and, on each entry, _story.
  • Single @web/mocks: make sure the Storybook dev server resolves exactly one copy of @web/mocks/browser.js (dedupe it); otherwise the recorder listens to a different worker and records nothing.

Manual setup

Instead of registering the addon preview, add the decorator yourself in .storybook/preview.js:

import { withInteractionRecording } from 'web-mocks-recorder/storybook/preview';

export const decorators = [withInteractionRecording];

Do not register the addon preset in this case, but the middleware must still be registered in .storybook/main.js: through wdsFinal (WDS) or, for Vite, by adding the plugin yourself:

import { storybookRecorderVitePlugin } from 'web-mocks-recorder/storybook/vite';

export default {
  viteFinal: (config) => ({
    ...config,
    plugins: [...(config.plugins ?? []), storybookRecorderVitePlugin({ outputDir: '.interactions' })],
  }),
};

Limitations

  • Dev server only: storybook build / static Storybook writes nothing.
  • Only the WDS and Vite builders are supported; webpack is not.
  • In Docs pages several stories render in one iframe, so _story may name the last rendered story.
  • Revisiting a story appends duplicate HAR entries during the same dev-server session.
  • Mocks edited through the addon panel trigger a page reload and are recorded with the edited responses.

Output

<outputDir>/wtr/<test file path>.<producer>.openapi.json, one OpenAPI 3 document per test file per producer (plus a <...>.har file when har: true); runs with zero interactions write nothing. For example, a test file that calls both https://orders.example.com/api/... and https://users.example.com/api/... produces:

.interactions/wtr/test/checkout.test.js.orders.example.com-api.openapi.json
.interactions/wtr/test/checkout.test.js.users.example.com-api.openapi.json

… and, with har: true, additionally:

.interactions/wtr/test/checkout.test.js.orders.example.com-api.har
.interactions/wtr/test/checkout.test.js.users.example.com-api.har

OpenAPI spec

By default each producer gets a standalone OpenAPI 3 document (generated by har-to-openapi from an in-memory HAR of that producer's interactions). It contains the observed paths, methods, and request/response schemas, and carries an info block that names the producer host and test file as the source. Set openApi: false to skip spec generation.

Example spec for a test that exercised GET /api/users/123 against http://localhost:8000 (run with pathSegments: 2):

{
  "openapi": "3.0.0",
  "info": {
    "title": "localhost/api/users API",
    "version": "1.0.0",
    "description": "OpenAPI spec generated by web-mocks-recorder from @web/mocks interactions recorded by \"test/mocks.test.js\" on 2026-10-25T08:04:30.624Z."
  },
  "paths": {
    "/api/users/123": {
      "get": {
        "description": "",
        "operationId": "getApiUsersById",
        "parameters": [],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": { "id": "123" },
                "schema": {
                  "additionalProperties": false,
                  "properties": { "id": { "format": "integer", "type": "string" } },
                  "required": [],
                  "title": "APIUsersByIDGetResponse",
                  "type": "object"
                }
              }
            },
            "description": "",
            "headers": {}
          }
        },
        "summary": "Get Api Users By ID"
      }
    }
  },
  "servers": [{ "url": "http://localhost:8000" }]
}

Note that the schema is inferred from the recorded response body — e.g. the "123" in the mock response becomes a string property with an integer format hint, and the response schema gets a generated title. Schemas, operationId and summary naming follow har-to-openapi's conventions.

HAR file

With har: true, the same interactions are also persisted as a HAR 1.2 file. Each HAR document carries a _producer: { host, pathPrefix } extension field describing which producer it contains.

{
  "log": {
    "version": "1.2",
    "creator": { "name": "web-mocks-recorder", "version": "0.1.0" },
    "entries": [{
      "startedDateTime": "ISO-8601",
      "time": -1,
      "request": {
        "method": "POST", "url": "…", "httpVersion": "",
        "cookies": [], "headers": [{ "name": "content-type", "value": "application/json" }],
        "queryString": [], "headersSize": -1, "bodySize": 5,
        "postData": { "mimeType": "application/json", "text": "{…}" }
      },
      "response": {
        "status": 201, "statusText": "", "httpVersion": "",
        "cookies": [], "headers": [], "content": { "size": -1, "mimeType": "" },
        "redirectURL": "", "headersSize": -1, "bodySize": -1
      },
      "cache": {},
      "timings": { "blocked": -1, "dns": -1, "connect": -1, "send": -1, "wait": -1, "receive": -1, "ssl": -1 },
      "_diagnostics": [{ "code": "UNSUPPORTED_PAYLOAD", "target": "response.body", "message": "…" }]
    }],
    "_executionId": "…", "_source": "wtr", "_testFile": "test/a.test.js",
    "_producer": { "host": "api.example.com", "pathPrefix": "/orders" }
  }
}
  • HTTP interactions use the standard HAR 1.2 request/response structure. Recorder metadata and diagnostics use HAR-compatible extension fields prefixed with _.
  • Header names are lower-cased and sorted. Values already comma-combined by Headers remain combined; separate Set-Cookie values are represented as separate HAR header records where supported.
  • Absent bodies are omitted from HAR text fields; empty bodies remain empty strings. Recorded bodies are stored verbatim as text.
  • HAR timing, HTTP version and header-size values unavailable to the recorder are marked -1 or left empty.
  • Non-text content types or non-UTF-8 bytes are not recorded: the HAR text field is omitted, size is marked unknown, a _diagnostics extension is added and a console.warn is emitted. Payload contents are never included in diagnostics.

If the OpenAPI conversion fails, the run is not failed and a diagnostic is recorded (in the HAR's _diagnostics extension when a HAR is written). The OpenAPI spec is generated from the same in-memory HAR document used for the HAR file, so enabling har has no effect on the spec's content.

Behaviour guarantees

The recorder listens passively to MSW's response:mocked event and reads clones. Its errors become diagnostics and never fail a test.

Open decisions (not decided here)

Output naming policy, empty-recording policy (currently: no file), redaction of sensitive headers/bodies (currently: none applied), binary encoding.

Tests

npm test runs unit tests and integration tests that drive real WTR runs and the Storybook recording flow (no Storybook install needed) in Chromium (npx playwright install chromium first).