The contact form is the only page on this site that generates leads. So that’s where the tests live.
We don’t chase coverage numbers. What we have is a targeted set of tests aimed at the things that would hurt if they broke: the forms, the pipeline that processes submissions and the CI that catches problems before production.
Dependency injection as a testing technique
Our submission pipeline accepts a dependencies object containing every external service: CRM client, file uploader, error notifier, spam verifier. Both form handlers use it.
That turns the pipeline into a pure function. In production, real services get passed in. In tests, we inject fakes directly — no mocking library, no patching globals. The function doesn’t know whether its dependencies are real.
What the unit tests cover
Five scenarios, each chosen because we saw it go wrong or imagined it going wrong in a way that costs money:
- Date formatting for availability labels. Garbled dates look unprofessional in client-facing notifications.
- File upload fails after lead creation. Losing a PDF is recoverable, losing the lead is not.
- Derived fields reach the CRM correctly. Silent data mismatches are the worst kind of bug.
- Fatal failures alert the team and rethrow. A swallowed error means a lost inquiry.
- Newsletter signup blocks low-scoring spam while still capturing legitimate subscribers.
What the e2e tests cover
The e2e suite tests what the user actually sees: inline validation messages appearing as they type, the success screen after a valid submission, the error state when something goes wrong. Three form variants (start-a-project, minimal and newsletter) are each tested through the real UI in a headless browser. Playwright intercepts server calls at the network layer, so the tests run without a backend but the user-facing behavior is identical to production.
Why Node’s built-in runner
We use Node’s built-in test runner — not Jest, not Vitest. The DI pattern means tests are just function calls with fake inputs, so there’s nothing to mock and no lifecycle hooks to manage. A test framework would add a dependency and a configuration layer for problems we don’t have. The built-in runner handles assertions and test grouping, which is the whole requirement here.
The same philosophy extends to e2e. Playwright intercepts network calls so the tests run against the real UI without touching real services — the suite runs anywhere node does, with nothing to spin up first.
What it caught
Since this setup went live, no form submission has been lost without the team knowing about it within minutes. Each test is still just a function call with fake inputs and a check on the output.
The authoring surface: a plain text file with a consistent structure.
Six weeks, six different ON logos. The variety makes each release feel distinct.