NewAndroid and iOS apps: same tests, same matrix

Drive the UI. Judge everything behind it.

danwo is the open-source testing platform for the agentic era. It clicks through your web and mobile app like a user, while emulated Stripe, Clerk, Resend and twenty more providers record every call, then breaks the run on purpose, once per fault you should survive.

no app code changes0 model calls on replay
$ npx @danwo/cli init
GitHubRead the docs
tests/buy-credits.danwo.ts›buys 500 credits through Stripe Checkout
$ danwo matrix

Fault matrix · one clean run, replayed per fault

running
Trial 0 of 18
held under the faultthe fault broke itnot applicable

Flight record · duplicate webhook

failed
00.4s0.8s1.2s
step /168msclick Buy104msPOST checkout142msINSERT orders9msclick Pay262msGET checkout118mswebhook96mswebhook (dup)dupUPDATE credits×2expect Credits1.0s
invariant: expected 1000 to be 500
granted credits ≠ credits Stripe was paid for
stripe.checkout_sessions+1paid 500ledger.credits+1000granted 2×

Emulated behind an egress proxy. Your app keeps calling the real hostnames.

StripeResendAmazon S3TigrisGoogleMicrosoftSlackGitHubTwilioPaystackFlutterwaveBachsAuth0ClerkNylasNangoUnipilePusherNovuTiptapComposioOpenAIAnthropicGeminiPerplexityBedrockAI Gateway
How it works

One test. Every way it can go wrong.

You write the happy path once. danwo records what it caused behind the app, then replays it under every fault your providers, network and database can throw.

Fig 0.1 · write
test("buys 500 credits", async () => { await app.open("/") await agent.act("buy 500 credits") await expect(credits).toHaveText("500") }).invariant(/* paid = granted */)

Write the test

Steps with locators, or plain English for the agent, plus an invariant: a rule that must hold whatever happened. danwo record can draft both.

Fig 0.2 · emulate and record
your appunchangedegress proxyrecords every call

danwo emulates and records

An egress proxy answers real provider hostnames from emulators. Every call, webhook and statement is logged, and the agent's choices are cached for replay.

Fig 0.3 · break
40 trials2 broke

The matrix breaks it

It replays the clean run once per planned fault: declines, 500s, timeouts, duplicate or forged webhooks, double-submits, Postgres and Redis faults, time travel.

Product

Other tools stop at the screen.
danwo keeps going.

The page said "Thank you". Did Stripe charge once? Did the email send? Did the ledger add up? That is where apps break, and where danwo looks.

Provider emulators

Stateful stand-ins for the 22 providers your app pays for, with real hostnames, signed webhooks, sockets and the error shapes your code has to handle, for your frontend, API and workers together behind one proxy. No sandbox keys, no rate limits, no flakes.

StripeResendAmazon S3TigrisGoogle sign-inGmailCalendarDriveMicrosoftSlackGitHubTwilioPaystackFlutterwaveBachsAuth0ClerkNylasNangoUnipilePusherNovuTiptapComposioOpenAIAnthropicGeminiPerplexityBedrock

Fault matrix

The faults the test's own steps could meet, planned from what they called, grouped and kept within a budget.

decline · card_declinedheld
status 500 · checkoutheld
timeout · resendheld
reset · checkout.stripe.comheld
duplicate · webhookbroke
forged · webhookheld
double-submit · POST /buyheld
pg · COMMIT failsheld
time · +30 daysheld
17 of 18 held · 1 broke the invariant

Replay without model calls

The agent's choices are cached as locators. Reruns are deterministic, fast and free.

0model calls
act cachedassert cachedact cached$0.00

Flight recorder

Steps, screenshots, a provider waterfall, the effects diff and emulator state, secrets redacted.

stepstripewebhookpostgresexpect

Mobile

The same tests, invariants and matrix on React Native and Expo apps for Android and iOS, in-app checkout browsers included.

checkout.stripe.com
Pay $5.00

MCP for coding agents

Claude Code, Cursor and Codex run, explain and write danwo tests through MCP, then fix what the matrix found.

