Why Filament Testing Deserves Its Own Strategy
Filament ships a first-class testing API built on top of Livewire's test utilities. Yet most teams either skip tests entirely or write fragile browser-level assertions. The sweet spot is the middle ground: use Filament's own assertion helpers to test intent, not implementation.
This article targets Filament v3 and v4. The core testing API is stable across both; v4 schema-based forms add a few nuances noted below.
Setting Up the Test Environment
Install the Filament test helpers (already bundled) and make sure your TestCase boots the panel:
// tests/TestCase.php
use Filament\FilamentServiceProvider;
protected function getPackageProviders($app): array
{
return [FilamentServiceProvider::class];
}
For panel-specific tests, authenticate as a user that satisfies your panel's authGuard and canAccess checks:
beforeEach(function () {
$this->actingAs(User::factory()->admin()->create());
});
Testing a List Resource
Filament exposes livewire(ListRecords::class) style helpers. Use assertCanSeeTableRecords to verify rows without touching the DOM:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
use App\Models\Order;
it('lists only the authenticated tenant orders', function () {
$visible = Order::factory()->count(3)->create(['tenant_id' => auth()->id()]);
$hidden = Order::factory()->count(2)->create(['tenant_id' => 99]);
livewire(ListOrders::class)
->assertCanSeeTableRecords($visible)
->assertCanNotSeeTableRecords($hidden);
});
assertCanSeeTableRecords compares model keys against the rendered table query — it will catch broken global scopes immediately.
Testing Table Actions
Table row actions are tested by calling callTableAction with the action name and the target record:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
it('marks an order as shipped', function () {
$order = Order::factory()->pending()->create();
livewire(ListOrders::class)
->callTableAction('mark_shipped', $order)
->assertHasNoTableActionErrors();
expect($order->fresh()->status)->toBe('shipped');
});
For actions that open a modal form, pass the form data as the third argument:
livewire(ListOrders::class)
->callTableAction('refund', $order, data: [
'reason' => 'damaged_goods',
'amount' => 49.99,
])
->assertHasNoTableActionErrors();
assertHasNoTableActionErrors checks Livewire validation state — far more reliable than asserting a flash message.
Testing Create and Edit Forms
For CreateRecord and EditRecord pages, use fillForm + call('create') or call('save'):
use App\Filament\Resources\ProductResource\Pages\CreateProduct;
it('creates a product with valid data', function () {
livewire(CreateProduct::class)
->fillForm([
'name' => 'Ergonomic Chair',
'price' => 299,
'sku' => 'CHAIR-001',
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', ['sku' => 'CHAIR-001']);
});
it('requires a unique sku', function () {
Product::factory()->create(['sku' => 'CHAIR-001']);
livewire(CreateProduct::class)
->fillForm(['sku' => 'CHAIR-001', 'name' => 'Dupe', 'price' => 10])
->call('create')
->assertHasFormErrors(['sku' => 'unique']);
});
v4 Schema API Note
In Filament v4, forms are defined via Schema::make() rather than Form::make(). The fillForm / assertHasFormErrors helpers remain unchanged — the schema change is internal to the component, not to the test surface.
Testing Header Actions
Page-level header actions (e.g., an "Export" button on a list page) use callAction:
livewire(ListOrders::class)
->callAction('export', data: ['format' => 'csv'])
->assertDispatched('download-ready');
Asserting Authorization
Filament respects policy methods. Test that unauthorized users cannot reach an action:
it('prevents non-admins from deleting orders', function () {
$this->actingAs(User::factory()->create()); // no admin role
$order = Order::factory()->create();
livewire(ListOrders::class)
->assertTableActionHidden('delete', $order);
});
assertTableActionHidden checks the action's visible / authorize callback — it does not make an HTTP request.
Key Takeaways
- Use
assertCanSeeTableRecords/assertCanNotSeeTableRecordsto validate query scopes, not rendered HTML. callTableActionwith a data array covers modal-form actions in a single assertion chain.assertHasNoFormErrors/assertHasFormErrorsmap directly to Laravel validation rule names.assertTableActionHiddentests policy-driven visibility without mocking the gate.- The Filament testing API is stable across v3 and v4; schema internals do not affect test syntax.
- Keep tests focused on one behavior per
it()block — Filament components carry a lot of state.