Claude
Skills
Sign in
Back

testing

Included with Lifetime
$97 forever

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

General

What this skill does


# 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
<?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

```php
// tests/Pest.php
uses(Tests\TestCase::class)
    ->in('Feature');

uses(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');
```

## Resource Tests

### List Page Tests

```php
<?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
<?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
<?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
<?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::facto

Related in General