valcuandrei/pest-e2e

A Laravel-first package that runs JS-owned E2E and component tests from Pest without introducing a PHP browser DSL.

Maintainers

Package info

github.com/valcuandrei/pestE2E

pkg:composer/valcuandrei/pest-e2e

Statistics

Installs: 27

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-05-19 04:05 UTC

This package is auto-updated.

Last update: 2026-05-19 04:06:23 UTC


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() and runTest()
  • 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(), or type() 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 / baseUrl passed to Playwright matching that port
  • worker-scoped auth ticket cache keys (no cross-worker ticket bleed)
  • the same DB_* / CACHE_PREFIX env as the PHP test worker (including testing_test_{TEST_TOKEN})

Default port formula when server.parallel_port_offset is enabled:

server.port + TEST_TOKEN

Example with base port 8800: worker 18801, worker 48804. 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.php to include E2ETestCase
  • Publish config/pest-e2e.php
  • Publish the base E2E test case
  • Publish the JS harness
  • Publish the Playwright integration
  • Install @playwright/test and download Playwright browser binaries (playwright install)
  • Create or update .env.testing with parallel-safe E2E overrides
  • Create database/testing.sqlite for projects that intentionally use SQLite
  • Configure phpunit.xml to let .env.testing control DB/cache (comment out overrides)
  • Ensure phpunit.xml defines a Browser testsuite for tests/Browser (when phpunit.xml exists; idempotent on every successful install)
  • When Laravel Sail is present (laravel/sail in composer.json or vendor/laravel/sail, plus a laravel.test service), 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, then docker-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:

  1. Boots a temporary Laravel HTTP server
  2. Forces it into APP_ENV=testing
  3. Binds it to 127.0.0.1 on a free port
  4. Executes your JS runner against that server
  5. Collects the JSON report
  6. 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 in E2ETestCase::$e2ePackageManager during 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.