Skip to Content
DocsAuthoring flowsNetwork mocking

Network mocking

klera mocks the network without monkey-patching your app. The runtime wraps fetch, XMLHttpRequest, and WebSocket at boot; mock declarations are first-class IR steps. Configure mocks declaratively in prose or YAML, run the flow, and assert against the rendered UI without touching test infrastructure.

How it works

When the runtime registers with the bridge it installs three interceptors:

  • fetch — wrapped with a request-matching shim that consults the active mock list before delegating to the real implementation.
  • XMLHttpRequest — shimmed at the open/send boundary so legacy axios / superagent code paths route through the same matcher.
  • WebSocket — message events can be replayed from a mock spec, though full duplex mocking is power-user territory.

A mock spec matches by method + path. On match the runtime synthesises a response from inline body, a file-backed fixture, or defaults to {}. On miss the request passes through to the real network. Mocks live for the duration of the flow (or until unmockNetwork) and never leak across runs.

┌──────────┐ fetch('/api/feed') ┌────────────┐ │ App │ ───────────────────▶ │ runtime │ match ──▶ synthesised response │ (Hermes) │ │ shim │ └──────────┘ └────────────┘ no match ──▶ real network │ mockNetwork: [...] │ unmockNetwork: true ┌────────────┐ │ flow IR │ └────────────┘

The IR step shapes

Three first-class steps drive the mock layer.

mockNetwork

Install one or more mocks. The argument is an array — one spec per endpoint. See the IR reference for the full schema.

- mockNetwork: - method: GET path: /api/feed status: 200 body: items: - { id: 1, title: 'Welcome to klera' } - { id: 2, title: 'Today is launch day' } - method: POST path: /api/track status: 204 delayMs: 50 # simulate latency headers: x-rate-limit-remaining: '99'

Every spec accepts:

  • method — HTTP verb. '*' matches any method.
  • path — exact-match in v1; glob and regex broaden the matcher in a later wave.
  • status — response code (default 200).
  • body — inline JSON body. Mutually exclusive with fixture.
  • fixture — path to a fixture file relative to the flow directory.
  • headers — response headers map.
  • delayMs — synthetic latency before responding (caps at 60s).

unmockNetwork

Clear all installed mocks (true) or a targeted subset.

- unmockNetwork: true # drop everything - unmockNetwork: - { method: GET, path: /api/feed } - { method: POST, path: /api/track }

Use the targeted form when you want to test the same screen against both mocked and live data within one flow — install, assert, remove specific entries, assert again.

assertNetworkCalled

Assert that a request matching the predicate was observed in the runtime’s per-flow network log. At least one of method or path must be supplied.

- assertNetworkCalled: method: POST path: /api/login - assertNetworkCalled: path: /api/analytics count: 0 # assert nothing went out - assertNetworkCalled: method: POST path: /api/orders bodyContains: { sku: 'WIDGET-1' } headersContain: authorization: 'Bearer'

count defaults to 1 and asserts exact number of matching requests. Supply 0 to assert no matching request occurred — the canonical way to test “we don’t hit analytics on this screen”.

Backing mocks with fixtures

Inline body is fine for one-off scalar responses; reach for fixture when the payload is non-trivial or shared across flows.

# flows/feed.flow.yaml - mockNetwork: - method: GET path: /api/feed fixture: fixtures/feed-success.json
// fixtures/feed-success.json { "items": [ { "id": 1, "title": "Welcome", "publishedAt": "2026-05-01T10:00:00Z" }, { "id": 2, "title": "Launch day", "publishedAt": "2026-05-01T11:00:00Z" } ], "nextCursor": null }

Fixture files participate in the watch-mode dependency set (see watch mode) — saving the JSON file re-runs the flow against the new payload without restarting the runtime.

The same fixture can carry sentinels for credentials and config:

{ "user": { "id": "u_42", "token": "${secret:E2E_TEST_TOKEN}" } }

${secret:...} resolves through the per-run Redactor before the runtime sees it; the rendered network log still shows <redacted:E2E_TEST_TOKEN>.

Interaction with waitForIdle

The runtime’s network idle gate counts in-flight requests through the same wrapped fetch / XHR / WebSocket. Mocked requests count toward the gate — they’re synthesised in-process, but delayMs keeps them in-flight for the simulated duration.

- mockNetwork: - method: GET path: /api/feed status: 200 body: { items: [] } delayMs: 200 - tap: { testID: feed-tab } - waitForIdle: { network: true } # waits for the mock to "complete" - assert: { visible: { testID: empty-state } }

Without delayMs, mocks resolve synchronously and the gate flips back to idle within the quiet window without ever observing in-flight state. That’s usually fine; bump delayMs when you want to assert loading spinners are visible during the mocked request.

Real networks have variance. If you’re testing a loading state, set delayMs to the smallest value that consistently lets you catch the spinner — usually 100–300ms is enough.

Lifecycle and scope

Mocks scope to the flow. Three guarantees:

  1. The runtime installs the mock list at the mockNetwork step.
  2. Mocks accumulate across multiple mockNetwork steps in the same flow — each call extends the list.
  3. The runtime drops every mock at flow end, regardless of pass/fail. No leakage across flows in the same run.

If you need to clear midway through a flow, use unmockNetwork.

A worked example

A flow that mocks the feed endpoint, asserts the rendered list, then removes the mock and asserts the real loading state.

# flows/feed.flow.md Mock GET /api/feed to return the two-item fixture from `fixtures/feed-success.json`. Open the feed tab; wait for the network to settle; assert "Welcome to klera" and "Launch day" are both visible. Take a visual snapshot called "feed-mocked". Then unmock /api/feed and tap the refresh button. Assert the loading spinner appears, then the real feed renders.
klera run flows/feed.flow.yaml

The report carries every captured request — both mocked and real — under each step’s network log. Failures include the full log in the HTML report and as a structured field on the triage block.

Patterns

Stub one endpoint, let the rest pass through

This is the default. Listing GET /api/feed in mockNetwork only intercepts that endpoint; every other request still hits the real backend. Useful when you want to test screen-rendering against a deterministic payload but exercise the real auth / config endpoints.

Stub everything (offline mode)

Install a catchall as the last spec:

- mockNetwork: - { method: GET, path: /api/feed, body: { items: [] } } - { method: '*', path: '*', status: 503 } # belt and braces — once glob lands

Until the glob matcher ships, list every real endpoint your flow touches. This is more work but more honest — you’re documenting your network surface in the flow itself.

Assert no analytics calls

- tap: { testID: privacy-mode-toggle } - waitForIdle: { animations: true } - assertNetworkCalled: { path: /api/analytics, count: 0 }

The most common use of count: 0 — proving a screen doesn’t leak data when a privacy toggle is on.

Next steps

  • Fixtures and secrets — committed test data, ${env:KEY} / ${secret:KEY} sentinels, the redaction contract.
  • IR reference — full schema for mockNetwork, unmockNetwork, assertNetworkCalled.
  • Watch mode — saving a fixture file re-runs the flow against the new payload.
Last updated on