Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Inertia Laravel Testing Laravel Package

claudiodekker/inertia-laravel-testing

Test helpers for Inertia.js apps in Laravel: assert Inertia responses, page components, props, and partial reloads without brittle manual checks.

View on GitHub
Deep Wiki
Context7

Getting Started

Check core first. Before installing anything, run:

composer show inertia-laravel | grep -i version

If your inertia-laravel already exposes assertInertia (modern versions bundle the testing helpers under Inertia\Testing\AssertableInertia), stop here — this package is redundant and archived (last release 2021-01-22). Install it only on a legacy stack where core has no testing helpers.

Install as a dev dependency:

composer require --dev claudiodekker/inertia-laravel-testing

The service provider auto-registers and adds the assertInertia macro to Laravel's TestResponse. No config to publish, no assets, no runtime footprint in production.

Look at these three things first, in order:

  1. The README's assertion table — it is the complete map of the fluent API in one screen.
  2. The AssertableInertia class in the package source — that is the object your closure receives, so its public methods (component, url, has, where, missing, etc, scope, hasAll) are the entire vocabulary you'll write tests in.
  3. The service provider — one glance shows you it's just TestResponse::macro('assertInertia', ...), which tells you exactly how to replace it later if you migrate to core.

First use case — assert the page object, not the HTML:

$response = $this->actingAs($user)->get('/users');

$response->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
    ->component('Users/Index')
    ->url('/users')
    ->has('users', 3)
    ->where('users.0.name', 'John Doe')
);

That replaces a pile of assertSee, assertViewHas, and string-matching on HTML. When the controller starts sending a different component or the prop shape drifts, you get a precise failure naming the missing prop instead of a vague "text not found".

Sanity-check the install once with a trivial assertInertia(fn ($page) => $page->component(...)) on a known route; if the closure never fires, your response isn't an Inertia response (see Gotchas).

Implementation Patterns

Treat the component name as the contract. The single highest-value assertion you can write is ->component('Users/Index'). It catches the most common Inertia regression: a controller that returns the wrong page after a refactor, a rename that didn't propagate, or a conditional branch falling through to the wrong view. Assert it on every page test, always.

Scope deep or repeated structure with closures, not dot strings. Dot-notation works for one-off values, but nested closures give better failure output and read closer to the shape you're asserting:

->has('users', fn (AssertableInertia $users) => $users
    ->has('data', 10)
    ->where('data.0.email', $user->email)
    ->etc()
)

Use etc() deliberately at the outer level. An Inertia response almost always carries shared props (auth user, flash, Ziggy routes, app name). Assertions are exhaustive by default, so without etc() every new shared prop breaks every page test. Assert the props your controller owns; allow the rest. The discipline is: etc() at the page level, tight assertions inside the props you actually care about.

Assert shared props once, in their own test. Rather than sprinkling where('auth.user.id', ...) across forty tests, write one test per shared-prop concern:

$this->actingAs($user)->get('/dashboard')
    ->assertInertia(fn ($page) => $page
        ->where('auth.user.id', $user->id)
        ->where('flash.success', 'Saved')
        ->etc()
    );

That's your contract test for HandleInertiaRequests::share() — the place where a broken merge silently ships user data or drops flash messages.

Drive partial reloads with headers. Lazy/optional props never appear on the initial load, so testing them requires simulating the partial reload the frontend performs:

$this->withHeaders([
    'X-Inertia'                   => 'true',
    'X-Inertia-Partial-Component' => 'Users/Index',
    'X-Inertia-Partial-Data'      => 'users',
])->get('/users')->assertInertia(fn ($page) => $page
    ->component('Users/Index')
    ->has('users')
    ->missing('stats')
);

This is the pattern nothing else in your stack covers: static analysis can't see it, and a normal feature test sees only the full payload. It's how you catch a Inertia::lazy() prop that never got wired up, or one that leaked into every request and is now costing you a query.

Centralise the boilerplate in your base TestCase. Every page in an Inertia app shares a layout contract, so wrap it:

protected function assertInertiaPage(TestResponse $response, string $component, ?Closure $props = null): void
{
    $response->assertOk()->assertInertia(function (AssertableInertia $page) use ($component, $props) {
        $page->component($component)->etc();
        $props?->call($this, $page);
    });
}

