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

Filament v3 Testing with Pest: Resources, Actions, and Form Assertions

3 min read Mohamed Said Mohamed Said

Why Filament Testing Deserves Its Own Strategy

Filament panels are Livewire components under the hood. That means standard HTTP tests miss most of what matters — form validation, action modals, bulk operations, and authorization checks all live inside component state. Pest's Livewire integration, combined with Filament's own test helpers, gives you a clean path to full coverage without brittle browser tests.


Setting the Auth Context

Every Filament resource test needs an authenticated panel user. Use actingAs before mounting the component, and make sure the user satisfies your panel's auth()->check() guard.

use App\Models\User;
use App\Filament\Resources\OrderResource\Pages\ListOrders;

it('renders the orders list for an admin', function () {
    $admin = User::factory()->admin()->create();

    livewire(ListOrders::class)
        ->actingAs($admin)
        ->assertSuccessful();
});

If your panel uses a custom guard, pass it explicitly: actingAs($admin, 'admin').


Asserting Table Columns and Records

Filament's InteractsWithTable trait exposes helpers that map directly to what the user sees.

it('shows pending orders in the table', function () {
    $admin = User::factory()->admin()->create();
    Order::factory()->pending()->count(3)->create();
    Order::factory()->completed()->count(2)->create();

    livewire(ListOrders::class)
        ->actingAs($admin)
        ->assertCountTableRecords(3); // default filter: pending
});

Use ->assertCanSeeTableRecords($records) when you need to assert specific model instances appear, and ->assertCanNotSeeTableRecords($records) for scoping and authorization checks.


Testing Table Actions

Row actions and bulk actions are where most bugs hide. Filament provides callTableAction and callTableBulkAction.

it('marks an order as shipped via row action', function () {
    $admin = User::factory()->admin()->create();
    $order = Order::factory()->pending()->create();

    livewire(ListOrders::class)
        ->actingAs($admin)
        ->callTableAction('mark_shipped', $order)
        ->assertHasNoTableActionErrors();

    expect($order->fresh()->status)->toBe(OrderStatus::Shipped);
});

For actions that open a modal with a form, pass data as the third argument:

->callTableAction('reassign', $order, data: [
    'assignee_id' => $agent->id,
])
->assertHasNoTableActionErrors();

Form Submission on Create/Edit Pages

Mount the CreateOrder or EditOrder page directly and use fillForm + call('create') or call('save').

use App\Filament\Resources\OrderResource\Pages\CreateOrder;

it('validates required fields on create', function () {
    $admin = User::factory()->admin()->create();

    livewire(CreateOrder::class)
        ->actingAs($admin)
        ->fillForm([
            'customer_id' => null,
            'total' => -10,
        ])
        ->call('create')
        ->assertHasFormErrors([
            'customer_id' => 'required',
            'total' => 'min',
        ]);
});

it('creates an order with valid data', function () {
    $admin = User::factory()->admin()->create();
    $customer = Customer::factory()->create();

    livewire(CreateOrder::class)
        ->actingAs($admin)
        ->fillForm([
            'customer_id' => $customer->id,
            'total' => 150.00,
            'status' => OrderStatus::Pending->value,
        ])
        ->call('create')
        ->assertHasNoFormErrors();

    $this->assertDatabaseHas('orders', [
        'customer_id' => $customer->id,
        'total' => 150.00,
    ]);
});

Testing Authorization Inside Resources

Filament respects policies. Test that unauthorized users are blocked at the component level, not just the route.

it('prevents non-admins from deleting orders', function () {
    $viewer = User::factory()->viewer()->create();
    $order = Order::factory()->create();

    livewire(ListOrders::class)
        ->actingAs($viewer)
        ->assertTableActionHidden('delete', $order);
});

assertTableActionHidden checks that the action is not rendered for that record — a stronger guarantee than checking HTTP status codes.


Practical Takeaways

  • Always call actingAs before mounting; Filament panels enforce auth at the component level.
  • Use assertCountTableRecords for scope/filter tests — it respects the component's default filters.
  • callTableAction with a data array covers modal-based actions without browser overhead.
  • assertHasFormErrors accepts field-rule pairs for precise validation assertions.
  • Test authorization with assertTableActionHidden / assertTableActionVisible rather than policy unit tests alone.
  • Keep each test focused on one behavior; Filament component state resets between Pest tests automatically.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need to register the Filament panel in tests?
Yes. Filament resolves resources through the panel registry, so your test environment must boot the panel. Typically this happens automatically via your AppServiceProvider or a dedicated test service provider. If resources are not found, ensure the panel's `boot` method runs during the test suite.
Q02 How do I test a custom Filament action that dispatches a job?
Fake the queue with `Queue::fake()` before calling `callTableAction`, then assert the job was pushed with `Queue::assertPushed(MyJob::class)`. The action executes synchronously inside the Livewire test, so the fake captures the dispatch correctly.
Q03 Can I test Filament notifications triggered by actions?
Yes. After calling an action, chain `->assertNotified()` or `->assertNotified('Order shipped')` to assert that a Filament notification was dispatched to the current session. This works for both success and error notifications defined inside your action's `action()` closure.

Continue reading

More Articles

View all