dánwòdánwò

Platforms

Web

danwo drives your web app in a real Chromium browser, behind its proxy, with locators, an agent and a recording of every test.

On the web, danwo opens your app in Chromium and clicks through it like a user. The browser runs behind danwo's proxy, so every request the page makes, and every call your server makes, is recorded and can be faulted.

A web test

tests/storefront.danwo.ts
import { test } from "@danwo/sdk"

test("shows the credits offer", async ({ app, screen, expect }) => {
  await app.open("/")
  await expect(screen.getByRole("button", { name: "Buy 500 credits" })).toBeVisible()
})

app.open("/") loads your app's address plus the path. screen finds elements the way a person names them, and expect waits until a check holds.

What a test gets

FixtureWhat it is
appyour app: open(path) and baseUrl
screenfinds elements: getByRole, getByLabel, getByText, getByTestId…
expectchecks that wait: toBeVisible, toHaveText, toHaveScreenshot…
agentplain-English steps: act, assert, extract, waitFor
usersanother person's browser in the same test
timemoves the app's clock forward
mailreads the emails and codes your app sent

Agent and locators explains when to use each style.

Each test gets a fresh browser

Every test, and every retry, opens a new browser context: no cookies, no storage, nothing left from the test before. To start signed in, use a saved session, which signs in once per worker and reuses the browser state.

Set the browser's language, time zone and size

browser.contextOptions in your config sets up every browser a run opens. That covers each test, each saved session's sign-in, each other person a test opens, and the MCP server's sessions:

danwo.config.ts · the browser
browser: {
  contextOptions: {
    locale: "de-DE",
    timezoneId: "Europe/Berlin",
    colorScheme: "dark",
    reducedMotion: "reduce",
    viewport: { width: 1440, height: 900 },
  },
},
OptionWhat it doesDefault
localethe browser's language: navigator.language, Accept-Languagethe system's
timezoneIdthe page's time zone, such as America/New_Yorkthe system's
colorSchemewhat prefers-color-scheme answers: light, darklight
reducedMotionreduce asks the page to cut its animationsno-preference
viewportthe page's size in pixels; null uses the window's size1280 × 720
httpCredentialsa user name and password for a site behind HTTP sign-innone
extraHTTPHeadersheaders sent with every request, to every hostnone

danwo checks these when the config loads. A misspelt key or a time zone that doesn't exist fails the run with CONFIG_INVALID, naming the key.

With timezoneId set, time.set("2031-07-04T09:30") means 9:30 in that zone, as the page shows it. A time with an offset or a Z means the same moment anywhere.

extraHTTPHeaders go to every host the page calls, Stripe.js and other providers included, so put nothing there those hosts shouldn't see. Give httpCredentials an origin to send them to your app alone, and read the password from the environment, not the config file.

The page waits for your backend

After each action danwo waits for the page to settle. A page that shows it is loading doesn't count as settled: an element marked aria-busy, a progress bar with no value, or a status that says "Loading". That wait stops after 3 seconds. With an environment, danwo also waits until nothing your app started is still in flight at the proxy:

readinessWaits for
page+backendthe page, the backend's calls, and the page again if they ran (default with a proxy)
pagethe page alone
offnothing

Add settle: true to also wait for webhooks to be delivered and background jobs to finish:

danwo.config.ts · readiness
readiness: { mode: "page+backend", settle: true },

What's recorded

Every test keeps a flight record: a video of the tab, each step with a screenshot, the page's console and network, and every provider call and webhook behind it. Open it with npx @danwo/cli serve --open. See Recordings.

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI