Testing Filament Resources, Actions, and Form Assertions with Pest
Filament ships with a first-class testing API built on top of Livewire's test utilities. Yet most teams either skip tests entirely or write fragile assertions against rendered HTML. This article shows you how to write meaningful, maintainable Pest tests for Filament resources — covering list pages, table actions, create/edit forms, and infolist assertions.
Setting Up the Test Environment
Filament tests need an authenticated user and an active panel. Use the actingAs helper combined with filament()->setCurrentPanel():
use App\Models\User;
use function Pest\Laravel\actingAs;
beforeEach(function () {
actingAs(User::factory()->create());
});
For multi-panel setups, resolve the panel explicitly:
use Filament\Facades\Filament;
beforeEach(function () {
$panel = Filament::getPanel('admin');
Filament::setCurrentPanel($panel);
actingAs(User::factory()->create());
});
Testing the List Page
Filament exposes livewire() helpers that understand resource routing. Use Livewire::test() with the resource's list page class:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
use Livewire\Livewire;
it('renders the orders list', function () {
Order::factory()->count(3)->create();
Livewire::test(ListOrders::class)
->assertCanSeeTableRecords(Order::all());
});
assertCanSeeTableRecords checks that the Eloquent models are present in the rendered table — no CSS selectors required.
Asserting Table Actions
Table row actions are tested by passing the action name and the target record:
it('soft-deletes an order via table action', function () {
$order = Order::factory()->create();
Livewire::test(ListOrders::class)
->callTableAction('delete', $order)
->assertHasNoTableActionErrors();
expect($order->fresh()->deleted_at)->not->toBeNull();
});
For bulk actions, use callTableBulkAction:
it('exports selected orders', function () {
$orders = Order::factory()->count(5)->create();
Livewire::test(ListOrders::class)
->callTableBulkAction('export', $orders)
->assertHasNoTableActionErrors();
});
Testing Create and Edit Forms
Form assertions use fillForm and assertHasNoFormErrors. Target the specific form by name when a page has multiple:
use App\Filament\Resources\OrderResource\Pages\CreateOrder;
it('creates an order with valid data', function () {
$customer = Customer::factory()->create();
Livewire::test(CreateOrder::class)
->fillForm([
'customer_id' => $customer->id,
'status' => 'pending',
'total' => 199.99,
])
->call('create')
->assertHasNoFormErrors();
expect(Order::where('customer_id', $customer->id)->exists())->toBeTrue();
});
Validation failures are equally straightforward:
it('requires a customer', function () {
Livewire::test(CreateOrder::class)
->fillForm(['status' => 'pending'])
->call('create')
->assertHasFormErrors(['customer_id' => 'required']);
});
Testing Header Actions and Modals
Header actions (e.g., a custom "Approve" action with a confirmation modal) are called via callAction:
use App\Filament\Resources\OrderResource\Pages\EditOrder;
it('approves an order through the header action', function () {
$order = Order::factory()->state(['status' => 'pending'])->create();
Livewire::test(EditOrder::class, ['record' => $order->getRouteKey()])
->callAction('approve')
->assertHasNoActionErrors();
expect($order->fresh()->status)->toBe('approved');
});
If the action opens a modal with its own form, chain fillActionForm before callAction:
->fillActionForm(['reason' => 'Verified by finance'])
->callAction('approve')
Infolist Assertions (Filament v4)
Filament v4 infolists expose assertSeeInInfolist:
use App\Filament\Resources\OrderResource\Pages\ViewOrder;
it('displays the order total in the infolist', function () {
$order = Order::factory()->create(['total' => 250.00]);
Livewire::test(ViewOrder::class, ['record' => $order->getRouteKey()])
->assertSeeInInfolist('order_details', '250');
});
Key Takeaways
- Use
assertCanSeeTableRecordsandassertCanNotSeeTableRecordsinstead of asserting raw HTML. callTableAction/callTableBulkActionare the stable API for row and bulk actions.fillForm+assertHasNoFormErrors/assertHasFormErrorscovers the full validation surface.- Always set the active panel in
beforeEachfor multi-panel apps to avoid resolver exceptions. - Test the database state after actions — Livewire assertions confirm UI, not persistence.