the whole setup · one sentence per flow · no test code

End-to-end tests for Laravel.

Dusk is good and it still needs ChromeDriver, a separate environment file, a test database and seeders that stay in step with your schema. That is why the Browser directory has three files in it. Write a sentence instead and point it at staging.

the whole setup
$ npx smolanalytics test \
    --url https://staging.yourapp.com \
    --test "a customer can add the annual plan to their subscription and see the new invoice"

It runs against whatever state that environment is in. There is no seeder to keep in step, because we do not seed data.

how do I write end-to-end tests for a Laravel app?

To get browser-level coverage on a Laravel app without maintaining Dusk, write one sentence per flow and run npx smolanalytics test --url against a running URL such as staging. An agent drives a real browser, reads the page through its accessibility tree so no selector exists to break when a Blade component is refactored, and returns passed, failed, stale or errored. There is no ChromeDriver to keep current, no .env.dusk, and no test database to migrate, because nothing runs inside your application. It does not seed data either, which is the constraint to plan around: keep a long-lived test account on staging and write the sentences for the state it is actually in, and keep your factory-driven feature tests for everything else. On Laravel it earns its place on Livewire and Blade — a wire:model binding that stops updating submits stale values with no error, a validation partial a redesign moved out of the layout leaves the user on a page that did nothing, and a queued job that never runs shows Sent regardless. On a pull request one comment says what broke, edited in place, from your own CI runner using the GITHUB_TOKEN GitHub Actions provides. Passing runs are recorded and replay with no model call at all. 14-day trial at Pro limits, no card, then $19/month.

Dusk's cost is not writing the first test, it is everything around it: a driver binary to keep current, a .env.dusk that drifts from .env, a database that has to be migrated and seeded before each run, and selectors that break when a Blade component is refactored. Nothing here touches any of that. The agent drives a browser against a URL, reads the page through its accessibility tree so there is no selector to break, and works with the state the environment already has.

That last point is a real constraint and it changes how you write the sentences. We do not seed test data. So this is not a replacement for feature tests that build a world with factories — keep those — it is a check that the deployed app works for an account that exists. Keep a long-lived test customer on staging and describe flows in terms of what is true for them.

What it catches in Laravel is mostly Livewire and Blade. A Livewire component whose wire:model binding stops updating leaves a form that looks live and submits stale values, with no error anywhere. A validation error rendered in a Blade partial that a redesign moved out of the layout, so the user sees a page that did nothing. A queued job that the flow depends on, where the page says Sent and the mail never goes. All of them are healthy from the server's side.

The second half is your tracking. In Laravel the moment worth measuring is usually an event or a listener, which is exactly what gets moved when someone tidies up. The agent has just walked the flow, so it knows the flow exists, and it writes and maintains the tracking calls in the SDK you already run — PostHog, Mixpanel, Amplitude, Google Analytics, Plausible or Segment.

How it works walks the whole loop from the sentence to the comment on the pull request, instrumentation covers the second half — the tracking calls the same walk writes and maintains — and the free tools run without an account. The docs have the flags, the suite format and the CI step.

Pro $19/mo, 100 tested pull requests included, unlimited projects, after a 14-day trial at Pro limits with no card. full pricing, including overage →

One sentence, on your Laravel app, tonight.

Point it at a URL that is already running and describe what should work. The first run uses the agent; once it passes it is recorded, so every run after that replays with no model at all — measured on our own site, 8.0s the first time and 1.4s the second.

questions

We already have Dusk tests. Should we delete them?
Keep them. The question is not whether Laravel can be browser-tested, it is whether anyone will still be maintaining the driver, the seeders and the selectors in six months. This is for the flows nobody got round to covering and for the specs that go red every time a component is renamed. When the UI changes here there is no selector to update, because the agent looks at the page again and works it out.
How does it work with Livewire's re-renders and wire:loading states?
The same way a person does: it waits for the control to be usable and then reads what is on screen. Livewire's partial DOM swaps are the classic reason a selector-based test becomes flaky, because the element it grabbed a moment ago has been replaced. Nothing here holds a reference to an element, so a swap is not an event that can break the test — only the visible outcome being wrong is.
What do I actually get back after a run on Laravel?
One of four words, and the difference between them is the product. Passed. Failed, which means your app did not do what the test describes, so it is a bug report. Stale, which means a recording no longer fits the page — a rename and a removal look identical to a replay, so this is never worded or coloured as a failure and the agent goes and works out which it was. Errored, which means our runner could not run at all: no browser, no network, no key. That one is our fault and it says so, because telling you your checkout is broken when our own runner fell over is the fastest way to lose you.

keep reading