Three layers, and each has a harness already built.
| Layer | Runs | Good for |
|---|---|---|
| Unit | Vitest or Jest, in process | Pure logic — validators, mappers, policies, the domain layer |
| Server end-to-end | A real Postgres in a testcontainer, driven by supertest | Guards, permissions, transactions, migrations, actual SQL |
| Admin end-to-end | Playwright against the built admin, with /api mocked | Flows, keyboard, accessibility |
npx nx test <project>
npx nx e2e server-e2e
npx nx e2e admin-e2eUnit tests
The domain/ layer of a layered plugin imports no framework — no NestJS, no
Drizzle, no React — which is exactly what makes it worth testing directly.
Validators, the publish gate, the rich-text structural rules and the locale
policy are all pure functions with unit tests.
If a rule matters, put it in domain/ and test it there rather than reaching it
through an HTTP round trip.
Server end-to-end
The harness starts the real application against a real PostgreSQL in a container, applies every plugin’s migrations, and drives it with supertest.
const app = await createTestApp();
// … seed through dependency injection, not SQL
await resetDb();
await closeTestApp(app);What that buys, and why it is worth the container:
- Guards and permissions are actually exercised. A route that forgot
@RequirePermissionsfails here and nowhere else. - Migrations run. A schema change that does not apply cleanly fails the suite.
- Transactions and constraints are real. A count-then-write race, a missing unique index or a cascade you did not intend shows up.
Seed through dependency injection rather than raw SQL, so a seed goes through the same validation as a real write and cannot construct a row the application could never have made.
resetDb between tests gives isolation. Per-suite configuration overrides let a
suite change plugin config — turning the login rate limit off, for instance.
Admin end-to-end
Playwright with a page-object harness, running against the built admin with
/api intercepted and mocked — the “seed” is a route mock rather than a
database.
That is a deliberate trade: the admin suite tests the interface, fast and deterministically, and the server suite tests the API. Nothing runs both halves together.
Two suites exist beyond the flows and are worth extending for any page you add:
- An accessibility suite running axe.
- A keyboard suite walking the interface without a mouse.
Where the conventions are written down
The repository encodes them as skills, under .agents/skills/:
| Skill | Covers |
|---|---|
server-plugin | The plugin pattern and its review-critical invariants |
admin-plugin | Routes, hooks, slots, and the data-layer pitfalls |
server-e2e | The testcontainer harness |
admin-e2e | The page-object harness, the mock layer, the DOM gotchas |
accessibility | WCAG conventions for admin UI |
shadcn | Working with the design system |
They are written for AI agents, and they are the most precise statement of house style in the project. Read the one matching what you are building.
Test artifacts
docs/testing/ in the repository holds one QA artifact per app and package:
what it does, how to test it by hand, what the automated suites really cover,
and what looks broken — written by reading the source rather than the
documentation.
They are a snapshot rather than a live report, so check a finding against the current code before acting on it. Several of the ones recorded there have since been fixed.
No load testing, and no contract tests
There is no performance suite, no load harness, and nothing that pins the public API’s response shapes against a consumer. If you depend on a field, an end-to-end test in your codebase is what protects you from it moving.