Nitro

Going further

Testing

A component is a PHP class, so you test its methods like any other PHP: create it, call a method, and assert on the result. To check what a page sends the browser, use assertNitro().

On this page

Testing methods as plain PHP

A browser method is ordinary PHP that Nitro also compiles for the browser, and both versions give the same results. That means you can test it with PHPUnit directly, without a browser. This test checks the cart from these docs:

PHP Browser
use App\Nitro\Components\Cart;

public function test_delivery_is_free_from_30(): void
{
    $cart = new Cart;

    $cart->add(0);    // Notebook, 4
    $this->assertSame(5, $cart->delivery());

    $cart->add(2);    // Backpack, 35
    $this->assertSame(0, $cart->delivery());
}

public function test_the_postcode_is_tidied(): void
{
    $cart = new Cart;

    $cart->updatedPostcode('  sw1a   1aa ');

    $this->assertSame('SW1A 1AA', $cart->postcode);
}

A #[Server] method runs on the server, with your session, your user and your database. In a test, you call it like any other method:

PHP Server
use App\Nitro\Components\Counter;

public function test_save_keeps_the_count(): void
{
    $counter = new Counter;
    $counter->count = 7;

    $counter->save();

    $this->assertSame(7, session('counter.count'));
    $this->assertNotSame('', $counter->savedAt);
}

Validation

$this->validate() in a #[Server] method checks the component's #[Validate] rules, and throws Laravel's ValidationException when one fails, as it does in a request. Assert that it is thrown, or read its messages:

PHP Server
use App\Nitro\Components\PostEditor;
use Illuminate\Validation\ValidationException;

public function test_a_post_needs_a_title(): void
{
    $editor = new PostEditor;
    $editor->title = '';

    $this->assertThrows(fn () => $editor->save(), ValidationException::class);
}

public function test_the_messages_are_the_ones_shown(): void
{
    $editor = new PostEditor;
    $editor->title = 'Hi';

    try {
        $editor->save();
        $this->fail('A two-letter title was accepted.');
    } catch (ValidationException $exception) {
        $this->assertSame(['The title field must be at least 3 characters.'], $exception->errors()['title']);
    }
}

Pages with mount()

A page's mount() takes what its route gives it, such as a bound model. Call it with the same, and assert on the properties it set:

PHP Server
use App\Nitro\Pages\Posts\Show;
use App\Models\Post;

public function test_a_post_page_shows_its_comments(): void
{
    $post = Post::factory()->hasComments(2)->create();

    $page = new Show;
    $page->mount($post);    // as the route calls it, with the bound model

    $this->assertCount(2, $page->comments);
}

Testing pages

assertNitro() reads the page from a response, whether the page was rendered in full or sent as the answer to a visit. You can then assert on its name and its state. Use dot notation for keys.

PHP
use Nitro\Testing\AssertableNitro;

public function test_the_orders_page(): void
{
    $this->get('/orders')->assertOk()->assertNitro(fn (AssertableNitro $page) => $page
        ->page('pages::orders.index')
        ->has('orders', 12)
        ->where('orders.0.status', 'new')
        ->where('total', fn ($total) => $total > 0)
        ->missing('secret')
        ->deferred('stats'));
}
Method Asserts
page($name) The page is the one with this name ( pages::orders.index )
has($key, $count) The state has the key. With a count, it is an array with that many items
missing($key) The state doesn't have the key, so it never reaches the browser
where($key, $expected) The value is the one given, or passes the given closure
deferred(...$properties) The page leaves these properties for later
notDeferred() The page leaves nothing for later
hasErrors(...$keys) There are validation messages for these properties ( form.email ), or any, when none are given
hasNoErrors() There are no validation messages
state($key) Returns the state, or one value of it
name() Returns the page's name

To write your own assertions, nitroState() returns the state itself:

PHP
$orders = $this->get('/orders')->nitroState('orders');

$this->assertCount(12, $orders);

Visits and deferred properties

A nitro:navigate visit asks for the page's data rather than its HTML, and a page with $this->defer() properties is asked again for them once it shows. Send the same headers the browser does, and assertNitro() reads either answer:

PHP
use Nitro\Testing\AssertableNitro;

public function test_the_report_loads_its_stats_after_it_shows(): void
{
    // The page as it first arrives: the stats come later.
    $this->get('/report')->assertNitro(fn (AssertableNitro $page) => $page
        ->deferred('stats'));

    // A nitro:navigate visit: the page's data instead of its HTML.
    $this->withHeaders(['X-Nitro-Navigate' => '1', 'Accept' => 'application/json'])
        ->get('/report')
        ->assertNitro(fn (AssertableNitro $page) => $page->page('pages::report'));

    // The browser's next request: the stats themselves (a partial reload).
    $this->withHeaders([
        'X-Nitro-Navigate' => '1',
        'X-Nitro-Partial' => 'fabcdefghij',   // the page's id in the browser
        'X-Nitro-Only' => 'stats',
        'Accept' => 'application/json',
    ])->get('/report')->assertNitro(fn (AssertableNitro $page) => $page
        ->notDeferred()
        ->has('stats.orders'));
}

Calling a #[Server] method as the browser does

Calling a method on the class tests what it does. To test the call itself, with the signed state, the #[Validate] rules and the page's middleware, send it the way the browser does. Add InteractsWithNitro to your test, fetch the page, and call:

PHP
use Nitro\Testing\AssertableNitro;
use Nitro\Testing\InteractsWithNitro;

class PostPageTest extends TestCase
{
    use InteractsWithNitro, RefreshDatabase;

    public function test_a_comment_is_added(): void
    {
        $post = Post::factory()->create();
        $page = $this->actingAs(User::factory()->create())->get("/posts/{$post->id}");

        $this->callNitro($page, 'addComment', updates: ['comment' => 'Nice post'])
            ->assertOk()
            ->assertNitro(fn (AssertableNitro $state) => $state->hasNoErrors()->where('comment', ''));

        $this->assertSame(1, $post->comments()->count());
    }

    public function test_an_empty_comment_is_refused(): void
    {
        $page = $this->actingAs(User::factory()->create())->get('/posts/'.Post::factory()->create()->id);

        $this->callNitro($page, 'addComment', updates: ['comment' => ''])
            ->assertNitro(fn (AssertableNitro $state) => $state->hasErrors('comment'));
    }
}
  • callNitro($page, $method, $params, $updates) takes the page's signed state from the response, sets the properties in $updates as the browser would, and calls the method with $params. It goes through your test's own client, so actingAs() and the session hold.
  • A component on the page rather than the page itself: name it, callNitro($page, 'save', component: 'posts.comment-form').
  • The answer reads with assertNitro(), as a page does: the new state, and hasErrors() when a rule refused it. A refused call still answers 200, with its messages.
  • A production build on your machine (after nitro:build) names methods and properties by aliases. callNitro() and assertNitro() translate them, so your tests keep your names.

Catching what can't run in the browser

A browser method or a view that uses something the browser can't run fails the build, with the file and line. Run the build in your CI, so such code never reaches production:

Terminal
php artisan nitro:build

It exits with an error when a component doesn't compile, and lists what it compiled when they all do: how many components, stores and pages, and what their views are written in.

What the browser sees

The state is exactly what the browser receives. A missing() assertion for each secret a page might hold is a cheap way to make sure it stays on the server.

Next steps