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.