| name | playwright-api-tests |
| description | Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e). |
Playwright API tests
HTTP API coverage lives in apps/web/playwright/api/<resource>/*.spec.ts. Auth is seeded once by Playwright globalSetup (apps/web/global-setup.ts → setup-test-workspace.ts) into playwright/.auth/api.json. Specs use the custom test from ../fixtures — not @playwright/test directly for test.
Do not put these under apps/web/tests/ (Vitest) or under playwright/partners / playwright/workspaces (browser e2e).
Before writing
- Find the route handler and response shape (Zod schemas under
apps/web/lib/zod, types under @/lib/types or Prisma).
- Prefer mirroring an existing nearby resource folder:
tags/, folders/, customers/, workspaces/.
- Confirm the endpoint is workspace-scoped Bearer API (most
/api/* routes). The fixture already sends Authorization: Bearer ….
What to cover
Before considering an endpoint covered, ask:
- What happens with valid input?
- What happens with invalid input?
- Who is allowed to call it?
- Can another workspace access the resource?
- What happens when the resource does not exist?
- What happens with duplicate data?
- What happens at boundary values?
- What state does the operation leave behind?
- Can the operation be safely retried?
- Does pagination/filtering/sorting behave correctly?
- What happens when multiple requests happen concurrently?
- Are errors returned in the documented format?
Not every endpoint needs a test for every item. The goal is to cover the actual failure modes and API contract, not to maximize the number of tests.
File layout
apps/web/playwright/api/
├── fixtures.ts # api + workspace fixtures (reuse; don't duplicate)
├── setup-test-workspace.ts # globalSetup seed only — extend only if auth/workspace setup must change
└── <resource>/
├── <resource>.spec.ts
└── <resource>-pagination.spec.ts # optional split for heavy pagination suites
One folder per resource. Name files kebab-case.spec.ts (tags/tags.spec.ts, customers/customers-pagination.spec.ts).
Spec template
import { expect } from "@playwright/test";
import { randomName } from "../../utils";
import { test, type ApiClient } from "../fixtures";
async function createThing(
api: ApiClient,
overrides: Record<string, unknown> = {},
) {
return api.post<YourType>("/api/things", {
name: randomName("thing"),
...overrides,
});
}
async function deleteThing(api: ApiClient, id: string | undefined) {
if (!id) return;
await api.delete(`/api/things/${id}`);
}
test("POST /things", async ({ api }) => {
let id: string | undefined;
try {
const body = { name: randomName() };
{ status, data } = api.<>(, body);
id = data.;
(status).();
(data).({
: expect.(),
...body,
});
} {
(api, id);
}
});
Required conventions
| Rule | Detail |
|---|
Import test from ../fixtures | Provides api, workspace, and program |
| Serial only when tests share state | API project is fullyParallel: true. Do not add mode: "parallel". Use test.describe.configure({ mode: "serial" }) only when tests in a file/describe share state (e.g. domains, seeded pagination) |
Cleanup in finally | Create → assert → always delete created rows |
| Unique names/ids | Use randomName / randomCustomer / randomPartnerEmail from ../../utils — never fixed colliding names |
| Assert status + body | Prefer toStrictEqual / toEqual on full shapes; use expect.any(String) for ids/timestamps |
| Default shape once | Happy-path POST owns the full default resource (and nested) shape. Variant tests assert only what they change |
| HTTP contract only | Assert status + JSON. Do not poll/sleep for waitUntil, R2, or other background jobs — CI has no STORAGE_* |
| Error responses | Match { error: { code, message, doc_url } } exactly |
| Typed generics | api.get<T>, api.post<T>, etc. |
| Seeded fixtures | { workspace }, { program } (id, defaultGroupId), TEST_WORKSPACE — not Vitest E2E_* constants |
Fixtures (api / workspace / program)
api.get/post/patch/delete(url, data?) → { status, data } (JSON parsed).
- Paths are app-relative (
/api/...); baseURL is http://localhost:8888.
workspace.id / workspace.slug and program.id / program.defaultGroupId come from .auth/api.json.
- Seeded program domain/url:
TEST_WORKSPACE.program in setup-test-workspace.ts.
Helpers
- Shared randomizers / sort assertions:
apps/web/playwright/utils.ts.
- Local
createX / deleteX helpers in the spec file when the same setup repeats.
- Once
createX exists, use it for setup and follow-up creates (pass overrides for identity fields that must stay stable, e.g. email). Happy-path tests may still inline api.post so the asserted body is local.
- Prisma direct seed is allowed for bulk fixtures (see
customers/customers-pagination.spec.ts); still clean up in finally / after hooks. No DELETE route → Prisma/conn cleanup is fine (partners/partners.spec.ts).
Error / table-driven cases
const errorCases = [
{
name: "POST /things – missing name",
body: {},
expected: {
status: 422,
data: {
error: {
code: "unprocessable_entity",
message: "…",
doc_url:
"https://dub.co/docs/api-reference/errors#unprocessable-entity",
},
},
},
},
];
for (const { name, body, expected } of errorCases) {
test(name, async ({ api }) => {
expect(await api.post("/api/things", body)).toEqual(expected);
});
}
Optional: validate richer payloads with Zod (.parse(...)) like workspaces/workspaces.spec.ts / enriched customer fields.
Migrating Vitest API tests
When converting apps/web/tests/<resource>/*.test.ts:
- Add or extend
playwright/api/<resource>/<resource>.spec.ts — do not leave a parallel Vitest HTTP spec.
- Map
IntegrationHarness / http.post({ path }) → api fixture (/api/... paths).
- Replace
E2E_* / E2E_PARTNER_GROUP with { workspace }, { program }, and TEST_WORKSPACE.
- Delete the Vitest file after the Playwright spec covers its cases.
Do not
- Call
setupTestWorkspace from specs (only globalSetup).
- Import browser
page / storage-state auth for API specs.
- Commit secrets or change the fixed Playwright token unless intentionally rotating local/CI test auth.
- Skip cleanup (parallel API project will leak / flake).
- Run
pnpm build after adding tests.
- Poll or
setTimeout for R2/storage/waitUntil side effects (no STORAGE_* in CI). Assert the immediate JSON body instead (e.g. foreign image URLs stay null on create).
- Copy Vitest
IntegrationHarness or E2E_* constants into Playwright specs.
Run
Dev server on port 8888 (local), then:
pnpm --filter web test:e2e --project=api
pnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts
pnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts -g "POST /tags"
-g / --grep matches the test title (regex). Combine with a file path to narrow further.
In CI, playwright.config.ts starts pnpm start -p 8888 via webServer.
More context: apps/web/playwright/README.md (API section).