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

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 assertCanSeeTableRecords and assertCanNotSeeTableRecords instead of asserting raw HTML.
  • callTableAction / callTableBulkAction are the stable API for row and bulk actions.
  • fillForm + assertHasNoFormErrors / assertHasFormErrors covers the full validation surface.
  • Always set the active panel in beforeEach for multi-panel apps to avoid resolver exceptions.
  • Test the database state after actions — Livewire assertions confirm UI, not persistence.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do Filament tests require a real database or can I use mocks?
Filament's testing API resolves Eloquent models directly, so you need a real (or in-memory SQLite) database. Use RefreshDatabase in your Pest test suite and factories to seed state. Mocking Eloquent at this layer adds more complexity than it saves.
Q02 How do I test a table filter in Filament?
Use `filterTable('filter_name', ['value' => 'x'])` on the Livewire test instance, then follow up with `assertCanSeeTableRecords` or `assertCanNotSeeTableRecords` to verify the filtered result set.
Q03 Can I test custom Filament actions that dispatch jobs or events?
Yes. Wrap the `callAction` call between `Queue::fake()` / `Event::fake()` and then assert with `Queue::assertPushed()` or `Event::assertDispatched()` as you would in any Laravel feature test.

Continue reading

More Articles

View all