dánwòdánwò

Core concepts

Seeding and state

Start an emulator with the data your test needs, and read back what your app did at it.

An emulator starts empty. A seed gives it the data your test expects to find: a webhook endpoint, a bucket, a user. After each test, danwo puts it back to that seed.

Seed an emulator

Pass seed to the service. Stripe's seed is a function, so it can use values known only once your app runs, like its webhook URL:

danwo.config.ts
import { appEnvironment, defineConfig, s3Service, stripeService } from "@danwo/sdk"

const url = "http://localhost:3000"

export default defineConfig({
  testDir: "tests",
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url },
      services: [
        stripeService({
          seed: () => ({
            webhookEndpoints: [
              {
                url: `${url}/api/webhooks/stripe`,
                events: ["checkout.session.completed"],
                secret: "whsec_test",
              },
            ],
          }),
        }),
        s3Service({ seed: { buckets: [{ name: "uploads" }] } }),
      ],
    }),
})

Each provider page lists what its seed takes. See Providers.

Read what your app did

Each emulator has a helper that reads its state in plain terms. Call it with the emulator's fixture:

tests/upload.danwo.ts
import { type S3Fixtures, s3Objects, test as base } from "@danwo/sdk"

const test = base.extend<S3Fixtures>()

test("uploads a receipt", async ({ app, screen, s3, expect }) => {
  await app.open("/receipts")
  await screen.getByLabel("Receipt").setInputFiles("fixtures/receipt.pdf")
  await screen.getByRole("button", { name: "Upload" }).click()

  expect(s3Objects(s3).list("uploads", "receipts/")).toHaveLength(1)
})

A few of the helpers:

FixtureHelperReads
resendresendInbox(resend)emails your app sent
s3s3Objects(s3)buckets and objects
slackslackWorkspace(slack)messages your app posted
twiliotwilioInbox(twilio)texts that reached phones
githubgithubRepo(github, "org/repo")issues, comments and check runs
llmllmCalls(llm)your app's calls to OpenAI, Anthropic…

The emulator helpers reference lists them all.

Ask the API directly

Every emulator also takes requests through its fixture, the way your app would call the real API. Use it when there's no helper for what you need:

tests/charges.danwo.ts
import { type StripeFixtures, test as base } from "@danwo/sdk"

const test = base.extend<StripeFixtures>()

test("charges once", async ({ app, screen, stripe, expect }) => {
  await app.open("/")
  await screen.getByRole("button", { name: "Buy" }).click()

  const res = await stripe.fetch(
    new Request("https://api.stripe.com/v1/payment_intents", {
      headers: { authorization: "Bearer sk_test_any" },
    }),
  )
  const { data } = await res.json()

  expect(data).toHaveLength(1)
})

Act as someone outside your app

Some helpers act as a third party, so you can test what happens when something arrives from outside:

tests/inbound-sms.danwo.ts · a text arrives
await twilioPhone(twilio).send("+15555550100", "+15555550199", "STOP")

That's a phone texting your app's number. Other helpers post a Slack message as a user, pay into a virtual account, or revoke a sign-in.

State never leaks between tests. Every test starts from the seed, and so does every retry. See Isolation.

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