testover · test system map

How many layers of tests are there, and what does each one guard?

This page walks through the whole automated test system: which kinds of tests exist, which tools run them, when they run, and how contract and characterization tests differ from ordinary ones. It is written for someone who has never done test automation before.

127tests that drive a real browser (Playwright)
42tests with no browser (node:test)
3boundaries guarded by a contract
2GitHub Actions workflows

1Test layers

The higher a test sits, the faster it runs and the more precisely it points at the broken spot. The lower it sits, the more it behaves like a real user, but the slower it is. The six layers complement each other; none replaces another.

42tests

Unit tests : small pieces of code, called directly

Call a function or the server directly, with no browser, so they finish in a fraction of a second. Examples: the server answers 200/301/304/400/404/405/500 correctly, and the notification script builds the right PASS/FAIL message.

unit/server.test.mjs (15) · unit/notify.test.mjs (16) · unit/contracts.test.mjs (11) · run npm run test:unit
6 + 11tests

Contract tests : agreements about the shape of data

Define "the shape the data must have" once (a schema), then check every place where data crosses from one part to another. If one side changes shape without the other knowing, a test fails immediately. Details in section 2.

contracts/schemas.mjs · tests/contracts/storage.spec.ts (6) · the rest live in unit/contracts.test.mjs (11)
37tests

Characterization tests : record what the app does today

They don't ask what the app should do. They record what it actually does right now, odd behaviour included, so you notice the moment someone changes it by accident. Details in section 3.

tests/characterization/validation.spec.ts · tests/characterization/resilience.spec.ts
70tests

E2E regression : use the site like a real user

Open a real browser, click, type, and check the result. Split by area of the app: sign-up/login 27, tasks 27, profile 10, and full multi-page journeys 6. Test data uses made-up users (demo, alice).

login.spec.ts · tasks.spec.ts · profile.spec.ts · journey.spec.ts · Page Objects in tests/support/pages.ts
14tests

Accessibility tests : can everyone use it?

axe-core scans every page and every state (normal, with an error, mid-edit, dark mode) against WCAG 2.1 levels A and AA. One special test injects a known violation to prove the scanner really catches it, so a pass is never silent.

tests/a11y.spec.ts · 16 tests carry the @a11y tag in total (including keyboard and page-title cases in other files)
5tests

Smoke tests : a fast "is it badly broken?" check

Not a new kind of test but a selection: 5 E2E tests for the most important paths run first (tag @smoke). They run on every push and pull request.

run npm run test:smoke · workflow smoke.yml
Part of the projectWhat it is for
app/index.html, app/server.mjsThe Task Board app and a static server: the target of all the tests
tests/support/pages.tsPage Objects: "press this button, fill that field" written once, so tests read well and only one place changes when the UI does
tests/support/storage.tsHelpers that read and write localStorage directly: seedStorage (inject data, then reload), readJson, readRaw, dumpStorage
tests/fixtures/reports/Three fake reports (pass, fail-nested, empty) so unit tests can exercise the notify script without a real Playwright run
contracts/schemas.mjsThe single place that defines the data shape for all 3 boundaries, imported by both Playwright and node:test

2Contract tests: agreements between two sides

Think of a form two departments agree to fill in the same way. If one changes the form without telling the other, the second can't read it. A contract test is the person who checks the form every time it is handed over.

Where data crosses a boundary in this project

Appapp/index.html
C1
localStoragetb.users · tb.session · tb.theme · tb.tasks.<user>
Playwrightrun results
C2
results.jsontest-results/
read by
notify.mjssummarises
C3
webhook bodycontent + text
sent to
Discord / Slack
NameWhat the agreement saysChecked by
C1 localStorage The app may only write the listed keys, and the data must have the right shape. For example every task must be { id: integer, title: string, priority: low|medium|high, done: boolean }, ids must be unique, the session must name a user that exists, and tasks must belong to an existing user. tests/contracts/storage.spec.ts drives the real UI (register, add/edit/delete tasks, change profile, delete account, switch users) and checks storage after each step.
C2 Playwright report results.json must contain stats (expected, unexpected, flaky, skipped as non-negative integers) and suites with titles, because notify.mjs depends on only this part. unit/contracts.test.mjs checks the 3 fixtures and the report from the latest real run (if the file exists), plus cases that must be rejected.
C3 webhook body Must have content (Discord) and text (Slack), both non-empty and identical, with no extra fields. unit/contracts.test.mjs starts a fake server, runs the real notify.mjs against it, and checks the body it receives against the schema.

The key point is that the schema lives in one place (contracts/schemas.mjs, written with zod). Browser tests and node tests import the same file, so the two sides can never disagree about "the right shape". The state being checked comes from driving the real UI, not from hand-built data.

3Characterization tests: write down what is

It is like photographing a room before moving furniture. The photo doesn't say the room should look this way, but if something goes missing later you know at once. It suits code with no written spec, or old code you are about to change without knowing what depends on it.

Contract

"Data must look like this"

Question it answers
Can the two sides still understand each other?
Example here
Every task's priority must be exactly low, medium or high
If it fails
Usually a bug, or someone changed the data shape on one side only. Make them match again, or if the change is intended, update the schema in one place.
Characterization

