Pest Bridge PluginTest External Frontends from Laravel
Write browser tests in PHP for Vue, React, Nuxt, Next.js — no JavaScript test code required
Write browser tests in PHP for Vue, React, Nuxt, Next.js — no JavaScript test code required
Two apps, two ports, two problems. Your tests can't reach the frontend. Your frontend can't find the API during tests.
pest-plugin-bridge solves both: bridge() for test→frontend, automatic API URL injection for frontend→API.
$this->bridge('/login')->assertSee('Welcome');
// Frontend auto-receives API URL via VITE_API_URLCreate test data in Laravel, assert on frontend UI. No JavaScript test files. No separate test runners.
All with familiar Pest syntax and assertions.
test('user can login', function () {
$user = User::factory()->create(['email' => 'test@example.com']);
$this->bridge('/login')
->typeSlowly('[data-testid="email"]', 'test@example.com')
->typeSlowly('[data-testid="password"]', 'password')
->click('[data-testid="login-button"]')
->waitForEvent('networkidle')
->assertPathContains('/dashboard')
->assertSee('Welcome');
});No manual server start. Frontend starts on first bridge() call, stops when tests complete.
API URL automatically injected for Vite, Nuxt, Next.js, and React. Child frontends share the same server process.
// tests/Pest.php
use TestFlowLabs\PestPluginBridge\Bridge;
Bridge::add('http://localhost:3000')
->child('/admin', 'admin') // Same server, /admin path
->child('/analytics', 'analytics')
->serve('npm run dev', cwd: '../frontend');Separate repos? No problem. GitHub Actions checks out both repositories side-by-side.
Tests run from Laravel, frontend served automatically. Works with private repos too.
steps:
- name: Checkout API
uses: actions/checkout@v4
with:
path: backend
- name: Checkout Frontend
uses: actions/checkout@v4
with:
repository: your-org/frontend-repo
path: frontend// tests/Pest.php
Bridge::add('http://localhost:3000')
->serve('npm run dev', cwd: '../frontend');QA-ready workflows. Trigger tests manually from GitHub UI or gh CLI.
Select branches for both frontend and backend. Choose test groups to run.
# Trigger with specific branches and test group
gh workflow run browser-tests.yml \
-f backend_branch=feature/payment \
-f frontend_branch=develop \
-f test_group=smokeworkflow_dispatch:
inputs:
backend_branch:
description: 'Backend branch'
default: 'develop'
test_group:
type: choice
options: [all, smoke, critical]Admin panel + Customer portal + Mobile? Test them all in one test suite with named bridged frontends.
Each bridged frontend gets its own port and server command. Child frontends share the parent's server.
// tests/Pest.php
Bridge::add('http://localhost:3000'); // Default
Bridge::add('http://localhost:3001', 'admin')
->child('/analytics', 'analytics')
->serve('npm run dev', cwd: '../admin');test('customer views products', function () {
$this->bridge('/products')->assertSee('Catalog');
});
test('admin manages users', function () {
$this->bridge('/users', 'admin')->assertSee('User Management');
});
test('analytics shows charts', function () {
$this->bridge('/', 'analytics')->assertVisible('[data-testid="chart"]');
});Reactive frameworks just work. typeSlowly() triggers real keyboard events that Vue v-model and React hooks respond to.
Works with Vue, Nuxt, React, Next.js, Angular, Svelte.
// fill() sets DOM value directly — Vue v-model won't see it
->fill('[data-testid="email"]', 'test@example.com')
// typeSlowly() triggers keydown/input/keyup events
->typeSlowly('[data-testid="email"]', 'test@example.com')test('form validation works', function () {
$this->bridge('/register')
->typeSlowly('[data-testid="email"]', 'invalid')
->click('body') // blur triggers validation
->assertSee('Invalid email format');
});Stripe, SendGrid, Weather APIs? Mock them in your tests without real network calls.
Bridge::fake() intercepts Laravel backend HTTP calls. Bridge::mockBrowser() intercepts frontend JavaScript fetch/XHR.
// Backend: Laravel → Stripe API
Bridge::fake([
'https://api.stripe.com/*' => [
'status' => 200,
'body' => ['id' => 'ch_123', 'status' => 'succeeded'],
],
]);
// Frontend: Browser JS → Weather API
Bridge::mockBrowser([
'https://api.weather.com/*' => [
'status' => 200,
'body' => ['city' => 'Istanbul', 'temp' => 25],
],
]);
$this->bridge('/dashboard')
->waitForEvent('networkidle')
->assertSee('Payment: succeeded')
->assertSee('Istanbul, 25°C');