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:
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:
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:
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:
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.
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:
$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:
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:
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$updatesas the browser would, and calls the method with$params. It goes through your test's own client, soactingAs()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, andhasErrors()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()andassertNitro()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:
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.