Why Testing Filament Panels Is Different
Filament v4 renders UI through Livewire components. That means your test suite can drive the full resource lifecycle — list, create, edit, delete — without a browser, using Livewire::test() and Filament's own test helpers. The trick is knowing which layer to assert against.
Setting Up the Test Environment
Install the Filament testing utilities alongside Pest:
composer require --dev pestphp/pest pestphp/pest-plugin-livewire
In TestCase.php, authenticate as a panel user before each test:
use App\Models\User;
use Filament\Facades\Filament;
beforeEach(function () {
$user = User::factory()->create();
$this->actingAs($user);
Filament::setCurrentPanel(Filament::getPanel('admin'));
});
Setting the current panel is essential — without it, policy checks and navigation guards resolve against the wrong context.
Testing a List Resource Page
use App\Filament\Resources\OrderResource\Pages\ListOrders;
use Filament\Tables\Actions\DeleteAction;
it('renders the orders list', function () {
livewire(ListOrders::class)
->assertSuccessful()
->assertCountTableRecords(5);
});
it('can delete an order from the table', function () {
$order = Order::factory()->create();
livewire(ListOrders::class)
->callTableAction(DeleteAction::class, $order)
->assertHasNoTableActionErrors();
expect(Order::find($order->id))->toBeNull();
});
assertCountTableRecords counts rows after applying the default query, so it respects global scopes and any modifyQueryUsing you have on the table.
Testing Create and Edit Forms
Filament v4 uses the unified Schema API. Form fields are still addressable by their name attribute:
use App\Filament\Resources\OrderResource\Pages\CreateOrder;
it('validates required fields on create', function () {
livewire(CreateOrder::class)
->fillForm([
'customer_id' => null,
'total' => -10,
])
->call('create')
->assertHasFormErrors([
'customer_id' => 'required',
'total' => 'min',
]);
});
it('creates an order with valid data', function () {
$customer = Customer::factory()->create();
livewire(CreateOrder::class)
->fillForm([
'customer_id' => $customer->id,
'total' => 150.00,
'status' => 'pending',
])
->call('create')
->assertHasNoFormErrors();
expect(Order::where('customer_id', $customer->id)->exists())->toBeTrue();
});
Nested Repeater Fields
Repeater state is passed as an indexed array:
->fillForm([
'line_items' => [
['product_id' => 1, 'qty' => 2],
['product_id' => 3, 'qty' => 1],
],
])
Testing Custom Table Actions
Custom actions registered with Action::make('approve') are callable by name:
it('approves an order', function () {
$order = Order::factory()->pending()->create();
livewire(ListOrders::class)
->callTableAction('approve', $order, data: [
'note' => 'Looks good',
])
->assertNotified();
expect($order->fresh()->status)->toBe('approved');
});
assertNotified() checks that a Filament notification was dispatched — a clean proxy for "the action completed without throwing".
Testing Infolists
Filament v4 infolists are also Livewire-driven. Assert entry state on the view page:
use App\Filament\Resources\OrderResource\Pages\ViewOrder;
it('displays the order total in the infolist', function () {
$order = Order::factory()->create(['total' => 299.99]);
livewire(ViewOrder::class, ['record' => $order->getRouteKey()])
->assertSuccessful()
->assertInfolists()
->assertInfolists(fn ($infolist) =>
$infolist->has('total')
);
});
Note:
assertInfolistswith a closure is available in Filament v4's test helpers. For entry values, read the component state directly via->get('infolist.total')if the helper isn't available yet in your build.
Key Takeaways
- Always call
Filament::setCurrentPanel()inbeforeEach— panel context affects auth, navigation, and policies. - Use
assertHasFormErrorswith field-rule pairs for precise validation assertions. callTableActionaccepts adataarray for modal-based actions.assertCountTableRecordsrespects your resource's base query, including global scopes.- Test infolist entries via the view page Livewire component, not a separate render path.
- Keep one assertion per test; Filament's Livewire layer is fast enough that granular tests don't slow CI meaningfully.