Skip to content
filament-testing logo

FilamentPHP Testing Skill

filament-testing

Generate Pest tests for FilamentPHP v4 resources, forms, tables, and authorization

SKILL.md

Full skill instructions

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/​filament-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:

  1. Page rendering tests
  2. CRUD operation tests
  3. Form validation tests
  4. Table feature tests (search, sort, filter)
  5. Action tests
  6. Authorization tests
  7. Widget tests
  8. Relation manager tests

More Other skills

Delegate independent tasks to specialized agents working concurrently with isolated context.

295.4K 206.3K
View

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

179.7K 100.1K
View

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

179.7K 89.5K
View

A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).

179.7K 76.1K
View

Fundamental design principles for writing maintainable, testable Python code.

40.2K 21.9K
View

Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.

40.2K 20K
View

Generate Playwright tests from scenarios using interactive browser exploration and validation.

39.7K 18.2K
View

Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.

40.2K 14.7K
View

GDPR-compliant data handling with consent management, data subject rights, and privacy controls.

40.2K 14.3K
View

Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.

40.2K 13.5K
View

Detailed multi-file refactor planning with safety checks and rollback guidance.

39.7K 13K
View