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
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

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