Pest Browser Testing with Laravel Sail on WSL2
Running browser tests in headed mode (with a visible browser window) is invaluable for debugging, but getting it to work with Laravel Sail on WSL2 requires some specific configuration. This guide walks you through the complete setup.
The Challenge
When working with Laravel Sail on WSL2, browser tests run inside Docker containers. By default, these containers don’t have access to your display, making it impossible to see the browser window during headed test runs. This becomes particularly frustrating when you want to debug failing tests or use methods like $page->debug() or $page->waitForKey().
Prerequisites
Before starting, ensure you have:
- WSL2 installed and running
- WSLg configured (verify with
echo $DISPLAY– should return:0) - Laravel Sail project set up
- Pest and Pest Browser plugin installed
If echo $DISPLAY doesn’t return anything, you’ll need to set up WSLg first (comes by default with recent Windows 11 updates).
Initial Setup
1. Install Pest Browser Plugin
First, install the necessary packages:
sail composer require pestphp/pest-plugin-browser --dev
sail npm install playwright@latest
2. Install Playwright Browsers
Run this inside your Sail container:
sail shell
npx playwright install --with-deps
exit
3. Configure .gitignore
Add the screenshots directory to avoid committing test artifacts:
echo "tests/Browser/Screenshots" >> .gitignore
Enabling Headed Mode in Docker
The key to running headed tests is configuring Docker to share your display with the container.
Modify docker-compose.yml
Add the following to your laravel.test service in docker-compose.yml:
services:
laravel.test:
build:
context: ./vendor/laravel/sail/runtimes/8.4
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
image: sail-8.4/app
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
DISPLAY: ':0' # Add this line
volumes:
- '.:/var/www/html'
- '/tmp/.X11-unix:/tmp/.X11-unix:rw' # Add this line
networks:
- sail
depends_on:
- mysql
- redis
- meilisearch
- mailpit
- selenium
What these lines do:
DISPLAY: ':0'– Tells the container which display to use'/tmp/.X11-unix:/tmp/.X11-unix:rw'– Shares the X11 Unix socket between WSL and Docker
Apply Changes
Restart your containers to apply the configuration:
sail down
sail up -d
Running Headed Tests
Now you can run your browser tests in headed mode:
# Run all browser tests in headed mode
sail pest --headed
# Run specific test file
sail pest tests/Feature/MyBrowserTest.php --headed
# Run specific test with filter
sail pest --filter "user can login" --headed
# Debug mode (pauses on failure)
sail pest --debug
When you run these commands, you’ll see browser windows appear on your WSL2 desktop!
Working with VSCode PHPUnit Test Explorer
If you’re using the PHPUnit Test Explorer extension for VSCode, you need to understand its limitations with headed tests.
The Problem
The VSCode extension runs tests through its output panel, which cannot receive keyboard input. When a test pauses (using $page->debug() or $page->waitForKey()), you won’t be able to press a key to continue, causing the test to hang.
Recommended Workflow
Use a hybrid approach:
For Regular Development:
- Use VSCode Test Explorer for quick, headless test runs
- Fast iteration and feedback during development
For Visual Debugging:
- Use the terminal with
sail pest --headed - Better control and ability to interact with paused tests
# Quick headless run (VSCode extension or terminal)
sail pest
# Visual debugging (terminal only)
sail pest tests/Feature/Auth/LoginTest.php --headed
Writing Tests
Here’s an example of a browser test that benefits from headed mode:
<?php
use function Pest\Laravel\{get, actingAs};
it('allows user to book an appointment', function () {
$user = User::factory()->create();
$page = visit('/booking')
->assertSee('Available Times')
->click('9:00 AM')
->type('name', 'John Doe')
->type('email', 'john@example.com')
->click('Book Appointment')
->assertSee('Booking Confirmed');
// Use this for debugging only when running with --headed
// $page->debug(); // Pauses execution and opens browser
});
Debugging Techniques
1. Visual Inspection with –headed
sail pest tests/Feature/BookingTest.php --headed
Watch the browser interact with your application in real-time.
2. Pause on Failure with –debug
sail pest --debug
Automatically opens the browser and pauses when a test fails.
3. Take Screenshots
// Takes screenshot with test name as filename
$page->screenshot();
// Custom filename
$page->screenshot(filename: 'booking-step-2');
// Full page screenshot
$page->screenshot(fullPage: true);
// Screenshot specific element
$page->screenshotElement('#booking-form');
Screenshots are saved to tests/Browser/Screenshots/.
4. Interactive Debugging with Tinker
// Open Tinker session in browser context
$page->tinker();
5. Wait and Inspect
// Pause and wait for manual inspection (terminal only!)
$page->waitForKey();
Common Issues and Solutions
Issue: Browser window doesn’t appear
Check WSLg:
echo $DISPLAY
# Should output: :0
Verify Docker volumes:
docker compose exec laravel.test bash
echo $DISPLAY
# Should also output: :0
Issue: "Executable doesn’t exist" error
Reinstall Playwright browsers:
sail shell
npx playwright install --with-deps
Issue: Tests hang with $page->debug()
Don’t run debug tests through VSCode extension. Use terminal instead:
sail pest tests/Feature/MyTest.php --headed
Issue: Permission denied on /tmp/.X11-unix
Ensure the volume mount has :rw (read-write) permissions:
volumes:
- '/tmp/.X11-unix:/tmp/.X11-unix:rw'
Best Practices
- Use headed mode sparingly – Run most tests headless for speed, use headed mode only when debugging
- Don’t commit with debug() calls – Remove
$page->debug()and$page->waitForKey()before committing - Create test-specific aliases – Make running headed tests easier:
alias sail-test-headed='sail pest --headed' alias sail-test-debug='sail pest --debug' - Use screenshots for CI – In CI environments where headed mode isn’t available, rely on screenshots:
if (! app()->environment('local')) { $page->screenshot(); } - Combine with conditional debugging:
if (env('TEST_DEBUG', false)) { $page->debug(); }
Then run:
TEST_DEBUG=true sail pest tests/Feature/MyTest.php --headed
Running in CI (GitHub Actions)
For CI environments, you’ll need to install Playwright differently:
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Browser Tests
run: ./vendor/bin/pest
Note: Don’t use --headed in CI. Use screenshots for debugging failed CI tests.
Conclusion
Running Pest browser tests in headed mode with Laravel Sail on WSL2 requires proper Docker configuration to share your display. Once set up, you get a powerful debugging workflow:
- VSCode Test Explorer for fast, headless development iterations
- Terminal with –headed for visual debugging when needed
- Screenshots and tinker() for inspecting state
The key is understanding when to use each tool and avoiding the pitfalls of running interactive tests through VSCode’s output panel.
Happy testing! 🧪✨
Quick Reference Commands:
# Initial setup
sail composer require pestphp/pest-plugin-browser --dev
sail npm install playwright@latest
sail shell && npx playwright install --with-deps
# Run tests
sail pest # Headless (fast)
sail pest --headed # Headed (visual)
sail pest --debug # Pause on failure
sail pest <file> --filter "test name" --headed
# Stop hung tests
docker compose exec laravel.test pkill -9 pest
# Verify WSLg
echo $DISPLAY # Should show :0