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.