Skip to content

Testing

Michael Dohmen edited this page Aug 24, 2026 · 11 revisions

Testing

npx playwright install chromium   # first time only, downloads a browser
npm test

npm test runs six suites. Three of them drive a real headless Chromium — not a jsdom simulation — because a good chunk of what needs testing (File System Access API availability, file:// origin behaviour, actual save-dialog interception) only shows up against a real browser engine; the other three are pure Node.

  • test/prompts-metrics.mjs — pure Node, no browser: checks that the metric section of the generated build prompt describes every declared metric, so an agent reading only the prompt builds the same tiles the demo shows.
  • test/actions-delete-guard.mjs — pure Node: the reference guard holds on every delete path, including an AI-proposed deletion naming a record that another entity still references.
  • test/timezone.mjs — pure Node: freezes the clock at two instants and reruns the due-date logic in child processes with shifted timezones — a due date follows the local calendar day, whether that day lies ahead of or behind the UTC date.
  • test/smoke.mjs — the single-entity path, against the already-built dist/index.html. Around 300 assertions across roughly a hundred scenarios.
  • test/multi-entity.mjs — the ENTITIES/reference-field path (see Building Your Own Tool). It builds examples/suppliers-certificates.domain.js into its own dist-multi-entity/ — swapping src/domain.js for the duration of that one build only, restored immediately after in a finally block regardless of outcome — then exercises the entity switcher, the reference dropdown in the edit form, the resolved-title chip and click-to-navigate in the table, the delete guard against a still-referenced record, CSV export resolving a reference to a name instead of a raw id, and an AI-proposed action that creates a record in one entity by naming a related record in another by its title text rather than its id. Around 60 assertions.
  • test/demos.mjs — opens every built demo once and checks the few things that must always hold: it renders without console errors, has records, computed fields produce values, and the configured accent colour arrives. The examples are the part of the repository that rots silently.

What gets exercised

  • URL and header parsing for the AI client — six endpoint-shape cases (plain OpenAI-style, trailing slash, a proxy needing /v1 appended, an Azure deployment URL with a preserved query string, a gateway with its own query string) and a header-parsing edge case.
  • Cold start over file:// — the seeded demo records render, crypto.subtle and isSecureContext are available in this context.
  • Create a record, save it — via the download fallback (the test disables showSaveFilePicker to force the path Firefox/Safari actually use), reopen the saved file, and confirm the new record survived the round-trip.
  • Encryption round-trip — set a weak passphrase (confirms the strength hint appears but nothing is blocked), save, confirm the saved source contains no trace of the plaintext, reopen with the wrong passphrase (rejected) and then the right one (decrypts, all records present).
  • CSV export, dark mode (and that it persists across a save/reopen, including on the lock screen before decryption), settings round-trip in general.
  • AI dialect negotiation against a mock endpoint that deliberately rejects max_tokens and a non-default temperature on its first two responses, forcing the real adaptation logic to run — then asserts the negotiated dialect is what actually got stored and reused on the next call.
  • A real chat exchange, an attached file actually reaching the model's context, and a deliberately mixed batch of AI-proposed actions — one valid update, one update with an invalid enum value, one create with an unknown field — asserting that the valid one applies, the invalid ones are named as rejected, and the record count only grows by the operations that actually succeeded.
  • API key handling: not saved into the file without an explicit opt-in, prompted for on next open when missing, a working "switch AI off instead" escape hatch, and — when the opt-in is set — that it does land in the file.
  • Configuration export/import: a saved config contains no records and no key; loading a config with a deliberately unknown field and a deliberately invalid temperature value drops/resets those and names it in the notice, rather than silently applying garbage.
  • Branding: a colour change actually reaches the CSS custom property, and — the important one — uploading a logo containing a <script> tag and an onclick handler results in neither surviving in the DOM, while the logo still renders in at least two of its four placements.
  • Calculated fields: that the computed column renders (negative for lapsed items, blank for closed ones), that sorting on it is numeric rather than lexicographic, that the form shows it as a read-only <output> instead of an input — and the load-bearing one, that a saved file contains no trace of the computed field's key anywhere in its data block.
  • The dashboard (Dashboards and Printing): that the declared tiles render, that the stat tiles agree with the sidebar's own counts (the two are computed by different code paths, so a disagreement means one of them is wrong), that the donut's centre total equals the sum of its legend, and that the category shade run actually reverses direction in dark mode.
  • The print stylesheet, via Playwright's print media emulation: asserting that the file bar, sidebar, search row, watermark and buttons all compute to display: none while the table does not.
  • CSV import (Getting Data In) against a fixture the test writes itself, containing on purpose: a German column heading the automatic mapping cannot match (asserting it stays unassigned, then assigning it by hand), a row with no title, an unknown enum value and a date in the wrong format. Asserts that exactly the four valid rows land, that all three objections are named with their line numbers, and that the hand-assigned column actually arrives in the imported record.
  • The usage counter (details) in all three of its states, by intercepting the request in the browser: preset (fires once, and the assertion checks the file name is not in the URL), pointed at a different endpoint and saved (the new endpoint travels with the file), and switched off and saved — where the assertion is that the reopened file makes zero outbound requests of any kind, not merely zero counting requests.
  • Example prompts, versions and the change log (details): that the hint boxes render and disappear when the setting is switched off, that a version reaches both the header badge and the saved file name, and that one log entry per save lands inside the payload with the note that was typed.
  • Screenshots at several points (light, dark, settings, drawer, mobile viewport, CSV mapping and result) for a quick visual spot-check, saved to test/.out/ (gitignored).

What this catches that a unit test wouldn't

Every one of these bugs was actually caught by this suite during development, not found by manual inspection:

  • A component useEffect that re-synced state from a stale prop on mount, racing a fast programmatic fill (exactly what .fill() does) and silently discarding the first keystroke — invisible to a human typing at normal speed, 100% reproducible under automation.
  • An internal call site calling a function with a since-changed signature, throwing only on the specific retry branch that a permissive mock endpoint would never exercise.
  • A hardcoded test fixture path that happened to exist on one machine and nowhere else — passed locally, would have failed on every fresh clone and in CI.

Extending it

If your change touches persistence, encryption, the AI client, or branding, add an assertion here rather than relying on manual clicking — per CONTRIBUTING.md: "a feature without a test tends to break silently three commits later, because nobody clicks through a single-file app by hand."

CI

.github/workflows/build.yml runs on every push and pull request: npm ci, npm run build, a sanity check that the built file is large enough to have actually inlined its script (a build that silently produces a ~9 KB file is a known failure mode — see Limits and Troubleshooting), then the full Playwright suite, plus a closedness gate over everything that gets published:

node scripts/check-self-contained.mjs dist/index.html docs/demo docs/demos

The script fails on any reference a renderer would fetch by itself — external <script src=, <link href=, <img>/<image>, <iframe>/media, <use href>, srcset, CSS @import and CSS url(//…)/url(https://…). Plain <a href="https://…"> links stay allowed (they are content, not loads), and the check deliberately does not match the bundle's own new URL("https://…") calls, which carry the usage counter. It replaced an earlier two-pattern grep that let external images and CSS imports through.

.github/workflows/pages.yml publishes the built file as a live demo on GitHub Pages on pushes to main — but only from a commit that builds cleanly: its deploy job carries needs: build with an internal build job running the same build, demo build, closedness check and demos-freshness check. A red main is no longer published; before this gate existed, Pages deployed independently of any workflow outcome.

Branch protection status

The ruleset on main enforces "changes via PR", but required status checks are not yet set — that needs repository admin rights. Until it lands, "CI green before merge" rests on process discipline (the agents' prompts) rather than on GitHub. The pending settings, once admin access is available:

  1. Ruleset main → add rule Require status checks to pass: contexts build and demo.
  2. Optionally enable the merge queue afterwards — it merges only into a re-tested main state, closing the remaining gap where the squashed combination of several PRs is first seen by CI after landing. Required checks are a prerequisite for the queue.
  3. Residual risk, documented until decided otherwise: all commits and merges carry one shared GitHub identity, so "nobody merges their own PR" cannot be verified from GitHub metadata alone; it remains a discipline rule backed by review evidence in the tracker.

Clone this wiki locally