Skip to main content
WireKit
Copy for LLM

Pest Browser-Test Setup

Pest v4's browser plugin (pestphp/pest-plugin-browser) drives a real Chromium instance under the hood via Playwright. It lets you write expressive end-to-end tests against any WireKit-using Laravel app — mobile-viewport rendering, click flows, animation behavior, console- error assertions — all in PHP.

This page is a tested recipe for wiring it into a fresh project. Follow the steps top-to-bottom and you should have a green smoke test inside one setup pass.

Prerequisites

Tool Minimum version
PHP 8.4
Composer 2.5
Node.js 20 LTS
npm 10
WireKit 2.0.0

Step 1 — Install the plugin

composer require pestphp/pest-plugin-browser --dev

The plugin's Composer service-provider hook should auto-generate a vendor/pest-plugins.json manifest. If your CI or local Composer is running in root mode (the COMPOSER_ALLOW_SUPERUSER flag is set), the hook is skipped — you'll need to run the dump manually:

composer pest:dump-plugins

You can verify the file exists:

test -f vendor/pest-plugins.json && echo "OK" || echo "missing"

Step 2 — Install Playwright

Pest-plugin-browser bundles Playwright as a transitive Node dependency. Add it to your project's package.json and install the Chromium binary:

npm install playwright@latest --save-dev
npx playwright install chromium

The first run downloads ~120 MB of browser binaries. Subsequent runs reuse the cached install.

Step 3 — Wire up tests/Pest.php

Add the Browser suite extension to your tests/Pest.php:

<?php

declare(strict_types=1);

use Tests\TestCase;

pest()->extend(TestCase::class)
    ->in('Feature');

pest()->extend(TestCase::class)
    ->in('Browser');

Both suites share the same Tests\TestCase base class. The Browser plugin auto-wires its WebSocket client to the Playwright server when the test runs under the Browser suite filter.

Step 4 — Verify your phpunit.xml

The browser plugin spins up an HTTP server to serve your Laravel app during tests. That server needs the same Laravel environment variables your normal app uses, especially:

<phpunit>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_KEY" value="base64:YOUR_TEST_KEY_HERE"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="CACHE_STORE" value="array"/>
    </php>
</phpunit>

Without APP_KEY, every page renders 500 Server Error (No application encryption key has been specified). Generate a fresh key for your test environment:

php artisan key:generate --show

Copy the output into the APP_KEY env entry above.

Step 5 — Build your front-end

Run a production build so the Vite manifest exists before the test suite starts:

npm run build

This populates public/build/manifest.json, which Blade's @vite() directive resolves to. Without the manifest, the test pages crash with a ViewException.

Step 6 — Write a smoke test

Create tests/Browser/SmokeTest.php:

<?php

declare(strict_types=1);

it('homepage renders without JavaScript errors', function () {
    $page = $this->visit('/');
    $page->wait(2);

    // Capture any console.error or uncaught exception that fires
    // during render. The errors array MUST be empty.
    $page->script("
        window.__capturedErrors = [];
        console.error = (...args) => window.__capturedErrors.push(args.map(a => String(a)).join(' '));
        window.addEventListener('error', (e) => window.__capturedErrors.push('uncaught: ' + e.message));
    ");

    $page->wait(1);

    $errors = $page->script('window.__capturedErrors || []');

    expect($errors)->toBe([]);
});

Run it:

vendor/bin/pest tests/Browser/SmokeTest.php

You should see one passing test.

Mobile-Viewport Testing

Switch the rendering viewport with the device API on the visited page:

it('landing page renders correctly on iPhone 14 Pro', function () {
    $page = $this->visit('/')->on()->iPhone14Pro();
    $page->wait(2);

    expect($page->script('(() => window.innerWidth)()'))->toBe(393);
});

it('landing page renders correctly on iPad Pro', function () {
    $page = $this->visit('/')->on()->iPadPro();
    $page->wait(2);

    expect($page->script('(() => window.innerWidth)()'))->toBe(1024);
});

The full device catalog includes iPhone 14/15/15Pro/SE, iPad Pro/Mini, Pixel 6a/7/8, Galaxy S22/S23/S24Ultra, plus macBook 14/16 and a generic desktop() / mobile() selector. Browse vendor/pestphp/pest-plugin-browser/src/Api/On.php for the full set.

Prefers-Reduced-Motion Override

Headless Chromium defaults to prefers-reduced-motion: reduce. That shortcuts every animation in your code path — including the ones you might want to test. Override it with a media-query stub:

$page->script("
    window.matchMedia = (q) => ({
        matches: false,
        media: q || '',
        onchange: null,
        addListener: () => {},
        removeListener: () => {},
        addEventListener: () => {},
        removeEventListener: () => {},
        dispatchEvent: () => false,
    });
");

Apply this before any assertion that depends on animation behavior.

Known Issues

Issue 1 — vendor/pest-plugins.json not generated under root composer

If composer ran with COMPOSER_ALLOW_SUPERUSER=1 (common in Docker containers, CI runners, and some Linux-as-root setups), the post-autoload-dump hook that writes vendor/pest-plugins.json is silently skipped. Without this file, every $this->visit(...) call fails with:

Call to a member function sendText() on null
  at vendor/pestphp/pest-plugin-browser/src/Playwright/Client.php:87

Fix: run composer pest:dump-plugins explicitly. The file is checked into vendor/, so it gets regenerated on every install.

Issue 2 — Playwright server process leaks across test runs

After every Pest browser run, the Node process spawned by ./node_modules/.bin/playwright run-server sometimes stays alive because the Symfony Process graceful-shutdown grace period doesn't terminate the child before the parent Pest process exits. Subsequent runs spawn fresh servers on different ports, and RAM accumulates.

Workaround: add a post-run cleanup step in CI:

vendor/bin/pest tests/Browser/
pkill -9 -f playwright

For local development the leaked servers are short-lived (killed on reboot), but in long-running CI containers the pkill line prevents OOM.

Cross-References