Set up end-to-end browser testing using Pest Browser Plugin + Playwright. New v4 test suite uses SQLite :memory: database with pre-generated schema dump (database/schema/testing-schema.sql) instead of running migrations, enabling faster test startup. - Add pestphp/pest-plugin-browser dependency - Create GenerateTestingSchema command to export PostgreSQL schema to SQLite - Add .env.testing configuration for isolated test environment - Implement v4 test directory structure (Feature, Browser, Unit tests) - Update Pest skill documentation with browser testing patterns, API reference, debugging techniques, and common pitfalls - Configure phpunit.xml and tests/Pest.php for v4 suite - Update package.json and docker-compose.dev.yml for testing dependencies
6.2 KiB
| name | description |
|---|---|
| pest-testing | Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. |
Pest Testing 4
When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
Documentation
Use search-docs for detailed Pest 4 patterns and documentation.
Test Directory Structure
tests/Feature/andtests/Unit/— Legacy tests (keep, don't delete)tests/v4/Feature/— New feature tests (SQLite :memory: database)tests/v4/Browser/— Browser tests (Pest Browser Plugin + Playwright)tests/Browser/— Legacy Dusk browser tests (keep, don't delete)
New tests go in tests/v4/. The v4 suite uses SQLite :memory: with a schema dump (database/schema/testing-schema.sql) instead of running migrations.
Do NOT remove tests without approval.
Running Tests
- All v4 tests:
php artisan test --compact tests/v4/ - Browser tests:
php artisan test --compact tests/v4/Browser/ - Feature tests:
php artisan test --compact tests/v4/Feature/ - Specific file:
php artisan test --compact tests/v4/Browser/LoginTest.php - Filter:
php artisan test --compact --filter=testName - Headed (see browser):
./vendor/bin/pest tests/v4/Browser/ --headed - Debug (pause on failure):
./vendor/bin/pest tests/v4/Browser/ --debug
Basic Test Structure
it('is true', function () { expect(true)->toBeTrue(); });
Assertions
Use specific assertions (assertSuccessful(), assertNotFound()) instead of assertStatus():
| Use | Instead of |
|---|---|
assertSuccessful() |
assertStatus(200) |
assertNotFound() |
assertStatus(404) |
assertForbidden() |
assertStatus(403) |
Mocking
Import mock function before use: use function Pest\Laravel\mock;
Datasets
Use datasets for repetitive tests:
it('has emails', function (string $email) { expect($email)->not->toBeEmpty(); })->with([ 'james' => 'james@laravel.com', 'taylor' => 'taylor@laravel.com', ]);
Browser Testing (Pest Browser Plugin + Playwright)
Browser tests use pestphp/pest-plugin-browser with Playwright. They run outside Docker — the plugin starts an in-process HTTP server and Playwright browser automatically.
Key Rules
- Always use
RefreshDatabase— the in-process server uses SQLite :memory: - Always seed
InstanceSettings::create(['id' => 0])inbeforeEach— most pages crash without it - Use
User::factory()for auth tests — create users withid => 0for root user - No Dusk, no Selenium — use
visit(),fill(),click(),assertSee()from the Pest Browser API - Place tests in
tests/v4/Browser/ - Views with bare
functiondeclarations will crash on the second request in the same process — wrap withfunction_exists()guard if you encounter this
Browser Test Template
use App\Models\InstanceSettings; use Illuminate\Foundation\Testing\RefreshDatabase;uses(RefreshDatabase::class);
beforeEach(function () { InstanceSettings::create(['id' => 0]); });
it('can visit the page', function () { $page = visit('/login');
$page->assertSee('Login');
});
Browser Test with Form Interaction
it('fails login with invalid credentials', function () { User::factory()->create([ 'id' => 0, 'email' => 'test@example.com', 'password' => Hash::make('password'), ]);$page = visit('/login');
$page->fill('email', 'random@email.com')
->fill('password', 'wrongpassword123')
->click('Login')
->assertSee('These credentials do not match our records');
});
Browser API Reference
| Method | Purpose |
|---|---|
visit('/path') |
Navigate to a page |
->fill('field', 'value') |
Fill an input by name |
->click('Button Text') |
Click a button/link by text |
->assertSee('text') |
Assert visible text |
->assertDontSee('text') |
Assert text is not visible |
->assertPathIs('/path') |
Assert current URL path |
->assertSeeIn('.selector', 'text') |
Assert text in element |
->screenshot() |
Capture screenshot |
->debug() |
Pause test, keep browser open |
->wait(seconds) |
Wait N seconds |
Debugging
- Screenshots auto-saved to
tests/Browser/Screenshots/on failure ->debug()pauses and keeps browser open (press Enter to continue)->screenshot()captures state at any point--headedflag shows browser,--debugpauses on failure
SQLite Testing Setup
v4 tests use SQLite :memory: instead of PostgreSQL. Schema loaded from database/schema/testing-schema.sql.
Regenerating the Schema
When migrations change, regenerate from the running PostgreSQL database:
docker exec coolify php artisan schema:generate-testing
Architecture Testing
arch('controllers') ->expect('App\Http\Controllers') ->toExtendNothing() ->toHaveSuffix('Controller');
Common Pitfalls
- Not importing
use function Pest\Laravel\mock;before using mock - Using
assertStatus(200)instead ofassertSuccessful() - Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting
assertNoJavaScriptErrors()in browser tests - Browser tests: forgetting
InstanceSettings::create(['id' => 0])— most pages crash without it - Browser tests: forgetting
RefreshDatabase— SQLite :memory: starts empty - Browser tests: views with bare
functiondeclarations crash on second request — wrap withfunction_exists()guard