Now each test is one line plus the props that matter — and if the shared layout contract changes, you change one method.

Cover prop shapes with data providers. These assertions are declarative and cheap, so a Pest dataset or PHPUnit provider over [route, component, requiredProps] gives you broad coverage of your backend/frontend contract for very little code. This is the pattern that makes an Inertia version upgrade or a prop rename safe.

Handle form flows as two steps. Post, assert the redirect and session state, then follow the redirect and assert what actually reaches the page:

$this->post('/users', $payload)
    ->assertRedirect('/users')
    ->assertSessionHasNoErrors();

$this->get('/users')->assertInertia(fn ($page) => $page
    ->where('flash.success', 'User created')
    ->etc()
);

Validation failures get the mirror treatment: assert assertSessionHasErrors, then GET and confirm the errors prop is populated. The prop is what the frontend actually renders; the session assertion alone doesn't prove it got shared.

Compose with scope() for reusable fragments. If several pages render the same sub-structure (a sidebar, a stats widget), extract it to a method that takes AssertableInertia and call it from multiple tests. Because the assertable is a plain class, this is just normal PHP — no framework gymnastics.

Gotchas and Tips

  • This package is archived and unmaintained. It exists because Inertia core didn't ship testing helpers in 2021; core does now. If you're adopting fresh, use core's assertInertia. If you inherit this package, the migration is trivial: remove the dev dependency, keep your tests, and fix imports — the API was designed to become core's API.

  • Namespace collision is the trap. Both this package and modern inertia-laravel define Inertia\Testing\AssertableInertia. If methods like etc() or missing() "don't exist", you almost certainly have the wrong class loaded, not a broken install. Verify with composer show or by reflecting the imported class; the fix is an import/alias change, not a downgrade.

  • assertInertia only works on an Inertia response. Redirects (302), JSON endpoints, and error responses are not Inertia page responses. In production X-Inertia requests get a 409 on version mismatch, but in tests the usual failure is simply asserting on a redirect. Always assertOk() / assertRedirect() first, then optionally followingRedirects() or a second GET before asserting the page.

  • Assertions are exhaustive by default. Any top-level prop you don't assert or permit with etc() fails the test. This is the single biggest source of "the test broke and I changed nothing" — it's almost always a newly shared prop. Reach for etc() at the page level, and only tighten down when you're deliberately locking a contract.

  • has('users', 3) counts array entries, not models. For paginated payloads the count lives at users.data; for API resources, at the data wrapper. When a count assertion fails by an unexpected number, dump the prop before rewriting the assertion — guessing at the shape wastes more time than a 10-second var_dump.

  • A typo in X-Inertia-Partial-Component produces an empty payload that looks like a controller bug. The header must match exactly the component name the controller renders. If a partial-reload test reports everything as missing, check the header string before you touch the controller.

  • Debug by reading the payload, not by fighting the assertion. Skip the assertion temporarily and inspect what the server actually sent:

    dump($response->viewData('page')); // component, props, url, version as a plain array
    

    Write assertions against the real shape rather than iterating on failure messages.

  • It tests the payload, not the render. A Vue/React component that destructures a prop that doesn't exist, renders the wrong field, or requires a nested key you didn't send will pass every PHP assertion. Complement with a component-level test in your frontend runner for anything where props are dynamically shaped.

  • Pin exact versions and run the full suite on every bump. 2.2.5 is a second attempt at fixing the same bug from 2.2.4 — evidence of churn in the assertion layer itself. A patch bump here isn't automatically risk-free; treat it like any other dependency change during a bisect.

  • Extension points are plain Laravel. The macro is registered on TestResponse and the assertable is a normal class, so customisation means a trait on your base TestCase, a macro registered in setUp(), or composed closure helpers. There's no config file — which also means there's no config to hide behaviour in when a test surprises you.

  • Check the support matrix before committing. The package tracks specific inertia-laravel, Laravel, and PHP versions from early 2021. On an older stack it's fine; on anything current, core supersedes it and you should not carry a second source of truth for the same assertions.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox