The open-source end-to-end testing framework for web and mobile apps.
Drive your app through its UI like a user, and check what happened behind it when you need to.
Learn more »
English · 简体中文
Quickstart · Docs · What you can test · Fault matrix · Coding agents · Contributing
danwo is a complete UI testing framework on its own: locators, plain-English agent steps that replay with no model calls, and screenshot comparison, in a browser, on Android or on iOS. When your app takes payments, sends email or handles webhooks, turn on the backend layer: emulated providers, invariants and built-in fault injection (the fault matrix).
import { test } from "@danwo/sdk"
test("signs up", async ({ app, screen, expect }) => {
await app.open("/sign-up")
await screen.getByLabel("Email").fill("ada@example.com")
await screen.getByLabel("Password").fill("correct horse battery staple")
await screen.getByRole("button", { name: "Create account" }).click()
await expect(screen.getByRole("heading", { name: "Welcome, Ada" })).toBeVisible()
})This runs with no model and no API key.
import { test } from "@danwo/sdk"
test("buys two mugs", async ({ app, agent, screen, expect }) => {
await app.open("/")
await agent.act("add two blue mugs to the basket and go to checkout")
await agent.assert("the basket shows two blue mugs")
await expect(screen).toHaveScreenshot("checkout")
})The first run needs a model and its API key: each step goes to the model, which works out the
clicks, and danwo records them. Later runs replay that recording with no model call. Commit the
recording and CI replays it too; danwo test --replay-only makes sure no model is called there. A
step goes back to the model only when the page has changed enough that its recording no longer
fits.
You need Node.js 22.22.3 or a later 22, or 24.11.1 or newer, on macOS, Linux or Windows (web tests; mobile tests need macOS or Linux). In your app's folder:
npx @danwo/cli init
npm run test:danwoinit asks what to test, which model and providers to use (pick none for a UI-only start) and
which coding agents to set up. It writes a config and a first test that passes, and installs the
browser. See the quickstart.
| Emulates the providers | Stripe, Clerk, Resend, Google, Slack and 20 more answer from local emulators behind a proxy. No sandbox keys, no code changes. |
| Judges the backend | An invariant reads your database and the provider after the test: one row, one charge, one email. |
| Breaks it on purpose | danwo matrix reruns a test once per fault: 500s, declines, duplicate webhooks, lost database commits. The invariant judges each run. |
test("joins the waitlist", async ({ app, screen, expect }) => {
await app.open("/")
await screen.getByLabel("Work email").fill(EMAIL)
await screen.getByRole("button", { name: "Join the waitlist" }).click()
await expect(screen.getByRole("status")).toContainText("You're on the list")
}).invariant(async ({ databaseUrl, resend, expect }) => {
// Whatever went wrong on the way: one row, one welcome email.
expect(await sql(databaseUrl, "select id from signups where email = $1", [EMAIL])).toHaveLength(1)
expect(resendInbox(resend).list(EMAIL)).toHaveLength(1)
})npx @danwo/cli matrix tests/waitlist.danwo.tsPASS status 429 on POST api.resend.com/emails
PASS reset-after on POST api.resend.com/emails
PASS serialization_failure on INSERT signups (postgres)
PASS double-submit on POST /api/join
…
faults: 16 passed
Remove the app's idempotency key and two trials turn red: a second welcome email went out. The faults are planned from what the test called and injected at danwo's proxy, so there is nothing to set up and every result reproduces. See the fault matrix.
Every run, with or without the backend layer, leaves a flight record: video, steps, and the calls behind them, in a dashboard, a PR comment and over MCP.
Each one runs with no real keys. Clone, install, test.
| Example | What it shows |
|---|---|
commerce |
A shop on Next.js, Hono, Postgres and Stripe Checkout. 86 faults held. |
saas |
Workspaces, a trial and billing, Slack and Google, Inngest jobs, live notes. |
clerk-login |
The real Clerk components, Google sign-in and org invites, on the emulator. |
better-auth |
Self-hosted Better Auth: email codes, Google and GitHub, linked accounts. |
with-next |
The smallest one: a Next.js waitlist on Postgres and Resend. |
with-swiftui |
The same waitlist as a native SwiftUI app on the iOS Simulator. |
with-compose |
The same waitlist as a native Jetpack Compose app on an Android emulator. |
with-flutter |
The same waitlist as a Flutter app, on an Android emulator or iOS Simulator. |
More in the examples gallery.
| Web apps | Any framework, started by danwo or already running. |
| Android | Expo, React Native, Jetpack Compose and Views apps on an emulator. |
| iOS | Expo, React Native, SwiftUI and UIKit apps on an iOS Simulator. |
| Playwright Test | Keep your tests, add emulators and the matrix. |
| From Cypress | Move a Cypress suite over one spec at a time. |
| From Detox | Move a Detox suite over one test file at a time. |
| Real stacks | Several apps, workers, databases and background jobs behind one proxy. |
| CI | A GitHub Check and a PR comment that say what broke and whose fault it is. |
| Coding agents | A skill and an MCP server for Claude Code, Cursor and Codex. |
Node 22.22.3+ or 24.11.1+, and bun:
bun install
bun run checkSee CONTRIBUTING.md, the Code of Conduct, and SECURITY.md for reporting a vulnerability. danwo is early: APIs will change.
Dánwò is Yorùbá for "to test". It comes from dán … wò, "try it and see": you put something to the test and watch what happens. Say it DAHN-woh, high then low.
The code is under the Apache License 2.0. The dánwò name and logo are trademarks of Carbonme Inc; see TRADEMARKS.md.