Skip to content

Pest Bridge PluginTest External Frontends from Laravel

Write browser tests in PHP for Vue, React, Nuxt, Next.js — no JavaScript test code required

Pest Bridge Plugin Logo

For Headless Laravel + Separate Frontend

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.

See the full picture →

Laravel API(dynamic port)/api/users/api/productsFrontend(localhost:3000)/login/dashboardPluginBridgeAPI URLbridge()
php
$this->bridge('/login')->assertSee('Welcome');
// Frontend auto-receives API URL via VITE_API_URL

One Test Suite, Full Stack Coverage

Create test data in Laravel, assert on frontend UI. No JavaScript test files. No separate test runners.

All with familiar Pest syntax and assertions.

Get started →

php
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');
});

Automatic Server Management

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.

Configuration →

php
// 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');

Multi-Repository CI/CD

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.

Multi-repo setup →

yaml
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
php
// tests/Pest.php
Bridge::add('http://localhost:3000')
    ->serve('npm run dev', cwd: '../frontend');

Manual Triggers with Branch Selection

QA-ready workflows. Trigger tests manually from GitHub UI or gh CLI.

Select branches for both frontend and backend. Choose test groups to run.

Manual triggers →

bash
# 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=smoke
yaml
workflow_dispatch:
  inputs:
    backend_branch:
      description: 'Backend branch'
      default: 'develop'
    test_group:
      type: choice
      options: [all, smoke, critical]

Multiple Bridged Frontends

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.

Multiple frontends →

php
// tests/Pest.php
Bridge::add('http://localhost:3000');                  // Default
Bridge::add('http://localhost:3001', 'admin')
    ->child('/analytics', 'analytics')
    ->serve('npm run dev', cwd: '../admin');
php
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"]');
});

Vue/React Compatible

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.

Best practices →

php
// 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')
php
test('form validation works', function () {
    $this->bridge('/register')
        ->typeSlowly('[data-testid="email"]', 'invalid')
        ->click('body')  // blur triggers validation
        ->assertSee('Invalid email format');
});

Mock External APIs

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.

HTTP mocking guide →

php
// 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');

Released under the MIT License.