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

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

3 min read Mohamed Said Mohamed Said

Why Testing Filament Panels Is Different

Filament v4 renders UI through Livewire components. That means your test suite can drive the full resource lifecycle — list, create, edit, delete — without a browser, using Livewire::test() and Filament's own test helpers. The trick is knowing which layer to assert against.


Setting Up the Test Environment

Install the Filament testing utilities alongside Pest:

composer require --dev pestphp/pest pestphp/pest-plugin-livewire

In TestCase.php, authenticate as a panel user before each test:

use App\Models\User;
use Filament\Facades\Filament;

beforeEach(function () {
    $user = User::factory()->create();
    $this->actingAs($user);
    Filament::setCurrentPanel(Filament::getPanel('admin'));
});

Setting the current panel is essential — without it, policy checks and navigation guards resolve against the wrong context.


Testing a List Resource Page

use App\Filament\Resources\OrderResource\Pages\ListOrders;
use Filament\Tables\Actions\DeleteAction;

it('renders the orders list', function () {
    livewire(ListOrders::class)
        ->assertSuccessful()
        ->assertCountTableRecords(5);
});

it('can delete an order from the table', function () {
    $order = Order::factory()->create();

    livewire(ListOrders::class)
        ->callTableAction(DeleteAction::class, $order)
        ->assertHasNoTableActionErrors();

    expect(Order::find($order->id))->toBeNull();
});

assertCountTableRecords counts rows after applying the default query, so it respects global scopes and any modifyQueryUsing you have on the table.


Testing Create and Edit Forms

Filament v4 uses the unified Schema API. Form fields are still addressable by their name attribute:

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

it('validates required fields on create', function () {
    livewire(CreateOrder::class)
        ->fillForm([
            'customer_id' => null,
            'total' => -10,
        ])
        ->call('create')
        ->assertHasFormErrors([
            'customer_id' => 'required',
            'total' => 'min',
        ]);
});

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

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

    expect(Order::where('customer_id', $customer->id)->exists())->toBeTrue();
});

Nested Repeater Fields

Repeater state is passed as an indexed array:

->fillForm([
    'line_items' => [
        ['product_id' => 1, 'qty' => 2],
        ['product_id' => 3, 'qty' => 1],
    ],
])

Testing Custom Table Actions

Custom actions registered with Action::make('approve') are callable by name:

it('approves an order', function () {
    $order = Order::factory()->pending()->create();

    livewire(ListOrders::class)
        ->callTableAction('approve', $order, data: [
            'note' => 'Looks good',
        ])
        ->assertNotified();

    expect($order->fresh()->status)->toBe('approved');
});

assertNotified() checks that a Filament notification was dispatched — a clean proxy for "the action completed without throwing".


Testing Infolists

Filament v4 infolists are also Livewire-driven. Assert entry state on the view page:

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

it('displays the order total in the infolist', function () {
    $order = Order::factory()->create(['total' => 299.99]);

    livewire(ViewOrder::class, ['record' => $order->getRouteKey()])
        ->assertSuccessful()
        ->assertInfolists()
        ->assertInfolists(fn ($infolist) =>
            $infolist->has('total')
        );
});

Note: assertInfolists with a closure is available in Filament v4's test helpers. For entry values, read the component state directly via ->get('infolist.total') if the helper isn't available yet in your build.


Key Takeaways

  • Always call Filament::setCurrentPanel() in beforeEach — panel context affects auth, navigation, and policies.
  • Use assertHasFormErrors with field-rule pairs for precise validation assertions.
  • callTableAction accepts a data array for modal-based actions.
  • assertCountTableRecords respects your resource's base query, including global scopes.
  • Test infolist entries via the view page Livewire component, not a separate render path.
  • Keep one assertion per test; Filament's Livewire layer is fast enough that granular tests don't slow CI meaningfully.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need a real database to test Filament resources with Pest?
Yes. Filament's test helpers drive actual Livewire components that query Eloquent models, so you need a database. Use RefreshDatabase or a SQLite in-memory connection in phpunit.xml for fast, isolated runs.
Q02 How do I test a Filament action that opens a modal with a form?
Use `callTableAction('actionName', $record, data: [...])` and pass the modal form values in the `data` array. Follow up with `assertHasNoTableActionErrors()` to confirm the form passed validation.
Q03 Can I test Filament pages that require specific permissions?
Yes. Create a user with the required roles or permissions before the test, authenticate with `actingAs`, and set the panel context. Filament resolves policies against the authenticated user, so your permission checks run as normal.

Continue reading

More Articles

View all