Why Filament Testing Feels Hard (and How to Fix It)
Filament generates a lot of UI surface area from a small amount of PHP. That density is its superpower in production, but it makes engineers reach for browser tests when unit or feature tests would do. The result is slow, flaky suites.
The good news: Filament ships first-class testing helpers built on top of Livewire's testing API. Pair those with Pest's expressive syntax and you get a fast, readable suite that catches real regressions.
Setting Up the Test Environment
Install the testing peer dependencies and make sure your TestCase boots the Filament panel:
// tests/TestCase.php
use Filament\FilamentServiceProvider;
protected function getPackageProviders($app): array
{
return [FilamentServiceProvider::class];
}
For panel-based apps (the common case), authenticate as a user that passes your panel's authGuard before every resource test:
// tests/Pest.php
use App\Models\User;
uses(Tests\TestCase::class)->beforeEach(function () {
$this->actingAs(User::factory()->create());
})->in('Feature/Filament');
Testing a List Table
Filament's livewire() helper mounts the resource's ListRecords page. Assert that records appear and that columns render expected values:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
use App\Models\Order;
it('lists orders on the table', function () {
$orders = Order::factory(3)->create();
livewire(ListOrders::class)
->assertCanSeeTableRecords($orders)
->assertCountTableRecords(3);
});
Need to verify a specific column value? Use assertTableColumnStateSet:
livewire(ListOrders::class)
->assertTableColumnStateSet('status', 'pending', record: $orders->first());
Testing Table Actions
Table actions are where most bugs hide. Test the full lifecycle — mounting, filling a form inside the action modal, and asserting the side-effect:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
it('cancels an order via table action', function () {
$order = Order::factory()->create(['status' => 'pending']);
livewire(ListOrders::class)
->callTableAction('cancel', $order, data: [
'reason' => 'Customer request',
])
->assertHasNoTableActionErrors();
expect($order->fresh()->status)->toBe('cancelled');
});
For actions that should be hidden from certain users, assert visibility separately:
it('hides cancel action for non-admin', function () {
$this->actingAs(User::factory()->create(['role' => 'viewer']));
$order = Order::factory()->create();
livewire(ListOrders::class)
->assertTableActionHidden('cancel', $order);
});
Testing Create and Edit Forms
Mount the CreateRecord or EditRecord page and use fillForm + call('create') or call('save'):
use App\Filament\Resources\OrderResource\Pages\CreateOrder;
it('creates an order with valid data', function () {
livewire(CreateOrder::class)
->fillForm([
'customer_id' => Customer::factory()->create()->id,
'total' => 9999,
'status' => 'pending',
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('orders', ['total' => 9999]);
});
it('validates required fields on create', function () {
livewire(CreateOrder::class)
->fillForm(['total' => null])
->call('create')
->assertHasFormErrors(['total' => 'required']);
});
Testing Page-Level Actions (Header Actions)
Header actions like "Export" or "Import" live on the page, not the table:
livewire(ListOrders::class)
->callAction('export')
->assertDispatched('filament.notifications');
Key Takeaways
- Mount Filament pages with
livewire(PageClass::class)— no browser required. - Use
assertCanSeeTableRecords,assertTableColumnStateSet, andassertCountTableRecordsfor table assertions. callTableActionhandles the full action lifecycle including modal forms.- Assert hidden/disabled actions explicitly for authorization coverage.
- Keep one assertion per test; Pest's
expect()chaining reads cleanly alongside Livewire assertions. - Always call
->fresh()on Eloquent models after mutations — Livewire tests don't refresh your local variable.