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 theopen/sendboundary 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 (default200).body— inline JSON body. Mutually exclusive withfixture.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:
- The runtime installs the mock list at the
mockNetworkstep. - Mocks accumulate across multiple
mockNetworksteps in the same flow — each call extends the list. - 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.
Prose
# 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.yamlThe 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 landsUntil 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.