From test-specialist
Generates Pest 4 tests for Filament resources, pages, widgets, actions, forms, tables, and authorization following Filament and Livewire best practices.
npx claudepluginhub mwguerra/claude-code-plugins --plugin test-specialist[target] [--resource name] [--page name] [--widget name] [--all] [--actions] [--forms] [--tables]# Test Filament Generate comprehensive Pest 4 tests for Filament admin panels, resources, pages, widgets, and custom actions following Filament testing best practices. ## Syntax ## Options - `--resource=<n>` - Test specific Filament resource - `--page=<n>` - Test specific Filament page - `--widget=<n>` - Test specific widget - `--all` - Test all Filament components - `--actions` - Include action tests - `--forms` - Include form field tests - `--tables` - Include table column/filter tests - `--access` - Include authorization tests - `--force` - Overwrite existing test files ## Documenta...
/testGenerates Pest tests for FilamentPHP v4 resources covering list, create, edit, view pages, CRUD operations, validation, table features, and optional authorization. Outputs to tests/Feature/Filament/{Resource}Test.php.
/planGenerates or updates a detailed E2E test plan for Laravel Filament apps, analyzing pages, roles, flows, and scenarios for QA execution. Saves to docs/detailed-test-list.md; supports --output, --force, --quick.
Share bugs, ideas, or general feedback.
Generate comprehensive Pest 4 tests for Filament admin panels, resources, pages, widgets, and custom actions following Filament testing best practices.
test-specialist:filament [options] [target]
--resource=<n> - Test specific Filament resource--page=<n> - Test specific Filament page--widget=<n> - Test specific widget--all - Test all Filament components--actions - Include action tests--forms - Include form field tests--tables - Include table column/filter tests--access - Include authorization tests--force - Overwrite existing test files<?php
use App\Filament\Resources\PostResource;
use App\Models\{User, Post};
use function Pest\Livewire\livewire;
describe('PostResource CRUD Operations', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
actingAs($this->admin);
});
describe('List Page', function () {
it('can render list page', function () {
livewire(PostResource\Pages\ListPosts::class)
->assertSuccessful();
});
it('can list posts', function () {
$posts = Post::factory()->count(10)->create();
livewire(PostResource\Pages\ListPosts::class)
->assertCanSeeTableRecords($posts);
});
it('can search posts by title', function () {
$posts = Post::factory()->count(10)->create();
$targetPost = $posts->first();
livewire(PostResource\Pages\ListPosts::class)
->searchTable($targetPost->title)
->assertCanSeeTableRecords([$targetPost])
->assertCanNotSeeTableRecords($posts->skip(1));
});
it('can filter posts by status', function () {
$publishedPosts = Post::factory()->published()->count(5)->create();
$draftPosts = Post::factory()->draft()->count(3)->create();
livewire(PostResource\Pages\ListPosts::class)
->filterTable('status', 'published')
->assertCanSeeTableRecords($publishedPosts)
->assertCanNotSeeTableRecords($draftPosts);
});
it('can sort posts by title', function () {
$posts = Post::factory()->count(3)->create();
livewire(PostResource\Pages\ListPosts::class)
->sortTable('title')
->assertCanSeeTableRecords($posts->sortBy('title'), inOrder: true);
});
it('can paginate posts', function () {
Post::factory()->count(30)->create();
livewire(PostResource\Pages\ListPosts::class)
->assertCanSeeTableRecords(Post::take(10)->get())
->call('nextPage')
->assertCanSeeTableRecords(Post::skip(10)->take(10)->get());
});
});
describe('Create Page', function () {
it('can render create page', function () {
livewire(PostResource\Pages\CreatePost::class)
->assertSuccessful();
});
it('can create post', function () {
$newData = Post::factory()->make();
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'title' => $newData->title,
'slug' => $newData->slug,
'content' => $newData->content,
'status' => $newData->status,
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas(Post::class, [
'title' => $newData->title,
'slug' => $newData->slug,
'content' => $newData->content,
]);
});
it('can validate input', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'title' => null,
])
->call('create')
->assertHasFormErrors(['title' => 'required']);
});
it('can validate unique slug', function () {
$existingPost = Post::factory()->create();
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'title' => 'New Post',
'slug' => $existingPost->slug,
])
->call('create')
->assertHasFormErrors(['slug' => 'unique']);
});
});
describe('Edit Page', function () {
it('can render edit page', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->assertSuccessful();
});
it('can retrieve data', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->assertFormSet([
'title' => $post->title,
'slug' => $post->slug,
'content' => $post->content,
'status' => $post->status,
]);
});
it('can save', function () {
$post = Post::factory()->create();
$newData = Post::factory()->make();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->fillForm([
'title' => $newData->title,
'slug' => $newData->slug,
'content' => $newData->content,
])
->call('save')
->assertHasNoFormErrors();
expect($post->refresh())
->title->toBe($newData->title)
->slug->toBe($newData->slug)
->content->toBe($newData->content);
});
it('can validate input', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->fillForm([
'title' => null,
])
->call('save')
->assertHasFormErrors(['title' => 'required']);
});
it('can delete', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->callAction('delete');
$this->assertModelMissing($post);
});
});
});
<?php
describe('PostResource Table Actions', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
actingAs($this->admin);
});
describe('Row Actions', function () {
it('can edit from table', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('edit', $post);
// Verify redirect or modal opens
});
it('can delete from table', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('delete', $post);
$this->assertModelMissing($post);
});
it('can view from table', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('view', $post)
->assertSuccessful();
});
});
describe('Bulk Actions', function () {
it('can bulk delete posts', function () {
$posts = Post::factory()->count(10)->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableBulkAction('delete', $posts);
foreach ($posts as $post) {
$this->assertModelMissing($post);
}
});
it('can bulk publish posts', function () {
$posts = Post::factory()->draft()->count(5)->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableBulkAction('publish', $posts);
foreach ($posts as $post) {
expect($post->refresh()->status)->toBe('published');
}
});
});
describe('Custom Actions', function () {
it('can archive post', function () {
$post = Post::factory()->published()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('archive', $post);
expect($post->refresh()->status)->toBe('archived');
});
it('can duplicate post', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('duplicate', $post);
$this->assertDatabaseCount(Post::class, 2);
$duplicate = Post::latest()->first();
expect($duplicate->title)->toContain('(Copy)');
});
});
});
<?php
describe('PostResource Form Fields', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
actingAs($this->admin);
});
it('can fill and save text input', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'title' => 'Test Title',
])
->assertFormSet([
'title' => 'Test Title',
]);
});
it('can fill and save textarea', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'content' => 'Test Content',
])
->assertFormSet([
'content' => 'Test Content',
]);
});
it('can fill and save select', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'status' => 'published',
])
->assertFormSet([
'status' => 'published',
]);
});
it('can fill and save relationship select', function () {
$category = Category::factory()->create();
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'category_id' => $category->id,
])
->assertFormSet([
'category_id' => $category->id,
]);
});
it('can fill and save file upload', function () {
Storage::fake('public');
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'featured_image' => UploadedFile::fake()->image('test.jpg'),
])
->call('create')
->assertHasNoFormErrors();
$post = Post::latest()->first();
Storage::disk('public')->assertExists($post->featured_image);
});
it('can fill and save repeater', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'meta_tags' => [
['key' => 'author', 'value' => 'John Doe'],
['key' => 'keywords', 'value' => 'test, blog'],
],
])
->call('create')
->assertHasNoFormErrors();
$post = Post::latest()->first();
expect($post->meta_tags)->toHaveCount(2);
});
});
<?php
describe('PostResource Authorization', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
$this->editor = User::factory()->editor()->create();
$this->user = User::factory()->create();
});
describe('Resource Access', function () {
it('allows admins to access resource', function () {
actingAs($this->admin);
livewire(PostResource\Pages\ListPosts::class)
->assertSuccessful();
});
it('allows editors to access resource', function () {
actingAs($this->editor);
livewire(PostResource\Pages\ListPosts::class)
->assertSuccessful();
});
it('denies regular users from accessing resource', function () {
actingAs($this->user);
livewire(PostResource\Pages\ListPosts::class)
->assertForbidden();
});
});
describe('Action Visibility', function () {
it('shows delete action to admins', function () {
actingAs($this->admin);
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->assertActionVisible('delete');
});
it('hides delete action from editors', function () {
actingAs($this->editor);
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->assertActionHidden('delete');
});
});
describe('Field Visibility', function () {
it('shows all fields to admins', function () {
actingAs($this->admin);
livewire(PostResource\Pages\CreatePost::class)
->assertFormFieldExists('title')
->assertFormFieldExists('featured')
->assertFormFieldExists('status');
});
it('hides featured checkbox from editors', function () {
actingAs($this->editor);
livewire(PostResource\Pages\CreatePost::class)
->assertFormFieldExists('title')
->assertFormFieldExists('status')
->assertFormFieldDoesNotExist('featured');
});
});
});
<?php
use App\Filament\Widgets\StatsOverview;
use App\Models\Post;
describe('Dashboard Widgets', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
actingAs($this->admin);
});
it('can render stats overview widget', function () {
livewire(StatsOverview::class)
->assertSuccessful();
});
it('displays correct post count', function () {
Post::factory()->count(5)->create();
livewire(StatsOverview::class)
->assertSee('5')
->assertSee('Total Posts');
});
it('displays correct published count', function () {
Post::factory()->published()->count(3)->create();
Post::factory()->draft()->count(2)->create();
livewire(StatsOverview::class)
->assertSee('3')
->assertSee('Published');
});
it('displays correct draft count', function () {
Post::factory()->published()->count(3)->create();
Post::factory()->draft()->count(2)->create();
livewire(StatsOverview::class)
->assertSee('2')
->assertSee('Drafts');
});
});
<?php
use App\Filament\Pages\Settings;
describe('Custom Filament Pages', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
actingAs($this->admin);
});
it('can render settings page', function () {
livewire(Settings::class)
->assertSuccessful();
});
it('can save settings', function () {
livewire(Settings::class)
->fillForm([
'site_name' => 'My Blog',
'site_description' => 'A great blog',
])
->call('save')
->assertHasNoFormErrors();
expect(setting('site_name'))->toBe('My Blog');
expect(setting('site_description'))->toBe('A great blog');
});
it('can validate settings', function () {
livewire(Settings::class)
->fillForm([
'site_name' => null,
])
->call('save')
->assertHasFormErrors(['site_name' => 'required']);
});
});
<?php
use App\Filament\Resources\PostResource\RelationManagers\CommentsRelationManager;
describe('Post Comments Relation Manager', function () {
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
$this->post = Post::factory()->create();
actingAs($this->admin);
});
it('can render relation manager', function () {
livewire(CommentsRelationManager::class, [
'ownerRecord' => $this->post,
])
->assertSuccessful();
});
it('can list comments', function () {
$comments = Comment::factory()->count(5)->create([
'post_id' => $this->post->id,
]);
livewire(CommentsRelationManager::class, [
'ownerRecord' => $this->post,
])
->assertCanSeeTableRecords($comments);
});
it('can create comment', function () {
livewire(CommentsRelationManager::class, [
'ownerRecord' => $this->post,
])
->callTableAction('create', data: [
'content' => 'Test comment',
])
->assertHasNoTableActionErrors();
expect($this->post->comments()->count())->toBe(1);
});
it('can edit comment', function () {
$comment = Comment::factory()->create([
'post_id' => $this->post->id,
]);
livewire(CommentsRelationManager::class, [
'ownerRecord' => $this->post,
])
->callTableAction('edit', $comment, data: [
'content' => 'Updated comment',
])
->assertHasNoTableActionErrors();
expect($comment->refresh()->content)->toBe('Updated comment');
});
it('can delete comment', function () {
$comment = Comment::factory()->create([
'post_id' => $this->post->id,
]);
livewire(CommentsRelationManager::class, [
'ownerRecord' => $this->post,
])
->callTableAction('delete', $comment);
$this->assertModelMissing($comment);
});
});
it('can open modal action', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\ListPosts::class)
->callTableAction('view', $post)
->assertSuccessful()
->assertSee($post->title);
});
it('can fill form across multiple sections', function () {
livewire(PostResource\Pages\CreatePost::class)
->fillForm([
'title' => 'Test Post',
'content' => 'Content',
'meta_title' => 'SEO Title',
'meta_description' => 'SEO Description',
])
->call('create')
->assertHasNoFormErrors();
});
it('shows success notification after save', function () {
$post = Post::factory()->create();
livewire(PostResource\Pages\EditPost::class, [
'record' => $post->getRouteKey(),
])
->fillForm(['title' => 'Updated Title'])
->call('save')
->assertNotified('Saved successfully');
});