protonemedia/inertiajs-events-laravel-dusk
Laravel Dusk helpers for testing Inertia.js events. Adds assertions and utilities so you can verify Inertia events fire as expected in browser tests, making it easier to cover Inertia-powered Laravel flows end to end.
This is a test-only helper. It does not ship anything to production; it exists so Dusk tests can wait on Inertia's client-side lifecycle events instead of falling back to ->pause() or guessing at selectors.
Install (dev only):
composer require --dev protonemedia/inertiajs-events-laravel-dusk
Version 1.3.0 changed the support floor — check you clear it before installing:
< 8.1)< 9)If your app is still on Laravel 8 or PHP 8.0, Composer will refuse to resolve 1.3.0; pin ^1.2 instead (or skip the package and use the hand-rolled macro in Gotchas).
Laravel package discovery registers the service provider; there is nothing to publish for a basic setup. Confirm it appears in vendor/protonemedia/inertiajs-events-laravel-dusk/src/ and that your DuskTestCase is otherwise stock.
Where to look first:
src/ — the service provider shows which Dusk Browser macros are registered (and therefore whether a wait is armed on page load or only when called).tests/ in the package — the clearest examples of intended usage, and now the best evidence that the macros still work against Dusk 7.composer.json — the require block is the authoritative support matrix (PHP ^8.1, laravel/framework ^9|^10, laravel/dusk ^7). Cite that, not the release notes, when debugging resolution errors.First use case — replace a sleep after an Inertia link click:
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard')
->clickLink('Settings')
->waitForInertiaEvent('success') // confirm exact macro name in README
->assertPathIs('/settings')
->assertSee('Account settings');
});
The win is determinism: the wait resolves when Inertia finishes the visit, not after an arbitrary timeout, so fast machines don't waste time and slow CI agents don't flake.
Wait, then assert — always in that order. Treat the Inertia event as the barrier and put every DOM assertion after it. This is the single pattern that eliminates most Inertia/Dusk flake:
$browser->click('@save-button')
->waitForInertiaEvent('success')
->assertSee('Saved');
Distinguish the phases of a visit. Inertia emits a sequence (start/navigate, then progress, then success or error, then finish). Use:
success when you care that the page visited and the response was OK.error when asserting a failed visit (500s, network failure).finish when you only care the request settled and don't need success/failure semantics.start/progress rarely — mostly for testing loading indicators.Use it for validation round-trips too. An Inertia form POST that returns a 422 will settle as a non-success event; wait for the settle event and then assert on the validation message. This is far more reliable than waitForText('The email field is required') alone, because the wait fires even when the response is fast.
Wrap it in a trait or Page object. You will repeat this everywhere, so centralize:
trait WaitsForInertia
{
protected function waitForVisit(Browser $browser, int $seconds = 10): Browser
{
return $browser->waitForInertiaEvent('success', $seconds);
}
}
class SettingsPage extends Page
{
public function save(): static
{
return $this->click('@save')->waitForVisit($this->browser);
}
}
Pin the whole test stack, not just the package. With 1.3.0 the package now sits inside the Laravel 9/10 + Dusk 7 + PHP 8.1/8.2 window. Pin laravel/dusk ^7, phpunit and your CI PHP image to the same minors you develop on; the laravel/framework constraint moving is what will silently bump Dusk under you.
Pair with path/URL assertions. After an Inertia visit, assertPathIs() proves the navigation actually completed, while the event proves it finished. Together they catch "the click didn't register" versus "the response was a redirect back."
Parallel Dusk (dusk --parallel) works fine because each browser has its own page context; just make sure any event-armed state is per-page, not shared global config.
Upgrading from 1.2.x: the constraint change is the breaking change. There is no API change in 1.3.0, but dropping Laravel < 9 and PHP < 8.1 means a composer update can now leave you on a version your app can't install — check composer why-not protonemedia/inertiajs-events-laravel-dusk 1.3.0 before blaming the code. In a mixed monorepo, some services may need ^1.2 while others take ^1.3.
The race is the whole game. If the listener is armed only when you call waitForInertiaEvent(), the event may already have fired and you'll hang until the timeout. Inspect the service provider: if waits are armed lazily, arm the wait before the interaction, or pre-inject the listener yourself (see fallback below). This is the most common cause of "it works locally, times out in CI."
Still no Inertia version guarantee. 1.3.0 refreshed Laravel/Dusk/PHP but said nothing about Inertia v1/v2. Inertia renamed and restructured its events (the inertia:navigate family), so a macro keyed to older event names will silently never resolve — a green-looking stack that always times out. Pin your @inertiajs/vue3/react version in package.json if this package is load-bearing, or migrate off it (below).
Maintenance is still a question mark, not a solved problem. One release moving the support matrix forward is not a roadmap. Before you double down, check the repo's issue/PR activity and whether a Dusk 8 or Laravel 11 bump is plausible; budget the escape hatch below as a fallback rather than a hypothetical.
error is overloaded. Inertia can surface validation errors and network failures through the same event family. If you need to distinguish a 422 from a 500, assert on the rendered error message or the response status recorded in the event detail, not just the event name.
Partial reloads fire events too. A link with only: [...] or a manual router.reload() still emits start/success. Don't assume an event means a full page change; combine with assertPathIs when navigation is the thing under test.
Timeouts default to Dusk's 5 s. Cold CI, queue-backed responses, or a slow asset graph will blow that. Pass an explicit timeout (->waitForInertiaEvent('success', 15)) rather than sprinkling pause() calls.
Debug what the page actually emitted. Dusk can execute JS and return values, so dump the recorded events instead of guessing:
dump($browser->script('return window.__inertiaEvents ?? [];'));
If the array is empty, the listener was never attached or your event name is wrong — not a timing problem.
Extension point / escape hatch. If the package lags your stack again (Dusk 8, Laravel 11, Inertia v2), the whole mechanism is ~10 lines and you can own it:
// In a DuskTestCase setUp() or a service provider:
Browser::macro('armInertiaEvents', function () {
$this->script(<<<'JS'
window.__inertiaEvents = [];
['navigate','start','progress','success','error','finish','invalid']
.forEach(name => document.addEventListener('inertia:' + name,
() => window.__inertiaEvents.push(name)));
JS);
return $this;
});
Browser::macro('waitForInertiaEvent', function (string $event, int $seconds = 5) {
return $this->waitUntil(
"Array.isArray(window.__inertiaEvents) && window.__inertiaEvents.includes('{$event}')",
$seconds
);
});
Then ->armInertiaEvents()->click(...)->waitForInertiaEvent('success') gives you the behaviour without the dependency. Note the explicit arm-then-click ordering — that's the fix for the race called out above. This path has no Laravel/Dusk/PHP ceiling, which is the actual argument for adopting it once the macro has proven itself in your suite.
Scope the dependency. Keep it in require-dev and out of production autoloading; if you use the hand-rolled macro instead, delete the package entirely so future Laravel/Dusk upgrades aren't gated on an unmaintained repo.
How can I help you explore Laravel packages today?