Why Filament Tests Are Worth the Effort
Filament panels generate a lot of implicit behaviour — authorization checks, form validation, table bulk actions, and page redirects — all wired together through Livewire. Skipping tests means every refactor is a gamble. The good news: Filament ships with first-class Pest/PHPUnit helpers that let you drive the entire panel through HTTP and Livewire assertions, no Dusk required.
Project Setup
Install the testing helpers if you haven't already:
composer require --dev pestphp/pest pestphp/pest-plugin-laravel
Filament's test helpers live in Filament\Testing and are available on the livewire() helper once the panel is booted. Add a base TestCase that boots your panel:
// tests/TestCase.php
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->admin()->create());
}
Testing a Resource List Page
The simplest assertion confirms the page renders and shows records:
use App\Filament\Resources\OrderResource;
use App\Models\Order;
use function Pest\Laravel\get;
it('renders the order list page', function () {
Order::factory()->count(3)->create();
livewire(OrderResource\Pages\ListOrders::class)
->assertCanSeeTableRecords(Order::all());
});
assertCanSeeTableRecords checks that the Livewire component's table query returns those models — it does not assert rendered HTML, so it stays fast.
Asserting Table Actions
Table row actions and bulk actions each have dedicated helpers:
it('marks an order as shipped via table action', function () {
$order = Order::factory()->pending()->create();
livewire(OrderResource\Pages\ListOrders::class)
->callTableAction('mark_shipped', $order)
->assertHasNoTableActionErrors();
expect($order->fresh()->status)->toBe('shipped');
});
it('bulk-deletes selected orders', function () {
$orders = Order::factory()->count(5)->create();
livewire(OrderResource\Pages\ListOrders::class)
->callTableBulkAction('delete', $orders)
->assertHasNoTableActionErrors();
expect(Order::count())->toBe(0);
});
callTableAction accepts the action name (the string you pass to Action::make()) and the target record. If the action opens a modal form, pass data as the third argument.
Form Field Assertions on Create/Edit Pages
it('creates an order with valid data', function () {
livewire(OrderResource\Pages\CreateOrder::class)
->fillForm([
'customer_id' => Customer::factory()->create()->id,
'total' => 199.99,
'status' => 'pending',
])
->call('create')
->assertHasNoFormErrors();
expect(Order::count())->toBe(1);
});
it('fails validation when total is negative', function () {
livewire(OrderResource\Pages\CreateOrder::class)
->fillForm(['total' => -10])
->call('create')
->assertHasFormErrors(['total' => 'min']);
});
assertHasFormErrors accepts either a plain field name or a field => rule pair, matching Laravel's validation key format.
Testing Authorization Inside Filament
Filament respects canCreate, canEdit, and canDelete policy methods. Test them explicitly:
it('hides the create button for viewers', function () {
$this->actingAs(User::factory()->viewer()->create());
livewire(OrderResource\Pages\ListOrders::class)
->assertActionHidden('create');
});
This prevents silent authorization regressions when policies change.
Infolist Assertions (Filament v4)
Filament v4 infolists are also testable:
it('displays the order total in the infolist', function () {
$order = Order::factory()->create(['total' => 250.00]);
livewire(OrderResource\Pages\ViewOrder::class, ['record' => $order->id])
->assertInfolists()
->assertInfolists(['total' => '$250.00']);
});
Key Takeaways
- Use
livewire(PageClass::class)— never raw HTTP — for Filament page tests. assertCanSeeTableRecordschecks query results, not DOM; it's fast and reliable.- Pass action data as the third argument to
callTableActionfor modal-driven actions. assertHasFormErrors(['field' => 'rule'])catches specific validation failures cleanly.- Always test authorization explicitly; policy regressions are silent without it.
- Filament v4 infolist helpers follow the same pattern as form helpers.