dánwòdánwò

Core concepts

Agent and locators

Two ways to write a step. Locators name an element exactly; the agent takes a plain-English instruction. Use both in one test.

A danwo test drives your app in two ways. A locator finds one element the way a user names it, like "the button called Pay". The agent takes an instruction in plain English, like "pay with the saved card", and works out the clicks itself.

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

test("pays for a basket", async ({ app, screen, agent, expect }) => {
  await app.open("/basket")
  await screen.getByRole("button", { name: "Checkout" }).click()
  await agent.act("pay with the test card 4242 4242 4242 4242")
  await expect(screen.getByRole("heading", { name: "Thank you" })).toBeVisible()
})

The first and last steps use locators. The payment page in the middle belongs to another company and changes often, so the agent handles it.

When to use which

Use a locator when…Use the agent when…
you can name the element: a button, a fieldthe step is easier to say than to pin down
the control is stable and yoursthe page is someone else's, like a provider's checkout
you check an exact value: a total, a countyou check what the page means: "the order is confirmed"
you want no model calls, evera recording can do the replays (see Replay cache)

Locators never call a model. Agent steps call one the first time, then replay from a recording.

Find an element with a locator

screen finds elements by what a user sees. Prefer these, in this order:

tests/sign-in.danwo.ts
import { test } from "@danwo/sdk"

test("signs in", async ({ app, screen }) => {
  await app.open("/login")
  await screen.getByLabel("Email").fill("[email protected]")
  await screen.getByRole("button", { name: "Sign in" }).click()
  await screen.getByText("Welcome back").waitFor()
})
FinderFinds
getByRole(role, { name })a button, link, heading or field by its name
getByLabel(text)a field by its label
getByText(text)an element by the text it shows
getByPlaceholder(text)a field by its placeholder
getByTestId(id)an element by its data-testid

A string matches part of the name, ignoring case. Pass { exact: true } for an exact match, or a regular expression.

Act on it

Once a finder has one element, call an action on it. Each action waits for the element to be ready first.

ActionWhat it does
click()clicks the element
longPress()presses it and holds, then lets go
fill(text)types into a field, replacing it
press(key)presses a key, like "Enter"
check()ticks a checkbox or radio
selectOption(value)picks an option in a select
hover()moves the pointer over it
setInputFiles(path)attaches a file to a file input
dragTo(target)drags it onto another element

Hold a press

Some controls do something else when you hold them: a message that opens a menu, a card you start dragging. longPress() presses the element, holds it, then lets go. It waits for the element like click() does.

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

test("opens a message's menu", async ({ app, screen, expect }) => {
  await app.open("/messages")
  await screen.getByText("See you at noon").longPress({ durationMs: 600 })
  await expect(screen.getByRole("menuitem", { name: "Reply" })).toBeVisible()
})
OptionWhat it doesDefault
durationMshow long the press is held, in milliseconds800
forceskips the readiness checks (web only)false
timeouthow long to wait for the elementthe session's action timeout

On a web page the mouse goes down on the middle of the element. On a phone the finger does.

When an action keeps waiting

Component libraries often draw a checkbox or radio over an invisible <input>. check() and uncheck() handle that for you: when the input's own label covers it, danwo clicks the label, as a user would, and then checks that the input changed.

For another control a user can reach but an action keeps waiting on, such as a button that never stops animating, pass { force: true }. The action then skips its readiness checks and acts at once.

tests/install.danwo.ts · force
await screen.getByRole("button", { name: "Start now" }).click({ force: true })

force also clicks a disabled or covered element, so the test can pass where a user is stuck. Use it only for the one control that needs it.

Narrow it to one element

An action needs exactly one match. When a finder matches several, narrow it:

Narrow withKeeps
.first()the first match
.nth(i)the match at index i, counting from 0
.filter({ hasText })the matches that contain this text
.filter({ hasNotText })the matches that don't contain this text
.filter({ has })the matches with this locator inside
.filter({ hasNot })the matches without this locator inside
another getBy… callelements inside the matches
.and(other)the matches the other locator finds too
tests/basket.danwo.ts · narrowing
await screen.getByRole("listitem").filter({ hasText: "Socks" }).getByRole("button").click()
await screen.getByRole("row").nth(2).click()
await screen.getByRole("row").filter({ hasNotText: "Archived" }).first().click()

When a locator finds nothing, the error says how many elements each call left, so you can see which call lost it:

locator.click: getByRole("listitem").filter(hasText: "Kiwi") matched nothing within 10000 ms; by step: getByRole("listitem") 3, .filter(hasText: "Kiwi") 0

Match one of two

.or(other) matches what either locator finds. Use it when the page shows one of two things, such as a dialog that only appears sometimes:

tests/checkout.danwo.ts · or
const pay = screen.getByRole("button", { name: "Pay" })
const cookies = screen.getByRole("button", { name: "Accept cookies" })

await expect(pay.or(cookies).first()).toBeVisible()
if (await cookies.isVisible()) await cookies.click()
await pay.click()

Not sure a locator matches one element? Your coding agent can check it against the running app with the check_locator MCP tool, which answers with the exact line to paste.

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