示例

Next.js waitlist

A waitlist on Next.js: the form saves an email to Postgres and sends a welcome email through Resend.

本页还没有中文版,下面是英文原文。

A waitlist on Next.js: the form saves an email to Postgres and sends a welcome email through Resend. See it on GitHub.

What it shows

  • An invariant that reads both sides: exactly one row in Postgres and exactly one welcome email in the Resend emulator
  • danwo matrix faulting Resend (429, 500, a lost answer), the app's own SQL through the data proxy, and a double submit
  • A Postgres snapshot per worker, restored before every test and trial, with no app restart

Stack

Next.js App RouterPostgresTailwind CSS

Emulates: Resend. Runs on: A Next.js app in Chromium, with Postgres in Docker.

Run it

Copy examples/with-next anywhere, then from its folder:

npm install
npm test

Its scripts:

npm run dev            # next dev
npm run build          # next build
npm run start          # next start
npm run typecheck      # tsc --noEmit
npm test               # danwo test
npm run test:agentic   # danwo test tests/agentic.danwo.ts
npm run matrix         # danwo matrix tests/deterministic.danwo.ts --grep "joins the waitlist"
npm run matrix:plan    # danwo matrix tests/deterministic.danwo.ts --grep "joins the waitlist" --plan
npm run serve          # danwo serve

Its README says what else it needs and what the last full matrix found.

Until the danwo version it pins is on npm, install it from a local build instead: from the repository's root, bun run release <version> --pack, then node examples/link.ts with-next, which installs a copy and prints where it is.

Your app's data in tests explains what this example leans on.

The test

tests/deterministic.danwo.ts
import { joinedOnce, test } from "./waitlist.ts"

const EMAIL = "[email protected]"

test("shows the waitlist form", async ({ app, screen, expect }) => {
  await app.open("/")

  await expect(screen.getByRole("heading", { name: "Join the waitlist" })).toBeVisible()
  await expect(screen.getByLabel("Work email")).toBeVisible()
})

test("asks for a valid email", async ({ app, screen, expect }) => {
  await app.open("/")

  await screen.getByLabel("Work email").fill("ada")
  await screen.getByRole("button", { name: "Join the waitlist" }).click()

  /* Next.js renders its own empty role=alert (the route announcer), so pick ours by its text. */
  await expect(
    screen.getByRole("alert").filter({ hasText: "Enter a valid email address." }),
  ).toBeVisible()
})

test("joins the waitlist", async ({ app, screen, expect }) => {
  await app.open("/")

  await screen.getByLabel("Work email").fill(EMAIL)
  /* Joining saves the row and calls Resend; readiness waits for both before the next line. */
  await screen.getByRole("button", { name: "Join the waitlist" }).click()

  await expect(screen.getByRole("status")).toContainText("You're on the list")
  await expect(screen.getByTestId("waitlist-count")).toHaveText("1")
}).invariant((fixtures) => joinedOnce({ ...fixtures, email: EMAIL }))

本页内容

在每个 Pull Request 上运行

每个 Pull Request 一个 GitHub Check 和一条评论,并附上完整的运行记录。

配置 CI