
testing
Generate Pest tests for FilamentPHP v4 resources, forms, tables, and authorization
- Installs
- 64
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/mwguerra/claude-code-plugins --skill testing- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads testing automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
FilamentPHP Testing Skill
Overview
This skill generates comprehensive Pest tests for FilamentPHP v4 components following official testing documentation patterns.
Documentation Reference
CRITICAL: Before generating tests, read:
/home/mwguerra/projects/mwguerra/claude-code-plugins/filament-specialist/skills/docs/references/general/10-testing/
Test Setup
Base Test Configuration
<?php
declare(strict_types=1);
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function setUp(): void
{
parent::setUp();
// Login as admin for Filament tests
$this->actingAs(\App\Models\User::factory()->create([
'is_admin' => true,
]));
}
}
Pest Configuration
// tests/Pest.php
uses(Tests\TestCase::class)
->in('Feature');
uses(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
Resource Tests
List Page Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource;
use App\Filament\Resources\PostResource\Pages\ListPosts;
use App\Models\Post;
use App\Models\User;
use Filament\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use function Pest\Livewire\livewire;
beforeEach(function () {
$this->user = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->user);
});
it('can render the list page', function () {
livewire(ListPosts::class)
->assertSuccessful();
});
it('can list posts', function () {
$posts = Post::factory()->count(10)->create();
livewire(ListPosts::class)
->assertCanSeeTableRecords($posts);
});
it('can render post title column', function () {
Post::factory()->create(['title' => 'Test Post Title']);
livewire(ListPosts::class)
->assertCanRenderTableColumn('title');
});
it('can search posts by title', function () {
$post = Post::factory()->create(['title' => 'Unique Search Term']);
$otherPost = Post::factory()->create(['title' => 'Other Post']);
livewire(ListPosts::class)
->searchTable('Unique Search Term')
->assertCanSeeTableRecords([$post])
->assertCanNotSeeTableRecords([$otherPost]);
});
it('can sort posts by title', function () {
$posts = Post::factory()->count(3)->create();
livewire(ListPosts::class)
->sortTable('title')
->assertCanSeeTableRecords($posts->sortBy('title'), inOrder: true)
->sortTable('title', 'desc')
->assertCanSeeTableRecords($posts->sortByDesc('title'), inOrder: true);
});
it('can filter posts by status', function () {
$publishedPost = Post::factory()->create(['status' => 'published']);
$draftPost = Post::factory()->create(['status' => 'draft']);
livewire(ListPosts::class)
->filterTable('status', 'published')
->assertCanSeeTableRecords([$publishedPost])
->assertCanNotSeeTableRecords([$draftPost]);
});
it('can bulk delete posts', function () {
$posts = Post::factory()->count(3)->create();
livewire(ListPosts::class)
->callTableBulkAction(DeleteBulkAction::class, $posts);
foreach ($posts as $post) {
$this->assertModelMissing($post);
}
});
Create Page Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource;
use App\Filament\Resources\PostResource\Pages\CreatePost;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use function Pest\Livewire\livewire;
beforeEach(function () {
$this->user = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->user);
});
it('can render the create page', function () {
livewire(CreatePost::class)
->assertSuccessful();
});
it('can create a post', function () {
$category = Category::factory()->create();
$newData = [
'title' => 'New Post Title',
'slug' => 'new-post-title',
'content' => 'This is the post content.',
'status' => 'draft',
'category_id' => $category->id,
];
livewire(CreatePost::class)
->fillForm($newData)
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas(Post::class, [
'title' => 'New Post Title',
'slug' => 'new-post-title',
]);
});
it('validates required fields', function () {
livewire(CreatePost::class)
->fillForm([
'title' => '',
'content' => '',
])
->call('create')
->assertHasFormErrors([
'title' => 'required',
'content' => 'required',
]);
});
it('validates title max length', function () {
livewire(CreatePost::class)
->fillForm([
'title' => str_repeat('a', 256),
])
->call('create')
->assertHasFormErrors(['title' => 'max']);
});
it('validates unique slug', function () {
Post::factory()->create(['slug' => 'existing-slug']);
livewire(CreatePost::class)
->fillForm([
'title' => 'New Post',
'slug' => 'existing-slug',
'content' => 'Content',
])
->call('create')
->assertHasFormErrors(['slug' => 'unique']);
});
Edit Page Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource;
use App\Filament\Resources\PostResource\Pages\EditPost;
use App\Models\Post;
use App\Models\User;
use Filament\Actions\DeleteAction;
use function Pest\Livewire\livewire;
beforeEach(function () {
$this->user = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->user);
});
it('can render the edit page', function () {
$post = Post::factory()->create();
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->assertSuccessful();
});
it('can retrieve data', function () {
$post = Post::factory()->create();
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->assertFormSet([
'title' => $post->title,
'slug' => $post->slug,
'content' => $post->content,
'status' => $post->status,
]);
});
it('can update a post', function () {
$post = Post::factory()->create();
$newData = [
'title' => 'Updated Title',
'slug' => 'updated-title',
'content' => 'Updated content.',
'status' => 'published',
];
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->fillForm($newData)
->call('save')
->assertHasNoFormErrors();
expect($post->refresh())
->title->toBe('Updated Title')
->slug->toBe('updated-title')
->status->toBe('published');
});
it('can delete a post', function () {
$post = Post::factory()->create();
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->callAction(DeleteAction::class);
$this->assertModelMissing($post);
});
it('validates unique slug excluding current record', function () {
$post = Post::factory()->create(['slug' => 'my-slug']);
$otherPost = Post::factory()->create(['slug' => 'other-slug']);
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->fillForm(['slug' => 'other-slug'])
->call('save')
->assertHasFormErrors(['slug' => 'unique']);
});
View Page Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\Pages\ViewPost;
use App\Models\Post;
use App\Models\User;
use function Pest\Livewire\livewire;
beforeEach(function () {
$this->user = User::factory()->create(['is_admin' => true]);
$this->actingAs($this->user);
});
it('can render the view page', function () {
$post = Post::factory()->create();
livewire(ViewPost::class, ['record' => $post->getRouteKey()])
->assertSuccessful();
});
it('can retrieve post data in infolist', function () {
$post = Post::factory()->create([
'title' => 'Test Post',
'status' => 'published',
]);
livewire(ViewPost::class, ['record' => $post->getRouteKey()])
->assertSee('Test Post')
->assertSee('published');
});
Form Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\Pages\CreatePost;
use App\Models\Category;
use App\Models\Tag;
use function Pest\Livewire\livewire;
it('can fill form fields', function () {
$category = Category::factory()->create();
livewire(CreatePost::class)
->fillForm([
'title' => 'Test Title',
'category_id' => $category->id,
])
->assertFormSet([
'title' => 'Test Title',
'category_id' => $category->id,
]);
});
it('has required form fields', function () {
livewire(CreatePost::class)
->assertFormFieldExists('title')
->assertFormFieldExists('slug')
->assertFormFieldExists('content')
->assertFormFieldExists('status')
->assertFormFieldExists('category_id');
});
it('renders select options', function () {
$categories = Category::factory()->count(3)->create();
livewire(CreatePost::class)
->assertFormFieldExists('category_id', function ($field) use ($categories) {
return $field->getOptions() === $categories->pluck('name', 'id')->toArray();
});
});
it('can handle repeater fields', function () {
livewire(CreatePost::class)
->fillForm([
'meta' => [
['key' => 'og:title', 'value' => 'Open Graph Title'],
['key' => 'og:description', 'value' => 'Open Graph Description'],
],
])
->call('create')
->assertHasNoFormErrors();
});
Table Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\Pages\ListPosts;
use App\Models\Post;
use function Pest\Livewire\livewire;
it('displays correct columns', function () {
Post::factory()->create([
'title' => 'Test Post',
'status' => 'published',
]);
livewire(ListPosts::class)
->assertCanRenderTableColumn('title')
->assertCanRenderTableColumn('status')
->assertCanRenderTableColumn('author.name')
->assertCanRenderTableColumn('created_at');
});
it('can filter by date range', function () {
$oldPost = Post::factory()->create([
'created_at' => now()->subMonths(2),
]);
$recentPost = Post::factory()->create([
'created_at' => now(),
]);
livewire(ListPosts::class)
->filterTable('created_at', [
'created_from' => now()->subWeek()->format('Y-m-d'),
'created_until' => now()->format('Y-m-d'),
])
->assertCanSeeTableRecords([$recentPost])
->assertCanNotSeeTableRecords([$oldPost]);
});
it('displays table empty state', function () {
livewire(ListPosts::class)
->assertSee('No posts yet');
});
Action Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\Pages\EditPost;
use App\Filament\Resources\PostResource\Pages\ListPosts;
use App\Models\Post;
use Filament\Tables\Actions\DeleteAction;
use function Pest\Livewire\livewire;
it('can call publish action', function () {
$post = Post::factory()->create(['status' => 'draft']);
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->callAction('publish');
expect($post->refresh()->status)->toBe('published');
});
it('shows publish action only for drafts', function () {
$draftPost = Post::factory()->create(['status' => 'draft']);
$publishedPost = Post::factory()->create(['status' => 'published']);
livewire(EditPost::class, ['record' => $draftPost->getRouteKey()])
->assertActionVisible('publish');
livewire(EditPost::class, ['record' => $publishedPost->getRouteKey()])
->assertActionHidden('publish');
});
it('can call table row action', function () {
$post = Post::factory()->create();
livewire(ListPosts::class)
->callTableAction(DeleteAction::class, $post);
$this->assertModelMissing($post);
});
it('can call action with form data', function () {
$post = Post::factory()->create();
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->callAction('send_notification', [
'subject' => 'Test Subject',
'message' => 'Test Message',
])
->assertHasNoActionErrors();
});
it('validates action form', function () {
$post = Post::factory()->create();
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->callAction('send_notification', [
'subject' => '',
'message' => '',
])
->assertHasActionErrors([
'subject' => 'required',
'message' => 'required',
]);
});
Authorization Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\Pages\CreatePost;
use App\Filament\Resources\PostResource\Pages\EditPost;
use App\Filament\Resources\PostResource\Pages\ListPosts;
use App\Models\Post;
use App\Models\User;
use function Pest\Livewire\livewire;
it('prevents unauthorized users from viewing list', function () {
$user = User::factory()->create(['is_admin' => false]);
$this->actingAs($user);
livewire(ListPosts::class)
->assertForbidden();
});
it('prevents unauthorized users from creating posts', function () {
$user = User::factory()->create(['is_admin' => false]);
$this->actingAs($user);
livewire(CreatePost::class)
->assertForbidden();
});
it('prevents unauthorized users from editing others posts', function () {
$author = User::factory()->create();
$otherUser = User::factory()->create();
$post = Post::factory()->create(['author_id' => $author->id]);
$this->actingAs($otherUser);
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->assertForbidden();
});
it('allows authors to edit their own posts', function () {
$author = User::factory()->create();
$post = Post::factory()->create(['author_id' => $author->id]);
$this->actingAs($author);
livewire(EditPost::class, ['record' => $post->getRouteKey()])
->assertSuccessful();
});
Widget Tests
<?php
declare(strict_types=1);
use App\Filament\Widgets\StatsOverview;
use App\Filament\Widgets\LatestPosts;
use App\Models\Post;
use App\Models\User;
use function Pest\Livewire\livewire;
it('can render stats overview widget', function () {
livewire(StatsOverview::class)
->assertSuccessful();
});
it('displays correct stats', function () {
Post::factory()->count(5)->create(['status' => 'published']);
Post::factory()->count(3)->create(['status' => 'draft']);
livewire(StatsOverview::class)
->assertSee('8') // Total posts
->assertSee('5'); // Published posts
});
it('can render table widget', function () {
$posts = Post::factory()->count(5)->create();
livewire(LatestPosts::class)
->assertSuccessful()
->assertCanSeeTableRecords($posts);
});
Relation Manager Tests
<?php
declare(strict_types=1);
use App\Filament\Resources\PostResource\RelationManagers\CommentsRelationManager;
use App\Models\Comment;
use App\Models\Post;
use function Pest\Livewire\livewire;
it('can render relation manager', function () {
$post = Post::factory()->create();
livewire(CommentsRelationManager::class, [
'ownerRecord' => $post,
'pageClass' => \App\Filament\Resources\PostResource\Pages\EditPost::class,
])
->assertSuccessful();
});
it('can list related comments', function () {
$post = Post::factory()->create();
$comments = Comment::factory()->count(3)->create(['post_id' => $post->id]);
livewire(CommentsRelationManager::class, [
'ownerRecord' => $post,
'pageClass' => \App\Filament\Resources\PostResource\Pages\EditPost::class,
])
->assertCanSeeTableRecords($comments);
});
it('can create related comment', function () {
$post = Post::factory()->create();
livewire(CommentsRelationManager::class, [
'ownerRecord' => $post,
'pageClass' => \App\Filament\Resources\PostResource\Pages\EditPost::class,
])
->callTableAction('create', data: [
'content' => 'New comment content',
]);
expect($post->comments)->toHaveCount(1);
});
Output
Generated tests include:
- Page rendering tests
- CRUD operation tests
- Form validation tests
- Table feature tests (search, sort, filter)
- Action tests
- Authorization tests
- Widget tests
- Relation manager tests
FAQ
What does the testing skill do?
Generate Pest tests for FilamentPHP v4 resources, forms, tables, and authorization
How do I install testing?
Run npx skills add https://github.com/mwguerra/claude-code-plugins --skill testing in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/testing/ (personal) or .claude/skills/testing/ (project).
Which AI agents can use testing?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is testing safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains testing?
It is published by mwguerra in mwguerra/claude-code-plugins. It has 64 recorded installs.
More from mwguerra/claude-code-plugins
filament-resource
mwguerra/claude-code-plugins
Generate FilamentPHP v4 resources with form, table, relation managers, and actions
Popular testing & qa skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
grilling
mattpocock/skills
Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
migrate-to-shoehorn
mattpocock/skills
Migrate test files from as type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace as in tests, or needs partial test data.
run-train
lllllllama/rigorpilot-skills
Rigor Train skill for deep learning research repositories. Use when a documented or selected training command should be run conservatively for startup verification, short-run verification, full kickoff, or resume, with command, config, seed, log, checkpoint, status, and metric evidence written to standardized trainoutputs/. Do not use for environment setup, exploratory sweeps, speculative idea implementation, or end-to-end orchestration.
test-driven-development
obra/superpowers
Use when implementing any feature or bugfix, before writing implementation code
finishing-a-development-branch
obra/superpowers
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work