Testing & Simulation

Every state your host page has to handle - an empty feed, each error phase, a widget that never says anything - normally requires an actual outage to observe. You cannot make our backend fail on demand, so without help you would ship a listener you have never once seen run.

The widget ships a built-in simulation harness for exactly this. Add a simulate* parameter to your embed URL and the widget induces the state for you: the same code path that handles a real failure runs, builds the same payload, and emits the same event to your page. Your integration reacts to a genuine WIDGET_ERROR, not a mock.

It is live on every deployment, including production - nothing needs to be enabled on our side. The whole point is that you test against your own live embed, on your own origin, before launch.

Try it right now: demo.odditt.com/?simulateError=network - a real embed with an induced initial failure. Note the SIMULATED badge in the corner:

How it works

The parameters do not paint fake UI. Each one intervenes at the real fetch seam - the initial page load, the pricing check, a filter-change refetch, pagination - and either fails it, empties it, or slows it down. Everything downstream of that seam is the production code path, so what your listener receives is byte-for-byte what a real incident would send it.

Guardrails, so a simulation can never be mistaken for - or contaminate - the real thing:

  • A SIMULATED badge renders on the widget whenever any simulation is active, labeled with what is being simulated (for example SIMULATED: loadMore 503). An induced 500 looks exactly like a real one otherwise; the badge is what keeps a test from being reported as an outage. A console warning says the same thing.
  • Events fired by simulateEvent carry simulated: true in their payload. Drop any event with that flag before it reaches your analytics or reporting.
  • The simulated BET_CLICKED is deliberately unusable for attribution. Its identifiers are synthetic (flowId: -1, handoffId: "fun_flow:-1", a placeholder buttonPayload) so that even if a host ignored the simulated flag, the values can never match a real record on either side.
  • Simulated sessions are excluded from our own telemetry. A simulate* session is you validating your integration, not user behavior - nothing from it enters our logs or any reporting.
  • None of these parameters ever reach our API. They are widget-local and are stripped before any backend request is built.

Parameters are read once per page load. To change a scenario, change the URL and let the iframe reload - toggling them without a navigation has no effect.

Parameter reference

All parameters go on the embed URL, alongside your normal ones. Booleans accept true or 1.

ParameterTypeWhat it does
simulateEmptybooleanThe initial load returns zero flows. Produces WIDGET_EMPTY.
simulateNoResultsbooleanForces the in-widget "No Results Found" state, without needing a filter combination that actually has none. Emits no event - this state is internal to the widget.
simulateErrorstringFails a fetch. Pass an HTTP status (500, 503, 422) or network for a request that never got a response - the status: null case, and the one hosts most often forget to branch on.
simulatePhasestringWhich fetch simulateError fails: initial (default), quote, retry, or loadMore. Sets the phase on the resulting WIDGET_ERROR.
simulateMessagestringOverrides the message on the simulated error, in both the event and the on-widget overlay. Capped at 200 characters.
simulateSilentbooleanThe widget loads but emits nothing at all, including WIDGET_READY.
simulateDelaynumberAdds N milliseconds of latency to the flows fetch, so WIDGET_READY arrives late.
simulateEventstringEmits one well-formed contract event on load: WIDGET_READY, WIDGET_EMPTY, BET_CLICKED, PAGE_LOADED, FILTER_CHANGED, GRAPH_EXPANDED, or GRAPH_COLLAPSED.

Inducing failures

simulateError picks what kind of failure; simulatePhase picks which fetch it happens to. They are independent on purpose: ?simulateError=503&simulatePhase=loadMore is a non-fatal error carrying a 5xx status, and that combination is the single most valuable test on this page. If your handler escalates on status >= 500 instead of branching on phase, it will hide a perfectly good feed because page three hiccuped - and this is how you find out before your users do.

The four phases, and how to trigger each one:

simulatePhaseThe fetch it failsHow to make it fireWhat you should seeFatal?
initial (default)The first page loadJust load the embedWIDGET_ERROR with phase: "initial" and your chosen status. No cards ever render.Yes - hide the section
quoteThe pricing check that validates cards after they loadLoad the embed (see prerequisites below)Cards render, then pricing invalidates them: phase: "quote", status: null.Yes - hide the section
retryThe refetch after a filter changeChange any filter in the widgetphase: "retry", with the previous cards still on screen.No - keep it visible
loadMorePaginationScroll to the bottom of the feedphase: "loadMore", with a full page of cards on screen.No - keep it visible

Full payload shape and the per-phase fatality contract: Widget Events.

🧪

quote has prerequisites

The quote phase only exists where pricing actually runs: an operator-mode embed with a country parameter, on a deployment with live pricing enabled. Anywhere else there is no quote fetch to fail, so the parameter does nothing - the widget logs a console warning saying so rather than leaving you wondering whether it is broken. Note that status is always null on this phase, whatever you passed to simulateError; that is the documented shape of a real quote failure, and the simulation honors it.

Worked examples:

<!-- The classic mistake, made visible: page one renders, pagination fails
     with a 5xx. Your section must stay on screen. -->
<iframe
  src="https://widget.example.com/?preset=brand_dark_v2&simulateError=503&simulatePhase=loadMore"
  ...
></iframe>
<!-- A request that never got a response: status is null, not a number.
     Handlers that assume status is always numeric fail here. -->
<iframe
  src="https://widget.example.com/?preset=brand_dark_v2&simulateError=network"
  ...
></iframe>

Empty, slow, and silent

Three states that are not failures but still need handling:

  • simulateEmpty=true - the feed genuinely loads and genuinely contains nothing. This emits WIDGET_EMPTY, and your page should collapse the section, heading included. This is different from an error: nothing went wrong, there is just nothing to show.
  • simulateNoResults=true - the widget's own "No Results Found" state, which users normally reach through a filter combination with no matches. It renders inside the widget and emits nothing, so there is nothing for your listener to do - this one is for checking how the state looks inside your layout.
  • simulateDelay=4000 - WIDGET_READY arrives about four seconds late. If you built the recommended reveal pattern (keep the section hidden until WIDGET_READY, with a timeout), this verifies the happy-but-slow path stays inside your timeout.
  • simulateSilent=true - the widget loads and says nothing, ever. This is the test for the reveal timeout itself: if your page still shows a blank block after your timeout elapses, the timeout is not wired. There is no event to catch here - by design.

Firing a single event

Most contract events need real user behavior to observe - a card someone actually taps for BET_CLICKED, a deep scroll for PAGE_LOADED. simulateEvent emits one well-formed event immediately on load so each handler can be verified in isolation:

?simulateEvent=BET_CLICKED

The payload has the exact production shape, plus simulated: true. Verify two things at once: that your handler fires, and that the simulated flag keeps the event out of your analytics and attribution - a simulated BET_CLICKED must never count as a real click anywhere in your reporting.

The pre-launch run

The Integration Recipes guide has the full checklist: a table of embed URLs to work through with your listener attached, each with the exact event you should observe and whether your section should collapse or stay visible. Running that table top to bottom is the closest thing to linting your integration - every state your handlers claim to cover, actually observed firing, before a single real user sees the embed.

A few to try against the live demo right now:

What simulation cannot cover

One failure mode happens before the widget can run any code: the iframe never booting because the embedding origin is not registered with us. No parameter can reproduce that, because there is no widget awake to simulate it. Test it from your side - load your embed page from the real production origin before launch, and re-test whenever the embedding domain changes. The full write-up, including the timeout pattern that catches it: The failure no event can report.

If you want simulation disabled on your production embed once you are live, ask your Odditt contact - it can be turned off per deployment.


Did this page help you?