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
SIMULATEDbadge renders on the widget whenever any simulation is active, labeled with what is being simulated (for exampleSIMULATED: 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
simulateEventcarrysimulated: truein their payload. Drop any event with that flag before it reaches your analytics or reporting. - The simulated
BET_CLICKEDis deliberately unusable for attribution. Its identifiers are synthetic (flowId: -1,handoffId: "fun_flow:-1", a placeholderbuttonPayload) so that even if a host ignored thesimulatedflag, 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.
| Parameter | Type | What it does |
|---|---|---|
simulateEmpty | boolean | The initial load returns zero flows. Produces WIDGET_EMPTY. |
simulateNoResults | boolean | Forces 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. |
simulateError | string | Fails 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. |
simulatePhase | string | Which fetch simulateError fails: initial (default), quote, retry, or loadMore. Sets the phase on the resulting WIDGET_ERROR. |
simulateMessage | string | Overrides the message on the simulated error, in both the event and the on-widget overlay. Capped at 200 characters. |
simulateSilent | boolean | The widget loads but emits nothing at all, including WIDGET_READY. |
simulateDelay | number | Adds N milliseconds of latency to the flows fetch, so WIDGET_READY arrives late. |
simulateEvent | string | Emits 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:
simulatePhase | The fetch it fails | How to make it fire | What you should see | Fatal? |
|---|---|---|---|---|
initial (default) | The first page load | Just load the embed | WIDGET_ERROR with phase: "initial" and your chosen status. No cards ever render. | Yes - hide the section |
quote | The pricing check that validates cards after they load | Load the embed (see prerequisites below) | Cards render, then pricing invalidates them: phase: "quote", status: null. | Yes - hide the section |
retry | The refetch after a filter change | Change any filter in the widget | phase: "retry", with the previous cards still on screen. | No - keep it visible |
loadMore | Pagination | Scroll to the bottom of the feed | phase: "loadMore", with a full page of cards on screen. | No - keep it visible |
Full payload shape and the per-phase fatality contract: Widget Events.
quotehas prerequisitesThe
quotephase only exists where pricing actually runs: an operator-mode embed with acountryparameter, 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 thatstatusis alwaysnullon this phase, whatever you passed tosimulateError; 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 emitsWIDGET_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_READYarrives about four seconds late. If you built the recommended reveal pattern (keep the section hidden untilWIDGET_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:
?simulateEmpty=true- a feed with nothing to show?simulateError=503&simulatePhase=loadMore- scroll to the bottom; the failure arrives with cards on screen?simulateNoResults=true- the in-widget no-results state?simulateEvent=BET_CLICKED- open the console to see the emitted event
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.
Updated about 2 months ago

