Skip to content

Developing Nibble

Tests and checks

bin/ci, what it runs, and the end-to-end suite.

bin/ci

One command, and the only definition of Nibble’s checks. Run it before every push; it takes well under a minute, and it is the same suite a release refuses to go out without. It runs, in order: bin/setup, RuboCop, nibble:check, ESLint, Prettier, vue-tsc, the frontend unit tests, bundler-audit, Brakeman, the client and SSR builds, an SSR smoke test, the test-asset build, the Rails tests, a site’s own tests if it has any, and the seeds.

Individually:

bin/rails test        # build test assets first if Vite has not: RAILS_ENV=test bin/vite build
npm run lint          # ESLint
npm run format:check  # Prettier
npm run check         # vue-tsc
npm run test:js       # frontend unit tests
bin/rubocop
bin/ssr-smoke         # renders every page through a freshly built SSR bundle

When it fails

It prints each check as it goes and stops on the first failure, naming it. A few are worth recognising on sight:

Check Usually means
Schema: nibble:check a blueprint, collection or generated type file is out of step
Types: vue-tsc a view uses a field the blueprint does not have, or types were not regenerated
Tests: SSR smoke a component touches a browser global while being imported
Security: Brakeman a new warning; fix it or record why it is safe in config/brakeman.ignore

Writing tests

Tests encode why behaviour matters, not just what it does. A test that cannot fail when the business rule changes is not doing anything, and it costs as much to maintain as a good one.

In practice that means the name says the rule — “a page whose file has gone is trashed, not left live” — and the assertion has a message explaining the consequence, so a failure reads as a sentence rather than as a diff.

The suite is Minitest, runs in parallel, and uses fixtures. There are three kinds of test worth distinguishing:

  • Unit tests under test/lib/ for the engine’s own behaviour.
  • Integration tests under test/integration/ for a whole request, through routing, rendering and caching.
  • Architecture tests under test/architecture/ for rules a reviewer would otherwise have to remember — database portability, documentation links, the SSR port agreeing at both ends.

The SSR smoke test

bin/ssr-smoke starts the built SSR bundle and renders every page component with only the shared props, checking the process survives. A component that touches browser globals on import would otherwise take down server rendering for the whole public site, and nothing else would catch it.

It runs on a port of its own, picked free at the moment it starts, so it can run while bin/dev is up — Puma’s inertia_ssr plugin holds 13714 whenever the Vite dev server was not already running when it booted, which is also why bin/dev sometimes logs Inertia SSR: process exited … restarting in 1s.

That is possible because lib/nibble/frontend/ssr/ssr.ts calls createServer itself, with port: process.env.INERTIA_SSR_PORT, rather than letting @inertiajs/vite write that call on its fixed port. config/initializers/inertia_rails.rb reads the same variable, so both ends move together and an instance that sets nothing gets 13714.

Give a second instance a port of its own, or it will fight the first for 13714 and one of them will sit in Puma’s restart loop:

INERTIA_SSR_PORT=13715 RAILS_ENV=test bin/rails s -p 3300

It also checks its port is free again before exiting: a server left behind is a failure the next run inherits.

End-to-end

The Playwright suite drives the control panel in a browser. It is separate from bin/ci because it needs a built application and a database of its own.

A site’s tests

A site puts its own in site/test/, which bin/ci runs after Nibble’s. Nibble’s own stay in test/, which is ours — a site putting tests there would meet them again at the next upgrade.