Features Apograph CMS on GitHub

Testing a plugin

The three test layers, what each is good at, and the harness each one gives you.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

Three layers, and each has a harness already built.

LayerRunsGood for
UnitVitest or Jest, in processPure logic — validators, mappers, policies, the domain layer
Server end-to-endA real Postgres in a testcontainer, driven by supertestGuards, permissions, transactions, migrations, actual SQL
Admin end-to-endPlaywright against the built admin, with /api mockedFlows, keyboard, accessibility
npx nx test <project>
npx nx e2e server-e2e
npx nx e2e admin-e2e

Unit 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 @RequirePermissions fails 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/:

SkillCovers
server-pluginThe plugin pattern and its review-critical invariants
admin-pluginRoutes, hooks, slots, and the data-layer pitfalls
server-e2eThe testcontainer harness
admin-e2eThe page-object harness, the mock layer, the DOM gotchas
accessibilityWCAG conventions for admin UI
shadcnWorking 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.