Expert knowledge for testing FilamentPHP 4.x resources, tables, schemas, actions, and notifications using Pest and Livewire...
This skill provides comprehensive guidance for testing FilamentPHP 4.x applications using Pest and Livewire testing utilities.
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Models\User;
use function Pest\Livewire\livewire;
it('can load the page', function () {
$users = User::factory()->count(5)->create();
livewire(ListUsers::class)
->assertOk()
->assertCanSeeTableRecords($users);
});
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Models\User;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Livewire\livewire;
it('can create a user', function () {
$newUserData = User::factory()->make();
livewire(CreateUser::class)
->fillForm([
'name' => $newUserData->name,
'email' => $newUserData->email,
])
->call('create')
->assertNotified()
->assertRedirect();
assertDatabaseHas(User::class, [
'name' => $newUserData->name,
'email' => $newUserData->email,
]);
});
use App\Filament\Resources\Users\Pages\EditUser;
use App\Models\User;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Livewire\livewire;
it('can load the page', function () {
$user = User::factory()->create();
livewire(EditUser::class, [
'record' => $user->id,
])
->assertOk()
->assertSchemaStateSet([
'name' => $user->name,
'email' => $user->email,
]);
});
it('can update a user', function () {
$user = User::factory()->create();
$newUserData = User::factory()->make();
livewire(EditUser::class, [
'record' => $user->id,
])
->fillForm([
'name' => $newUserData->name,
'email' => $newUserData->email,
])
->call('save')
->assertNotified();
assertDatabaseHas(User::class, [
'id' => $user->id,
'name' => $newUserData->name,
'email' => $newUserData->email,
]);
});
use App\Filament\Resources\Users\Pages\ViewUser;
use App\Models\User;
use function Pest\Livewire\livewire;
it('can load the page', function () {
$user = User::factory()->create();
livewire(ViewUser::class, [
'record' => $user->id,
])
->assertOk()
->assertSchemaStateSet([
'name' => $user->name,
'email' => $user->email,
]);
});
use function Pest\Livewire\livewire;
it('can render page', function () {
livewire(ListPosts::class)
->assertSuccessful();
});
use function Pest\Livewire\livewire;
it('cannot display trashed posts by default', function () {
$posts = Post::factory()->count(4)->create();
$trashedPosts = Post::factory()->trashed()->count(6)->create();
livewire(PostResource\Pages\ListPosts::class)
->assertCanSeeTableRecords($posts)
->assertCanNotSeeTableRecords($trashedPosts)
->assertCountTableRecords(4);
});
use function Pest\Livewire\livewire;
it('can search posts by title', function () {
$posts = Post::factory()->count(10)->create();
$title = $posts->first()->title;
livewire(PostResource\Pages\ListPosts::class)
->searchTable($title)
->assertCanSeeTableRecords($posts->where('title', $title))
->assertCanNotSeeTableRecords($posts->where('title', '!=', $title));
});
use function Pest\Livewire\livewire;
it('can filter users by locale', function () {
$users = User::factory()->count(5)->create();
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->filterTable('locale', $users->first()->locale)
->assertCanSeeTableRecords($users->where('locale', $users->first()->locale))
->assertCanNotSeeTableRecords($users->where('locale', '!=', $users->first()->locale));
});
use function Pest\Livewire\livewire;
it('can sort users by name', function () {
$users = User::factory()->count(5)->create();
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->sortTable('name')
->assertCanSeeTableRecords($users->sortBy('name'), inOrder: true)
->sortTable('name', 'desc')
->assertCanSeeTableRecords($users->sortByDesc('name'), inOrder: true);
});
use function Pest\Livewire\livewire;
it('shows the correct columns', function () {
livewire(PostResource\Pages\ListPosts::class)
->assertTableColumnVisible('created_at')
->assertTableColumnHidden('author');
});
use function Pest\Livewire\livewire;
it('can toggle all columns', function () {
livewire(PostResource\Pages\ListPosts::class)
->toggleAllTableColumns();
});
use function Pest\Livewire\livewire;
it('can send invoices', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->callAction('send');
expect($invoice->refresh())
->isSent()->toBeTrue();
});
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('can send invoice', function () {
$invoice = Invoice::factory()->create();
livewire(ListInvoices::class)
->callAction(TestAction::make('send')->table($invoice));
});
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('can create invoice', function () {
livewire(ListInvoices::class)
->callAction(TestAction::make('create')->table());
});
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
use function Pest\Laravel\assertDatabaseMissing;
it('can bulk delete users', function () {
$users = User::factory()->count(5)->create();
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->selectTableRecords($users)
->callAction(TestAction::make(DeleteBulkAction::class)->table()->bulk())
->assertNotified()
->assertCanNotSeeTableRecords($users);
$users->each(fn (User $user) => assertDatabaseMissing($user));
});
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('can send from schema component', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class)
->callAction(TestAction::make('send')->schemaComponent('customer_id'));
});
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('can access nested action', function () {
$invoice = Invoice::factory()->create();
livewire(ManageInvoices::class)
->callAction([
TestAction::make('view')->table($invoice),
TestAction::make('send')->schemaComponent('customer.name'),
]);
});
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use function Pest\Livewire\livewire;
use function Pest\Laravel\assertDatabaseMissing;
it('can delete a user', function () {
$user = User::factory()->create();
livewire(EditUser::class, [
'record' => $user->id,
])
->callAction(DeleteAction::class)
->assertNotified()
->assertRedirect();
assertDatabaseMissing($user);
});
use function Pest\Livewire\livewire;
it('can only print invoices', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->assertActionHidden('send')
->assertActionVisible('print')
->assertActionExists('print')
->assertActionDoesNotExist('unsend');
});
use function Pest\Livewire\livewire;
it('can only print a sent invoice', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->assertActionDisabled('send')
->assertActionEnabled('print');
});
use function Pest\Livewire\livewire;
it('stops sending if invoice has no email address', function () {
$invoice = Invoice::factory(['email' => null])->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->callAction('send')
->assertActionHalted('send');
});
use function Pest\Livewire\livewire;
it('confirms the target address before sending', function () {
$invoice = Invoice::factory()->create();
$recipientEmail = $invoice->company->primaryContact->email;
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->mountAction('send')
->assertMountedActionModalSee($recipientEmail);
});
use function Pest\Livewire\livewire;
it('can send invoices to the primary contact by default', function () {
$invoice = Invoice::factory()->create();
$recipientEmail = $invoice->company->primaryContact->email;
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->mountAction('send')
->assertSchemaStateSet([
'email' => $recipientEmail,
])
->callMountedAction()
->assertHasNoFormErrors();
expect($invoice->refresh())
->isSent()->toBeTrue()
->recipient_email->toBe($recipientEmail);
});
use Filament\Actions\Action;
use function Pest\Livewire\livewire;
it('has the correct description', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->assertActionExists('send', function (Action $action): bool {
return $action->getModalDescription() === 'This will send an email...';
});
});
use function Pest\Livewire\livewire;
it('links to the correct sites', function () {
$invoice = Invoice::factory()->create();
livewire(EditInvoice::class, [
'invoice' => $invoice,
])
->assertActionHasUrl('filament', 'https://filamentphp.com/')
->assertActionDoesNotHaveUrl('filament', 'https://github.com/filamentphp/filament')
->assertActionShouldOpenUrlInNewTab('filament')
->assertActionShouldNotOpenUrlInNewTab('github');
});
use function Pest\Livewire\livewire;
it('has a form', function () {
livewire(CreatePost::class)
->assertFormExists();
});
use function Pest\Livewire\livewire;
it('has title field', function () {
livewire(CreatePost::class)
->assertSchemaComponentExists('title');
});
it('does not have conditional component', function () {
livewire(CreatePost::class)
->assertSchemaComponentDoesNotExist('no-such-section');
});
use function Pest\Livewire\livewire;
it('has correct enabled/disabled state', function () {
livewire(CreatePost::class)
->assertFormFieldEnabled('title')
->assertFormFieldDisabled('slug');
});
use Illuminate\Support\Str;
use function Pest\Livewire\livewire;
it('can automatically generate a slug from the title', function () {
$title = fake()->sentence();
livewire(CreatePost::class)
->fillForm([
'title' => $title,
])
->assertSchemaStateSet([
'slug' => Str::slug($title),
]);
});
use Illuminate\Support\Str;
use function Pest\Livewire\livewire;
it('can automatically generate a slug without spaces', function () {
$title = fake()->sentence();
livewire(CreatePost::class)
->fillForm([
'title' => $title,
])
->assertSchemaStateSet(function (array $state): array {
expect($state['slug'])
->not->toContain(' ');
return [
'slug' => Str::slug($title),
];
});
});
use Filament\Schemas\Components\Section;
use Illuminate\Testing\Assert;
use function Pest\Livewire\livewire;
test('comments section has heading', function () {
livewire(EditPost::class)
->assertSchemaComponentExists(
'comments-section',
checkComponentUsing: function (Section $component): bool {
Assert::assertTrue(
$component->getHeading() === 'Comments',
'Failed asserting section has correct heading.',
);
return true;
},
);
});
use function Pest\Livewire\livewire;
it('moves to next wizard step', function () {
livewire(CreatePost::class)
->goToNextWizardStep()
->assertHasFormErrors(['title']);
});
use Filament\Forms\Components\Repeater;
use function Pest\Livewire\livewire;
it('has correct number of items', function () {
$post = Post::factory()->create();
$undoRepeaterFake = Repeater::fake();
livewire(EditPost::class, ['record' => $post])
->assertSchemaStateSet(function (array $state) {
expect($state['quotes'])
->toHaveCount(2);
});
$undoRepeaterFake();
});
use App\Models\Quote;
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('can send quote', function () {
$post = Post::factory()->create();
$quote = Quote::first();
livewire(EditPost::class, ['record' => $post])
->callAction(TestAction::make('sendQuote')->schemaComponent('quotes')->arguments([
'item' => "record-{$quote->getKey()}",
]))
->assertNotified('Quote sent!');
});
use Filament\Notifications\Notification;
use function Filament\Notifications\Testing\assertNotified;
use function Pest\Livewire\livewire;
// Using Livewire helper
it('sends a notification', function () {
livewire(CreatePost::class)
->assertNotified();
});
// Using dedicated helper
it('sends a notification', function () {
assertNotified();
});
// Using Notification facade
it('sends a notification', function () {
Notification::assertNotified();
});
use Filament\Notifications\Notification;
use function Pest\Livewire\livewire;
it('does not send a notification', function () {
livewire(CreatePost::class)
->assertNotNotified()
// or with specific title
->assertNotNotified('Unable to create post')
// or with exact notification object
->assertNotNotified(
Notification::make()
->danger()
->title('Unable to create post')
->body('Something went wrong.'),
);
});
use Filament\Notifications\Notification;
use function Pest\Livewire\livewire;
it('sends a notification', function () {
livewire(CreatePost::class)
->assertNotified(
Notification::make()
->danger()
->title('Unable to create post')
->body('Something went wrong.'),
);
});
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\RelationManagers\PostsRelationManager;
use App\Models\User;
use function Pest\Livewire\livewire;
it('can load the relation manager', function () {
$user = User::factory()->create();
livewire(EditUser::class, [
'record' => $user->id,
])
->assertSeeLivewire(PostsRelationManager::class);
});
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\RelationManagers\PostsRelationManager;
use App\Models\Post;
use App\Models\User;
use function Pest\Livewire\livewire;
it('can load the relation manager', function () {
$user = User::factory()
->has(Post::factory()->count(5))
->create();
livewire(PostsRelationManager::class, [
'ownerRecord' => $user,
'pageClass' => EditUser::class,
])
->assertOk()
->assertCanSeeTableRecords($user->posts);
});
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\RelationManagers\PostsRelationManager;
use App\Models\Post;
use App\Models\User;
use Filament\Actions\Testing\TestAction;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Livewire\livewire;
it('can create a post', function () {
$user = User::factory()->create();
$newPostData = Post::factory()->make();
livewire(PostsRelationManager::class, [
'ownerRecord' => $user,
'pageClass' => EditUser::class,
])
->callAction(TestAction::make(CreateAction::class)->table(), [
'title' => $newPostData->title,
'content' => $newPostData->content,
])
->assertNotified();
assertDatabaseHas(Post::class, [
'title' => $newPostData->title,
'content' => $newPostData->content,
'user_id' => $user->id,
]);
});
Always import and use Pest\Livewire\livewire function for FilamentPHP tests:
use function Pest\Livewire\livewire;
All FilamentPHP tests should use the RefreshDatabase trait or Pest's uses():
uses(RefreshDatabase::class);
Follow this pattern for FilamentPHP resource tests:
<?php
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Livewire\livewire;
uses(RefreshDatabase::class);
it('can load the page', function () {
// Test implementation
});
it('can create a user', function () {
// Test implementation
});
// More tests...
When testing resources with policies or permissions:
use function Pest\Laravel\actingAs;
it('authorized user can delete', function () {
$user = User::factory()->create();
$admin = User::factory()->admin()->create();
actingAs($admin);
livewire(EditUser::class, ['record' => $user->id])
->callAction(DeleteAction::class)
->assertNotified();
});
Always use factories to create test data:
$user = User::factory()->create();
$newUserData = User::factory()->make();
assertOk() - Assert successful page loadassertNotified() - Assert notification was shownassertRedirect() - Assert redirect occurredassertSuccessful() - Assert component rendered successfullyassertCanSeeTableRecords($records) - Assert records visibleassertCanNotSeeTableRecords($records) - Assert records not visibleassertCountTableRecords($count) - Assert record countassertTableColumnVisible($column) - Assert column visibleassertTableColumnHidden($column) - Assert column hiddenassertTableColumnFormattedStateSet($column, $value, record: $record)assertSchemaStateSet($data) - Assert form/infolist dataassertSchemaComponentExists($key) - Assert component existsassertSchemaComponentDoesNotExist($key) - Assert component doesn't existassertFormFieldEnabled($field) - Assert field is enabledassertFormFieldDisabled($field) - Assert field is disabledassertFormExists() - Assert form existsassertHasFormErrors($fields) - Assert form has errorsassertHasNoFormErrors() - Assert no form errorsassertActionExists($action) - Assert action existsassertActionDoesNotExist($action) - Assert action doesn't existassertActionVisible($action) - Assert action visibleassertActionHidden($action) - Assert action hiddenassertActionEnabled($action) - Assert action enabledassertActionDisabled($action) - Assert action disabledassertActionHalted($action) - Assert action was haltedassertActionHasUrl($action, $url) - Assert action has URLassertMountedActionModalSee($content) - Assert modal contentassertDatabaseHas($model, $data) - Assert record existsassertDatabaseMissing($model) - Assert record doesn't existit('can perform action') formatSince this app uses Arabic UI: