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
- Authoring Custom Alpine Plugins — discipline for the plugins your browser tests will exercise.
- Console Output Baseline — what a clean install should emit (and what to filter out in your assertions).