Make checkout survive the matrix
run_matrix tests/buy-credits · 1 of 18 broke
show_run duplicate webhook granted credits twice
The webhook handler had no idempotency check. I keyed it on event.id; the matrix now passes 18 of 18.

GitHub Check and PR comment

Every pull request gets a check run and a comment: what broke, under which fault, with the flight record one click away.

danwo matrix · broke under 1 of 18 faults25.4s
danwo test · 42 passed, 3 cached8.1s
integration lint · idempotency keys present0.2s

Fidelity checks

An emulator is only worth what it matches. danwo compares every emulator with the real API by structure, from the provider's published description and its live answers, so a green run means what it says.

POST /v1/checkout/sessionsmatched live
POST /emailsmatched live
PUT and GET an object on Tigrismatched live
sign-in by emailed codematched live
GET /transaction/verifymatched live
In code

Tests that read like the product.

TypeScript, a Playwright-shaped API, and an invariant that judges the backend rather than the pixels.

  • 1Locators or the agent. Mix screen.getByRole with agent.act; both replay without a model.
  • 2One environment function. Emulators and the proxy, wired into the app by environment variables.
  • 3Run the matrix. Locally, in CI, or from your coding agent.
import { type StripeFixtures, test as base } from "@danwo/sdk"import { grantedCredits, paidCredits } from "../src/ledger.ts"const test = base.extend<StripeFixtures>()test("buys 500 credits through Stripe Checkout", async ({ app, screen, expect }) => {  await app.open("/")  await screen.getByRole("button", { name: "Buy 500 credits" }).click()  await expect(screen.getByTestId("checkout-total")).toContainText("$5.00")  /* Paying calls Stripe; readiness waits for the call and plays the webhook it causes. */  await screen.getByTestId("checkout-pay").click()  await expect(screen.getByRole("heading", { name: "Thank you for your purchase" })).toBeVisible()  await expect(screen.getByText(/^Credits: /)).toHaveText("Credits: 500")}).invariant(async ({ app, stripe, expect }) => {  // Whichever way the purchase went, the shop grants exactly the credits Stripe was paid for.  expect(await grantedCredits(app.baseUrl, "u1")).toBe(await paidCredits(stripe, "u1"))})
Compare

The screen is the easy part.

UI-only AI testing tells you the button worked. danwo tells you what the button did.

danwo
UI-only AI testing
Drives the UI with an agentplain-English steps on web and mobile
Yes
Yes
Replays with no model callscached choices, deterministic reruns
Always
Sometimes
Emulated third-party providersStripe, Resend, Slack, model APIs and more
22 providers
Real sandboxes
Judges what happened behind the appinvariants over calls, webhooks and data
Yes
Pixels only
Fault matrixdeclines, 500s, timeouts, webhook faults
Planned per run
No
Races and data faultsdouble-submit, Postgres and Redis, time travel
Yes
No
Flight record of the backendwaterfall, effects diff, emulator state
Yes
Video and DOM
Runs on your own machinesyour CI, your data
Open source
Rarely
Open source

Yours to run. Ours to host, if you like.

The whole engine runs on your machines: runner, emulators, matrix, recorder and dashboard. The cloud adds history and a team around it.

self-host

Open source

Everything that runs a test, on your laptop or in your CI. Nothing leaves your machines.

  • All emulators, faults and the matrix
  • Local dashboard with danwo serve
  • GitHub Check and pull request comment, MCP server
  • Drop into Playwright Test with @danwo/playwright
$ npx @danwo/cli init $ npx @danwo/cli matrix tests/buy-credits.danwo.ts $ npx @danwo/cli serve
cloud · coming

danwo Cloud

The same engine, with the history your team needs to trust it.

  • Run history, flaky-test insights and impact
  • Hosted flight records, shareable per pull request
  • Matrix runs fanned out across workers
  • Teams, single sign-on and model-spend tracking

Not open yet. Everything above runs self-hosted today.

Find the fault before your users do.

One command sets danwo up in your project, for you and for your coding agents.

$ npx @danwo/cli init
GitHubDocs