平台
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
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
| Fixture | What it is |
|---|---|
app | your app: open(path), url(), clipboard() and baseUrl |
screen | finds elements: getByRole, getByLabel, getByText, getByTestId… |
expect | checks that wait: toBeVisible, toHaveText, toHaveScreenshot… |
agent | plain-English steps: act, assert, extract, waitFor |
users | another person's browser in the same test |
time | moves the app's clock forward |
mail | reads 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:
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 pass | It matches |
|---|---|
| a path | that path on app.baseUrl, query included, exactly |
| a full URL | that address, exactly |
| a RegExp | any 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:
browser: {
contextOptions: { permissions: ["clipboard-read", "clipboard-write"] },
},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:
browser: {
contextOptions: {
locale: "de-DE",
timezoneId: "Europe/Berlin",
colorScheme: "dark",
reducedMotion: "reduce",
viewport: { width: 1440, height: 900 },
},
},| Option | What it does | Default |
|---|---|---|
locale | the browser's language: navigator.language, Accept-Language | the system's |
timezoneId | the page's time zone, such as America/New_York | the system's |
colorScheme | what prefers-color-scheme answers: light, dark | light |
reducedMotion | reduce asks the page to cut its animations | no-preference |
viewport | the page's size in pixels; null uses the window's size | 1280 × 720 |
httpCredentials | a user name and password for a site behind HTTP sign-in | none |
extraHTTPHeaders | headers sent with every request, to every host | none |
permissions | what pages may do unasked, such as clipboard-read | none |
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:
browser: {
name: ["chromium", "firefox", "webkit"],
},| Engine | What it is |
|---|---|
chromium | Chrome and Edge's engine |
firefox | Firefox's engine, Gecko |
webkit | Safari's engine |
For one run, --browser replaces the config. It takes one engine or a comma-separated list:
npx @danwo/cli test --browser firefox,webkitRunning 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 1Each 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,webkitAdd --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
| Feature | Chromium | Firefox | WebKit |
|---|---|---|---|
| Locators, uploads and dialogs | yes | yes | yes |
| The agent's view of the page, and its steps | yes | yes | yes |
| Replaying a recording from the cache | yes | yes | yes |
| Provider calls over HTTPS through the proxy | yes | yes | yes |
| WebSockets through the proxy | yes | yes | yes |
| Video, with the pointer and secrets shown as dots | yes | yes | yes |
| A popup becoming the active tab | yes | yes | yes |
Moving the page's clock with time | yes | yes | yes |
toHaveScreenshot baselines | <test>/ | <test>-firefox/ | <test>-webkit/ |
| Matrix races: double submit, early webhooks | yes | yes | yes |
| How the proxy's certificates are trusted | the CA's key | the proxy alone | the proxy alone |
Requests to an app on localhost, on macOS | through the proxy | through the proxy | some go around it |
danwo record and the MCP server | yes | no | no |
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:
readiness | Waits for |
|---|---|
page+backend | the page, the backend's calls, and the page again if they ran (default with a proxy) |
page | the page alone |
off | nothing |
Add settle: true to also wait for webhooks to be delivered and background jobs to finish:
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.