More than 700 automated tests check the Warehouse through its front doors: HTTP requests, interface messages, console commands, and a real browser. One command runs them all, and nothing is pushed unless it passes.
The strategy
The tests describe behaviour, not classes. A test sets up a situation with factories, does what a user or a system would do, and checks what can be observed afterwards: the response, the database, the message that was sent. How the code is arranged inside is free to change.
That is why almost all tests are feature tests. A warehouse rule is rarely interesting in isolation; it matters together with the stock, the task, and the order around it. A feature test that books a pick through the Handheld route exercises validation, authorisation, the action, the locks, the journal, and the report to the ERP in one go.
Browser tests cover what only a browser can show: that pages render without JavaScript errors, that dialogs, filters, and scans work, and that the Floor reacts to the keyboard and the pointer. Unit tests are reserved for pure calculations such as the demand curve.
Every behaviour change comes with a test, and a regression test must fail without its fix. Pure copy and styling changes need none.
How deep each kind of test reaches, with the number of tests.
Feature tests
The feature tests are written with Pest 4 and run against SQLite in memory. Each test starts with a migrated, empty database (RefreshDatabase) and builds exactly the world it needs from 31 model factories with named states.
For pages, the tests assert on the Inertia response: the right component, the right props, and above all the absence of props a viewer must not see. For interfaces, they post JSON with a bearer token and check the logged message, its status, and the effect. For scheduled work, they call the command and move the clock.
Rules with many cases use datasets: one test body, run once per case. Time is controlled with travelTo(), always as Berlin wall-clock time, so a test about the 14:00 cut-off or a Friday release reads like the rule itself.
Nothing leaves the test
The base test case forbids real network traffic with Http::preventStrayRequests(). The ERP and the carrier are replaced by fakes at erp.example and carrier.example that accept every call; a test that needs a refusal points the system at another host and fakes that answer.
Mail goes to an in-memory mailer, the queue runs synchronously, the cache and the sessions are arrays, and password hashing is turned down to its cheapest setting. Chance is replaced as well: the three luck classes are swapped for fixed versions, so the virtual crew, the supplier, and the carrier do exactly what the test says. Server-side rendering is switched off (INERTIA_SSR_ENABLED=false), so no test depends on a running Node.js process.
Because nothing is shared, the suite runs in parallel, one in-memory database per process.
Browser tests
The browser tests use Pest’s browser plugin, which drives Chromium through Playwright. They are written in the same style and the same files as the feature tests, with the same factories, and run in the same command.
They always run against the built assets, never against the dev server, so they see what would be deployed, except for server-side rendering: the browser renders every page itself. Each test ends by asserting that the page raised no JavaScript error. Every wait has a ten-second limit.
Thirteen of them cover the Floor: walking into an aisle with the arrow keys, stepping aside, turning the head with the mouse, the joystick on a phone, selecting a pallet by its front, and a pallet card that follows its pallet when the stock changes. Under test, the scene skips post-processing, which keeps it fast and the pixels stable.
What the tests cover
As of October 2026 there are 776 tests: 682 feature tests, 91 browser tests, and 3 unit tests, in 104 files and 12,800 lines, next to 22,700 lines of application PHP.
Every one of the 16 interface endpoints, every Handheld step, and every screen that changes data is covered by at least one feature test, including its important refusals: the wrong status, the wrong Warehouse, the blocked location, the missing right. Each step of the flow in The flow has its own test file, from order intake and release through picking, clarification, packing, and shipping to receiving and putaway.
The figure counts tests by the area they protect. The largest groups are not the happy paths of the Warehouse but everything around them: who may see and do what, and whether every screen renders.
Coverage is not measured as a percentage of lines. No coverage driver is part of the setup, and the number would say little here: one feature test crosses dozens of classes. The honest measure is the list of behaviours that have a test.
Tests by the area they protect, October 2026.
Security and privacy tests
About 125 tests guard accounts, rights, and personal data. They are written from the attacker’s side: a guest, a stranger with an account, and an administrator ask for the same page, and the test checks what each receives.
A dedicated security audit file holds 28 of them. It checks, among other things, that names stay masked inside logged message payloads even when the message is malformed, that old sessions end after a password change, that a new email address only counts after its signed confirmation, that an interface token works for its own direction only, and that oversized calls are refused before anything is stored.
Further files cover the admin gate, the masking of customers and workers, account deletion, sign-in, and two-factor authentication. They include the indirect leaks: searching for a hidden name finds nothing, and sorting by picker uses the masked name, not the real one.
Static analysis and style
Tests show that the code does the right thing for the cases somebody thought of. Static analysis finds a different kind of mistake without running anything.
PHPStan
Level 7 with Larastan over app, config, database, routes, and bootstrap/app.php. It knows Eloquent models, relations, and the container.
Pint
Laravel preset. The gate only checks; composer lint fixes.
TypeScript
tsc --noEmit in strict mode. Wayfinder’s generated route functions make broken links a type error.
Oxlint and Oxfmt
npm run check: type-aware lint rules with warnings as errors, and formatting including the order of Tailwind classes.
The quality gate
composer test runs tests/gate.sh: Pint, PHPStan, a fresh production build of the assets, and then all three test suites in parallel. It takes about two and a half minutes.
The script is strict about its own behaviour. Only one gate runs on the machine at a time; a second one waits its turn. A run that exceeds eight minutes is stopped together with every process it started, so a hanging browser cannot block the next run. The last line is always GATE PASSED or GATE FAILED with the step that failed.
There is deliberately no hosted CI. The gate on the developer machine is the only gate, and the rule is simple: it runs before every push, and a commit is pushed only after GATE PASSED.
Lockone at a timePintcode stylePHPStanlevel 7Buildreal assetsTestsin parallelVerdictGATE PASSED
The steps of composer test.
What is not tested
The picture on the Floor is not compared pixel by pixel. The browser tests check that the scene loads, reacts, and shows the right data; whether a shadow looks right is still judged by eye.
The tests run on SQLite while production runs on PostgreSQL. Queries are written with Eloquent to behave the same on both, but a difference between the two databases would only show on the development database.
Server-side rendering is not part of the suite. When it was introduced in October 2026, 163 page states were rendered on the server and 69 pages were opened as a guest in Chromium, all without a hydration error. A later change that breaks hydration would show up as an error in the browser console, not as a failed test.
There are no load tests. The application is a demo for a handful of visitors and a crew of eight, and it is sized for that.