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

Testing Filament Resources, Actions, and Form Assertions with Pest

4 min read Mohamed Said Mohamed Said

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 / assertCanNotSeeTableRecords to validate query scopes, not rendered HTML.
  • callTableAction with a data array covers modal-form actions in a single assertion chain.
  • assertHasNoFormErrors / assertHasFormErrors map directly to Laravel validation rule names.
  • assertTableActionHidden tests 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.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need a real database for Filament Pest tests?
Yes. Filament's testing helpers execute real Eloquent queries through Livewire, so you need either an in-memory SQLite database or a dedicated test database. Use the `RefreshDatabase` or `LazilyRefreshDatabase` trait in your base test case.
Q02 How do I test a Filament action that dispatches a queued job?
Fake the queue before calling the action: `Queue::fake()`. After `callTableAction`, assert with `Queue::assertPushed(MyJob::class)`. Because Filament actions run synchronously in tests, the job is dispatched but not executed, letting you assert dispatch intent cleanly.
Q03 Can I test custom Filament widgets with the same helpers?
Widgets are standard Livewire components, so you use `livewire(MyWidget::class)` directly. Filament does not add extra assertion helpers for widgets, but Livewire's `assertSee`, `assertSet`, and `assertDispatched` cover most widget testing needs.

Continue reading

More Articles

View all