dánwòdánwò

Core concepts

Agent steps

The agent's four calls, act, assert, extract and waitFor, with examples, parameters and limits.

The agent fixture gives a test four calls. Each takes plain English. Each is checked, recorded the first time it passes, and replayed with no model call after that.

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

test("upgrades to Pro", async ({ app, agent }) => {
  await app.open("/settings/billing")
  await agent.act("upgrade the workspace to the Pro plan")
  await agent.assert("the plan shows as Pro")
})

Pick a model

Agent steps need a model in your config. A model id goes through the Vercel AI Gateway, which reads AI_GATEWAY_API_KEY from the environment:

danwo.config.ts · the model
model: "anthropic/claude-sonnet-5",

danwo init can set up Anthropic, OpenAI, Google, OpenRouter or any OpenAI-compatible server instead.

How much of a page the model reads

The model reads the page as text. A long page is cut to fit the model, so a small model still has room for the instruction and its answer:

Model's input windowPage text it reads, at most
128k tokens or more, or a model unknown120 000 characters (about 30k tokens)
Smaller, such as 32k tokensa quarter of it: 32 000 characters (8k tokens)
The smallest windows8 000 characters

A step whose page was cut says so in its record, with how long the page was. If a step misses something far down a long page, scroll or filter the page first.

act: do something

act carries out an instruction on the page: clicks, typing, choosing, scrolling, across several pages if needed.

tests/sign-up.danwo.ts
import { test, unique } from "@danwo/sdk"

test("signs up", async ({ app, agent, secret }) => {
  await app.open("/")
  await agent.act("sign up as {email} with the password {password}", {
    params: { email: unique("[email protected]"), password: secret("password") },
  })
})

Values in braces come from params:

  • unique(value) gives a fresh value on every run, such as a new email address. A replay accepts the new value.
  • secret(name) passes a configured secret. The model only ever sees the name.

assert: check what the page means

assert asks a judge whether a claim is true of the page right now. It throws when it isn't:

tests/order.danwo.ts · assert
await agent.assert("the order is confirmed and shows one item")
await agent.assert("the status says paid", { poll: true })

With { poll: true }, the judge looks again until the claim holds or time runs out. Use it when the page gets there on its own, like after a webhook.

On replay, assert waits for the text that proved the claim last time, with no model call. A claim can stop holding while that text is still shown, such as "the total matches the items" after a pricing change. For a claim like that, ask the judge on every run:

tests/order.danwo.ts · a claim judged every run
await agent.assert("the total matches the items in the cart", { replay: false })

It costs a judge call on every run, and fails under --replay-only.

extract: read typed values

extract reads values off the page and checks them against a schema. Use it for numbers you then compare with the backend:

tests/balance.danwo.ts
import { test, z } from "@danwo/sdk"

test("shows the balance", async ({ app, agent, expect }) => {
  await app.open("/account")
  const { balance } = await agent.extract("the account balance", z.object({ balance: z.number() }))

  expect(balance).toBe(500)
})

The values come from the elements the model points at, read by the runner, not typed by the model.

waitFor: wait for a state

waitFor waits until a condition holds, then returns:

tests/order.danwo.ts · waitFor
await agent.waitFor("the order shows as paid", { timeout: 20_000 })

On replay, it waits for the text that proved the condition last time, with no model call.

Options

OptionOnWhat it doesDefault
paramsact, extract, waitForValues the instruction names in braces.none
filesactFiles the step may upload, by name.none
budgetactLimits for the step: turns, model calls, cost, time.the session's
visionact, assert, extractWhether the step may see a screenshot: true, "auto" or false."auto"
timeactLets the step move time forward, e.g. "wait until the trial ends".false
pollassertAsk again until the claim holds.off
replayassertfalse asks the judge on every run and keeps no recording.true
timeoutwaitForHow long to wait, in milliseconds.30 000

With vision: "auto", the agent is told when part of a page isn't in its text view, such as a chart or an icon button with no name. It can then look at a screenshot. Those steps replay with no model too.

Icon buttons with no name

A button that holds only an icon has no name for the agent to read. When the icon's markup names it, such as a lucide-trash class or a trash.svg image, the agent's view shows the word:

the agent's view · a row with a delete icon
- listitem [ref=e7]:
  - text: Invoice 43
  - generic [ref=e9] [cursor=pointer] [icon="trash"]

The agent clicks it, and the recording finds it again by its icon. An icon with no such markup is left for a screenshot. Giving the button an aria-label is better still: people using a screen reader need the name too.

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