valcuandrei / pest-e2e
A Laravel-first package that runs JS-owned E2E and component tests from Pest without introducing a PHP browser DSL.
Requires
- php: ^8.4
- illuminate/support: ^13.0
- laravel/prompts: ^0.3
- symfony/process: ^8.0
- symfony/yaml: ^7.0 || ^8.0
Requires (Dev)
- laravel/agent-detector: ^2.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.3.1
- pestphp/pest-dev-tools: ^4.0.0
- pestphp/pest-plugin: ^4.0.0
Suggests
- laravel/agent-detector: Automatic PAO-style agent environment detection (^2.0)
README
Laravel-first E2E orchestration for JavaScript-native browser testing.
Run your existing JS E2E suite (Playwright by default) from Pest — without introducing a PHP browser DSL.
What This Is
pestE2E is a Laravel-native orchestration layer that runs JavaScript-owned browser tests and maps structured results back into Pest output.
Conceptually, this is an Inertia-style bridge for E2E testing:
- Laravel owns test intent, state, authentication, and data
- JavaScript owns browser execution
- A stable contract connects the two
Pest orchestrates JS execution, passes context (environment, params, auth), and consumes structured JSON reports.
This package does not wrap Playwright in PHP. It orchestrates your existing JS test suite from Laravel.
Key Features
- JS test filtering via
only()andrunTest() - Laravel authentication using one-time auth tickets
- Runner agnostic (Playwright by default, extensible via worker contracts)
- Managed testing server (no manual
php artisan serve) - Isolated testing environment
- Stable JSON reporting contract (
pest-e2e.v1) - Fully type-safe (PHPStan compliant)
What This Is NOT
- Not a browser abstraction
- Not a PHP wrapper around Playwright
- Not Dusk
- Not Selenium
- No
visit(),click(), ortype()in PHP — ever
All browser logic lives in JavaScript.
Status
Stable v1
The public PHP API, authentication contract, and JSON report schema are locked. Internal runner adapters may evolve.
Parallel test execution
Browser tests that call e2e()->…->run() can run with Pest / Laravel parallel testing:
php artisan test --parallel --processes=4
Required .env.testing setup
Use a real test database (MySQL or PostgreSQL), not SQLite in-memory. Laravel parallel testing creates one database per worker (testing_test_1, testing_test_2, …). Recommended overrides:
SESSION_DRIVER=array CACHE_STORE=array QUEUE_CONNECTION=sync DB_DATABASE=testing
Run with --recreate-databases when bootstrapping worker databases for the first time.
Per-worker isolation
Each Pest worker process gets:
- its own managed Laravel server on a dedicated port
APP_URL/baseUrlpassed to Playwright matching that port- worker-scoped auth ticket cache keys (no cross-worker ticket bleed)
- the same
DB_*/CACHE_PREFIXenv as the PHP test worker (includingtesting_test_{TEST_TOKEN})
Default port formula when server.parallel_port_offset is enabled:
server.port + TEST_TOKEN
Example with base port 8800: worker 1 → 8801, worker 4 → 8804. Serial (non-parallel) runs keep using an ephemeral free port.
Configure the base port in config/pest-e2e.php under server.port (or legacy parallel.base_port) or via PEST_E2E_SERVER_PORT / PEST_E2E_PARALLEL_BASE_PORT in .env.testing. Set server.host / PEST_E2E_SERVER_HOST when the app must bind to a specific interface.
Installation
Install the package:
composer require valcuandrei/pest-e2e --dev
Then run:
php artisan pest-e2e:install
The installer can:
- Update your
pest.phpto includeE2ETestCase - Publish
config/pest-e2e.php - Publish the base E2E test case
- Publish the JS harness
- Publish the Playwright integration
- Install
@playwright/testand download Playwright browser binaries (playwright install) - Create or update
.env.testingwith parallel-safe E2E overrides - Create
database/testing.sqlitefor projects that intentionally use SQLite - Configure
phpunit.xmlto let.env.testingcontrol DB/cache (comment out overrides) - Ensure
phpunit.xmldefines a Browser testsuite fortests/Browser(whenphpunit.xmlexists; idempotent on every successful install) - When Laravel Sail is present (
laravel/sailincomposer.jsonorvendor/laravel/sail, plus alaravel.testservice), offer to merge the Headed Mode in Sail block into your Docker Compose file — the file is chosen in the same order Docker Compose does:compose.yaml,compose.yml,docker-compose.yaml, thendocker-compose.yml
Each step is skipped if already done. Use explicit flags to force: --setup-env-testing, --update-testing-env, --setup-testing-database, --configure-phpunit.
When publishing E2ETestCase, the installer sets the default JS package manager from tools found on your PATH (pnpm, yarn, bun, npm). The same resolved manager is used for the Playwright dev dependency install (so it no longer follows auto-detection / PEST_E2E_PACKAGE_MANAGER during that step). Pass --package-manager=pnpm (etc.) to force the stub value and skip detection. If more than one is available, you are prompted to pick one in interactive installs; with --no-interaction / --yes, it prefers a manager that matches an existing lockfile, otherwise the first in priority order (pnpm → yarn → bun → npm). If none are on PATH, it falls back to lockfile-only detection (same order), then npm.
Unattended / CI mode
php artisan pest-e2e:install --yes
Alias:
php artisan pest-e2e:install --unattended
Options
| Option | Description |
|---|---|
--yes |
Answer yes to all questions (performs full setup: update-pest, publish-config, publish-base-test-case, publish-js-harness, publish-js-playwright, add-csrf-exclusion, setup-env-testing/update-testing-env, setup-testing-database, configure-phpunit, sail-wslg-headed when Sail is detected, install-playwright) |
--no |
Answer no to all questions |
--force |
Overwrite existing files when publishing |
--update-pest |
Update Pest config to include E2ETestCase |
--setup-env-testing |
Create .env.testing with parallel-safe E2E overrides |
--update-testing-env |
Patch an existing .env.testing with parallel-safe session/cache/queue settings |
--setup-testing-database |
Create database/testing.sqlite for projects that intentionally use SQLite |
--configure-phpunit |
Comment out DB/cache env in phpunit.xml so .env.testing controls them |
--sail-wslg-headed |
Merge WSLg display/volume settings into the Sail laravel.test service of the resolved Compose file (see Headed Mode in Sail below) |
--add-csrf-exclusion |
Add pest-e2e auth route to CSRF exclusion (required for Herd/Windows) |
--publish-config |
Publish config |
--publish-base-test-case |
Publish E2ETestCase |
--publish-js-harness |
Publish JS harness |
--publish-js-playwright |
Publish Playwright adapter |
--publish-browser-tests |
Publish browser tests |
--publish-playwright-tests |
Publish Playwright tests |
--install-playwright |
Install @playwright/test and run playwright install (browser binaries) |
--package-manager= |
Force the value embedded in E2ETestCase and used for Playwright install (npm, yarn, pnpm, bun); skips PATH / lockfile detection and interactive choice |
Testing Environment (Important)
pestE2E starts a managed Laravel server using:
--env=testing
If a .env.testing file exists, Laravel automatically loads it.
The installer can create this for you with --setup-env-testing (included in --yes). It prefers Sail-compatible MySQL defaults because Laravel parallel testing creates per-worker databases from the base database name:
APP_ENV=testing APP_URL=http://127.0.0.1 DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=testing DB_USERNAME=sail DB_PASSWORD=password SESSION_DRIVER=array CACHE_STORE=array QUEUE_CONNECTION=sync PEST_E2E_AUTH_ROUTE_ENABLED=true
For existing .env.testing files, use --update-testing-env or --yes. The installer preserves existing MySQL/PostgreSQL database credentials, updates SESSION_DRIVER=array, CACHE_STORE=array, and QUEUE_CONNECTION=sync, and warns before replacing SQLite with Sail MySQL defaults unless --yes, --unattended, or --force already implies consent.
Manual setup: If you prefer to configure yourself, create .env.testing with a real isolated test database:
APP_ENV=testing APP_DEBUG=true DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=testing DB_USERNAME=sail DB_PASSWORD=password SESSION_DRIVER=array CACHE_STORE=array QUEUE_CONNECTION=sync PEST_E2E_AUTH_ROUTE_ENABLED=true
This ensures:
- Your development database is not modified
- Auth routes are enabled only during testing
- The Pest process and the managed server use the same database
- Feature tests and browser tests do not share database-backed session/cache/queue state across workers
For php artisan test --parallel, Laravel creates per-worker databases such as testing_test_1, testing_test_2, etc. SQLite is not recommended for parallel browser testing because it does not provide the same per-worker database isolation model.
Your phpunit.xml must not override DB_CONNECTION, DB_DATABASE, CACHE_STORE, or SESSION_DRIVER — let .env.testing control them. The installer can comment these out for you with --configure-phpunit.
The managed server is started in isolation and inherits no development state beyond explicitly provided environment variables.
Quick Start
Configure a target inside the setUp() method of tests/E2ETestCase.php:
e2e()->target('frontend', fn ($p) => $p ->dir('resources/js/e2e') ->env(['APP_URL' => 'http://localhost']) ->params(['baseUrl' => 'http://localhost']) );
Register targets in your base E2E test case (
E2ETestCase::setUp()), not inside individual test functions. For--parallel, ensure per-worker databases (see Parallel test execution under Status).
Run all tests:
e2e('frontend')->run();
Run a specific test:
e2e('frontend')->runTest('UserProfile can update their profile');
Example: Complex Frontend Flow
PHP (Pest)
use App\Models\User; test('that a user can update their profile', function () { $user = User::factory()->create(); e2e('frontend') ->actingAs($user) // or ->loginAs($user) ->withParams([ 'name' => 'Test User', 'email' => 'test@example.com', ]) ->runTest('UserProfile can update their profile'); expect($user->fresh()->name)->toBe('Test User'); expect($user->fresh()->email)->toBe('test@example.com'); });
JavaScript (Playwright)
import { test, expect } from '@playwright/test'; import { readParams } from '../pest-e2e/core.mjs'; test('UserProfile can update their profile', async ({ page }) => { const { name, email } = await readParams(); await page.goto('/settings/profile'); await page.locator('#name').fill(name); await page.locator('#email').fill(email); await page.getByTestId('update-profile-button').click(); await expect(page.getByText('Saved.')).toBeVisible(); });
Laravel controls state and authentication. JavaScript controls the browser.
Why Not Use Pest’s Native Browser Testing?
Pest’s built-in browser testing (Dusk-style) is excellent for:
- Form submissions
- CRUD flows
- Traditional backend-driven pages
- Simple UI assertions
However, for advanced frontend systems such as:
- Drag-and-drop page builders
- Resizable layout systems
- CSS box-model assertions (width, height, margin, padding)
- Transform-based positioning
- Vue / Pinia state inspection
- DOM measurement and layout calculations
You need full native Playwright running in its own JavaScript environment.
pestE2E orchestrates your JS suite — it does not abstract it.
Managed Testing Server
When you call:
e2e('frontend')->run();
pestE2E automatically:
- Boots a temporary Laravel HTTP server
- Forces it into
APP_ENV=testing - Binds it to
127.0.0.1on a free port - Executes your JS runner against that server
- Collects the JSON report
- Shuts the server down
No manual php artisan serve required.
No environment leakage into development.
Running Tests
Local:
php artisan test
Sail:
sail artisan test
E2E / Browser tests: parallel runs are supported when each worker has an isolated database (see Parallel test execution under Status).
php artisan test --parallel --processes=4
Successful E2E detail output is shown in normal test runs. In --compact and --parallel runs, passed E2E details are suppressed so Pest output stays readable; failed E2E runs still print their details.
Agent / PAO output
For AI agents and CI parsers, enable compact JSON (one line per e2e()->run()):
PEST_E2E_AGENT_OUTPUT=1 php artisan test ./tests/Browser php artisan test ./tests/Browser --pest-e2e-agent-output php artisan test ./tests/Browser --parallel --pest-e2e-agent-output
Also auto-detected when laravel/agent-detector is installed or common agent env vars are set (e.g. CURSOR_AGENT). Configure via PEST_E2E_AGENT_OUTPUT / PAO_FORCE in .env.testing, or agent_output in config/pest-e2e.php.
Disable with PEST_E2E_AGENT_OUTPUT_DISABLE=1 or PAO_DISABLE=1.
In agent mode, human-readable Pest output is suppressed. Failed runs include php_test, failures (JS name, file, message, stack), and report_dir. See .docs/API.md for the full JSON contract.
Debug & Headed Mode
php artisan test --browse php artisan test --debug php artisan test --run-using=yarn
--browse/--headed→ runs browser in headed mode--debug→ enables debug mode and implies headed mode--run-using=npm|yarn|pnpm|bun→ use a specific package manager for E2E runs (default is set inE2ETestCase::$e2ePackageManagerduring install)
Timing Instrumentation
Enable baseline timing markers:
PEST_E2E_TIMING=true
Markers are emitted to stderr with prefix:
[pest-e2e:timing]
Each marker is JSON payload with phase, atMs, and optional durationMs.
Headed Mode in Sail (WSL2 + WSLg)
If you run Pest inside Sail on Windows (WSL2) and want headed mode, forward WSLg into the container by adding this to your laravel.test service:
environment: DISPLAY: ${DISPLAY} WAYLAND_DISPLAY: ${WAYLAND_DISPLAY} XDG_RUNTIME_DIR: ${XDG_RUNTIME_DIR} PULSE_SERVER: ${PULSE_SERVER} volumes: - /mnt/wslg:/mnt/wslg - /tmp/.X11-unix:/tmp/.X11-unix
This is only required for headed browser mode inside Docker on WSL2. Headless mode works without additional configuration.
Authentication Contract
Default auth route:
/pest-e2e/auth/login
Configurable via:
config('pest-e2e.auth.route');
Security:
- Disabled by default
- Requires header (default:
X-Pest-E2E: 1) - Tickets are single-use and short-lived
Reports
The package does not store JSON reports on disk. Playwright emits its JSON report to stdout; the PHP side parses it in memory and maps it to the canonical pest-e2e.v1 schema.
Playwright artifacts are written to a run-scoped directory:
{reports.base_dir}/{target}/{runId}
Configure the base directory globally with config('pest-e2e.reports.base_dir'). The default is storage/framework/testing/pest-e2e.
Old run directories are pruned according to reports.prune. Only directories marked as pestE2E runs are deleted, and the current run directory is never pruned.
Configuration
Key config keys in config/pest-e2e.php:
| Key | Description |
|---|---|
auth.route |
Auth endpoint path (default: /pest-e2e/auth/login) |
auth.route_enabled |
Enable auth route (default: false, set via PEST_E2E_AUTH_ROUTE_ENABLED) |
auth.ttl_seconds |
Auth ticket TTL (default: 60) |
auth.header.name / auth.header.value |
Header required for auth requests (default: X-Pest-E2E: 1) |
server.driver |
Server runner: artisan or php_builtin (default: php_builtin) |
server.host |
Bind address for the managed server (default: 127.0.0.1) |
server.port |
Base HTTP port for parallel workers (default: 8800) |
server.parallel_port_offset |
When true, parallel workers use server.port + TEST_TOKEN (default: true) |
reports.base_dir |
Base directory for Playwright artifacts (default: storage/framework/testing/pest-e2e) |
reports.prune.enabled |
Enable old run pruning (default: true) |
reports.prune.keep_runs |
Number of most recent marked runs to keep (default: 50) |
reports.prune.keep_days |
Age window for marked runs to keep (default: 7) |
timing.enabled |
Enable timing instrumentation (default: false, set via PEST_E2E_TIMING) |
js_runner.driver |
JS runner (default: playwright) |
js_runner.mode |
Runner mode: cold or warm (default: cold) |
package_manager |
Package manager for E2E runs: npm, yarn, pnpm, or bun (default: set in E2ETestCase during install, overridable via --run-using) |
parallel.base_port |
Deprecated alias for server.port |
agent_output |
Force agent JSON output (default: from PEST_E2E_AGENT_OUTPUT / PAO_FORCE env) |
bindings |
Contract-to-implementation map for swapping the JS runner. Keys: JsWorkerContract::class, JsonParserContract::class. Default: Playwright. Override to use Cypress, Puppeteer, etc. |
Final Positioning
pestE2E is not browser testing for Laravel.
It is a contract-driven bridge between Laravel and JS-native E2E systems.
If you are building advanced frontend applications — page builders, editors, complex layouts — and you want Laravel to orchestrate while JavaScript owns the browser, this package is for you.