Testing Filament Resources, Actions, and Form Assertions with Pest
#filament #pest #testing #laravel

Testing Filament Resources, Actions, and Form Assertions with Pest

3 min read Mohamed Said Mohamed Said

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, and assertCountTableRecords for table assertions.
  • callTableAction handles 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.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need to register the Filament panel in tests, or is the service provider enough?
For most resource tests, booting the FilamentServiceProvider and acting as an authenticated user is sufficient. If your panel uses custom middleware or auth guards, configure those in your TestCase `setUp` or use a dedicated Pest `uses()` group with a `beforeEach` that calls `$this->actingAs()` with the correct guard.
Q02 How do I test a Filament action that dispatches a queued job?
Call `Queue::fake()` before mounting the Livewire component, execute the action with `callTableAction` or `callAction`, then assert with `Queue::assertPushed(YourJob::class)`. This keeps the test fast and decoupled from the job's internals.
Q03 Can I test custom Filament form components the same way?
Yes. Custom components that extend Filament's base Field classes are exercised through `fillForm` and `assertHasFormErrors` just like built-in fields. If the component has client-side behaviour, test the PHP contract in Pest and cover the JS separately with a browser test only when necessary.

Continue reading

More Articles

View all