"Today it behaves like this"

Question it answers
Did the behaviour change since yesterday?
Example here
A 20-character username passes, 21 does not, and the error messages come out in a fixed order
If it fails
Don't panic yet. Ask "did we mean to change this?" If yes, update the expected value. If no, fix the code.

What the project covers

FileRecords that
validation.spec.tsThe limits of every form: username length (3 to 20 characters; letters, digits and underscore only), password minimum of 8, task title up to 80, the order of checks (username before password; current password, then length, then confirmation), usernames ignore case but passwords do not, and every failed login shows the same message.
resilience.spec.tsHow the app boots from odd storage (broken JSON, wrong types, good and bad tasks mixed, a strange theme) and the redirect rule of every route, including each page's title.
unit/contracts.test.mjsThe exact text notify.mjs builds from each kind of report, and that a run with zero tests counts as PASS.
Tests whose names start with QUIRK record behaviour that is probably unintended but is written down as it really is. There are 4 right now: When these bugs get fixed, the QUIRK tests will fail. That is a good sign: update the expected value to the new, correct behaviour.

4Tools in use and what each one does

Taken from package.json, playwright.config.ts and the workflow files actually in use.

Playwright Test

@playwright/test 1.49.1
main test runner

Launches a real Chromium, drives it to click and type, and provides assertions, retries and parallel runs. It starts the test site automatically before running.

Config in playwright.config.ts · 2 retries and 2 workers on CI · screenshot, video and trace kept only for failures

node:test

built into Node.js
unit test runner

The test runner that ships with Node, nothing to install. Used for tests that need no browser, so results come back fast. Has a built-in coverage mode.

npm run test:unit · npm run test:unit:coverage

zod

zod 4.6.7
defines the contracts

A library for writing a data schema and checking that real data matches it. It is the common language of all three contracts.

contracts/schemas.mjs

axe-core

@axe-core/playwright 4.14.0
accessibility checker

Scans a page for WCAG 2.1 A/AA violations, such as images without alt text, text with too little contrast, or form fields without labels.

tests/a11y.spec.ts

Node HTTP server

app/server.mjs
the site under test

A hand-written static server with nothing to install. Playwright starts it automatically (port 3000, changeable with PORT), or you can point at another site with BASE_URL.

The server itself has unit tests (unit/server.test.mjs)

Playwright reporters

list · html · json
run-time reporting

list prints results in the terminal. html builds a report you open with npm run report. json writes test-results/results.json, which the notify script reads next.

If you pass --reporter yourself there is no results.json and the notification wrongly says FAIL

Allure

allure-playwright 3.1.0 · allure-commandline 2.46.1
in-depth reports

Collects raw results into allure-results and builds a report grouped by behaviour and suite, handy for looking back. It needs Java installed when generating the report.

npm run allure:generate · allure:open · allure:serve

GitHub Actions

checkout / setup-node / upload-artifact v4
runs everything automatically

Runs on ubuntu with Node 22. Two workflows: Smoke on every push to main and every PR, and Weekend E2E on the weekend schedule, which then uploads reports and keeps them for 14 days.

.github/workflows/smoke.yml · weekend-e2e.yml

notify.mjs + webhook

Discord / Slack
chat notification

The script reads results.json, writes a ✅/❌ summary and POSTs it to a webhook kept secret in GitHub Secrets (WEBHOOK_URL). It runs every time, even when tests fail.

scripts/notify.mjs

5When it runs and where the results go

This is the full weekend workflow. Smoke uses only steps 1 to 4, with just the 5 smoke tests.

  1. GitHub Actions wakes the workflow on schedule (Saturday and Sunday 09:00 Thailand time) or when someone starts it by hand.
  2. Install: npm ci and npx playwright install --with-deps chromium.
  3. Playwright opens the Task Board and runs 127 tests (a failure can be retried twice).
  4. Upload playwright-report and test-results as artifacts.
  5. Install Java, build the Allure report and upload it as an artifact.
  6. notify.mjs reads results.json and posts a summary to Discord or Slack with a link to the run.

6What are you changing, and what should you run?

SituationCommand
A tiny change, want a quick checknpm run test:smoke
Changed the notify script or the servernpm run test:unit
Changed the shape of stored datanpx playwright test --grep @contract
Changed validation rules or error messagesnpx playwright test --grep @characterization
Changed look, layout or coloursnpx playwright test --grep @a11y
Before a releasenpm run test:unit then npm test
See what a command would run, without running itnpx playwright test --list --grep @contract

Kind tags in use: @smoke @validation @security @persistence @resilience @destructive @a11y @responsive @contract @characterization, plus area tags @auth @tasks @profile @journey. A test can carry several tags.

7Good to know: current gaps

Unit tests don't run in CI yet. Both smoke.yml and weekend-e2e.yml run only Playwright. The 42 tests in unit/ (including contracts C2 and C3) must be run by hand with npm run test:unit. To have them caught automatically, add that step to a workflow.
There is no heartbeat. If GitHub's schedule is disabled or GitHub is down, no message arrives and nobody notices. The README suggests healthchecks.io but it is not wired up.
Chromium only. Firefox and Safari are not covered.

Keep reading