平台

Web

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

本页还没有中文版,下面是英文原文。

On the web, danwo opens your app in a browser and clicks through it like a user: Chromium by default, or Firefox and WebKit. 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.

Moving from another tool? Playwright Test keeps your Playwright tests, and Coming from Cypress maps each Cypress command to danwo's.

What a test gets

FixtureWhat it is
appyour app: open(path), url(), clipboard() 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.

Check the page's address

expect(screen).toHaveURL() waits until the tab's address matches, like any other check:

tests/checkout.danwo.ts · the address
await screen.getByRole("button", { name: "Pay" }).click()
await expect(screen).toHaveURL("/thanks")
await expect(screen).toHaveURL(/\/orders\/\d+$/)
await expect(screen).not.toHaveURL("/checkout")
You passIt matches
a paththat path on app.baseUrl, query included, exactly
a full URLthat address, exactly
a RegExpany address the expression finds a match in

app.url() reads the address once, without waiting. Both are for the web: a phone app has no address, so on a device they fail with a config error.

Read what the page copied

A copy button writes to the clipboard, and app.clipboard() reads it back. The browser only lets a page do both once the config grants it the clipboard's permissions:

danwo.config.ts · the clipboard
browser: {
  contextOptions: { permissions: ["clipboard-read", "clipboard-write"] },
},
tests/install.danwo.ts · the copy button
await screen.getByRole("button", { name: "Copy the setup command" }).click()
expect(await app.clipboard()).toBe("npx @danwo/cli init")

Reading the clipboard works in Chromium. Firefox and WebKit ask the person to paste instead, so there app.clipboard() fails with a config error.

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
permissionswhat pages may do unasked, such as clipboard-readnone

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.

Browsers

Tests run in Chromium unless you name another engine. Set browser.name to one engine, or to a list to run every test once in each:

danwo.config.ts · the browsers
browser: {
  name: ["chromium", "firefox", "webkit"],
},
EngineWhat it is
chromiumChrome and Edge's engine
firefoxFirefox's engine, Gecko
webkitSafari's engine

For one run, --browser replaces the config. It takes one engine or a comma-separated list:

npx @danwo/cli test --browser firefox,webkit
Running 6 tests (3 in each of firefox, webkit) with 2 workers
  ✓ tests/checkout.danwo.ts › pays for a lamp [firefox] 2573 ms, worker 0
  ✓ tests/checkout.danwo.ts › pays for a lamp [webkit] 4347 ms, worker 1

Each result is labelled with its engine in the terminal, the JUnit and GitHub reports, the flight record and the dashboard.

Install the engines

danwo drives its own Playwright's builds. Install Firefox and WebKit once per machine:

npx @danwo/cli install-browser --browser firefox,webkit

Add --with-deps on Linux for the system libraries. When you set up a project, danwo init asks which browsers to run in and installs them. danwo doctor checks every engine the config names.

What works in each engine

FeatureChromiumFirefoxWebKit
Locators, uploads and dialogsyesyesyes
The agent's view of the page, and its stepsyesyesyes
Replaying a recording from the cacheyesyesyes
Provider calls over HTTPS through the proxyyesyesyes
WebSockets through the proxyyesyesyes
Video, with the pointer and secrets shown as dotsyesyesyes
A popup becoming the active tabyesyesyes
Moving the page's clock with timeyesyesyes
toHaveScreenshot baselines<test>/<test>-firefox/<test>-webkit/
Matrix races: double submit, early webhooksyesyesyes
How the proxy's certificates are trustedthe CA's keythe proxy alonethe proxy alone
Requests to an app on localhost, on macOSthrough the proxythrough the proxysome go around it
danwo record and the MCP serveryesnono

A recording made in one engine replays in the others: the agent's view of a page is the same in each. danwo matrix runs in one engine, the first browser.name names, or --browser.

How each engine trusts the proxy

danwo's proxy answers HTTPS for every host with certificates from a CA it makes for each run. Nothing is added to your machine's certificate store:

  • Chromium gets the CA's public key on its command line, and trusts that key alone.
  • Firefox and WebKit have no such flag: Firefox keeps its own certificate store, and WebKit on macOS reads the system keychain. Their pages ignore certificate errors instead, but only behind danwo's proxy. The proxy is then the one server they talk TLS to, and it checks each real site's certificate itself.

WebKit on macOS sends some requests for an app on localhost or 127.0.0.1 straight to the app, around the proxy. They are missing from the record, and the app receives the whole URL as the path. Each record says so in its notes. WebKit on Linux, as in CI, sends everything through the proxy. So does an app served on a hostname the proxy routes, such as shop.test.

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

本页内容

在每个 Pull Request 上运行

每个 Pull Request 一个 GitHub Check 和一条评论,并附上完整的运行记录。

配置 CI