Writing Tests
Learn how to write effective browser tests for your external frontend applications.
Basic Structure
A browser test uses bridge() to navigate to your frontend:
test('homepage loads correctly', function () {
$this->bridge('/')
->assertSee('Welcome');
});The bridge() Method
bridge() is the core method provided by the plugin. It:
- Takes a path (e.g.,
/login,/dashboard) - Prepends your configured base URL
- Returns a Pest browser page object for chaining
// If base URL is http://localhost:5173
$this->bridge('/login');
// Actually visits: http://localhost:5173/loginPage Navigation
Basic Navigation
test('navigation works', function () {
$this->bridge('/')
->click('a[href="/about"]')
->assertPathContains('/about');
});Direct Navigation
test('can access different pages', function () {
$this->bridge('/products')
->assertSee('Products');
$this->bridge('/contact')
->assertSee('Contact Us');
});Form Interactions
Filling Inputs
Vue/React/Nuxt Users
Use typeSlowly() instead of fill() for reactive frameworks. Vue's v-model and React's controlled inputs don't sync with fill() because it sets DOM values directly without firing input events.
// For non-reactive forms (plain HTML):
$this->bridge('/register')
->fill('input[name="name"]', 'John Doe');
// For Vue, React, Nuxt, Next.js (recommended):
$this->bridge('/register')
->waitForEvent('networkidle')
->click('input[name="name"]')
->typeSlowly('input[name="name"]', 'John Doe', 20);Detailed Framework Guide
For comprehensive explanation of fill() vs typeSlowly() behavior and complete working patterns for Vue/React, see Best Practices: Vue/Nuxt Framework-Specific.
Using Data Test IDs (Recommended)
test('can fill form with data-testid', function () {
$this->bridge('/register')
->fill('[data-testid="name-input"]', 'John Doe')
->fill('[data-testid="email-input"]', 'john@example.com')
->fill('[data-testid="password-input"]', 'secret123');
});Clicking Buttons
test('can submit form', function () {
$this->bridge('/login')
->fill('[data-testid="email"]', 'user@example.com')
->fill('[data-testid="password"]', 'password')
->click('[data-testid="submit-button"]');
});Selecting Options
test('can select from dropdown', function () {
$this->bridge('/settings')
->select('[data-testid="language-select"]', 'en')
->select('[data-testid="timezone-select"]', 'UTC');
});Checkboxes and Radio Buttons
test('can interact with checkboxes', function () {
$this->bridge('/preferences')
->check('[data-testid="newsletter-checkbox"]')
->uncheck('[data-testid="marketing-checkbox"]');
});Radio Buttons
test('can select radio options', function () {
$this->bridge('/survey')
->radio('size', 'large')
->radio('color', 'blue');
});Appending Text
Add text to an input without clearing existing content:
$this->bridge('/editor')
->append('[data-testid="description"]', ' Additional text');Clearing Fields
Clear an input field:
$this->bridge('/search')
->clear('[data-testid="search-input"]');Pressing Buttons
Press a button by its visible text:
$this->bridge('/checkout')
->press('Complete Order');Press and Wait
Press a button and wait for a specified duration:
$this->bridge('/checkout')
->pressAndWaitFor('Submit', 2); // Press and wait 2 secondsForm Submission
Submit the first form on the page:
$this->bridge('/contact')
->fill('[data-testid="email"]', 'user@example.com')
->fill('[data-testid="message"]', 'Hello!')
->submit();File Uploads
Attach a file to a file input:
$this->bridge('/profile')
->attach('[data-testid="avatar-input"]', '/path/to/image.jpg');Waiting Strategies
Fixed Wait
Use wait() for simple timing:
test('waits for animation', function () {
$this->bridge('/dashboard')
->click('[data-testid="menu-toggle"]')
->wait(0.5) // Wait 500ms for animation
->assertVisible('[data-testid="sidebar"]');
});Wait for Element
Use assertVisible() which waits for elements:
test('waits for element to appear', function () {
$this->bridge('/search')
->fill('[data-testid="search-input"]', 'test')
->click('[data-testid="search-button"]')
->assertVisible('[data-testid="search-results"]'); // Waits automatically
});Wait After Actions
For async operations, add waits after actions:
test('handles async login', function () {
$this->bridge('/login')
->fill('[data-testid="email"]', 'user@example.com')
->fill('[data-testid="password"]', 'password')
->click('[data-testid="login-button"]')
->wait(2) // Wait for API call and redirect
->assertPathContains('/dashboard');
});Working with Text
Assert Text Visible
test('displays welcome message', function () {
$this->bridge('/')
->assertSee('Welcome to our app')
->assertDontSee('Error');
});Assert Text in Element
test('displays user name in header', function () {
$this->bridge('/dashboard')
->assertSeeIn('[data-testid="user-greeting"]', 'Hello, John');
});Element Visibility
test('modal opens and closes', function () {
$this->bridge('/page')
->assertNotVisible('[data-testid="modal"]')
->click('[data-testid="open-modal"]')
->assertVisible('[data-testid="modal"]')
->click('[data-testid="close-modal"]')
->assertNotVisible('[data-testid="modal"]');
});URL Assertions
test('redirects after login', function () {
$this->bridge('/login')
->fill('[data-testid="email"]', 'user@example.com')
->fill('[data-testid="password"]', 'password')
->click('[data-testid="login-button"]')
->wait(2)
->assertPathContains('/dashboard')
->assertPathIs('/dashboard');
});Getting Element Values
Get Element Text
Retrieve the text content of an element:
$this->bridge('/dashboard')
->text('[data-testid="welcome-message"]'); // Returns "Welcome, John"Get Input Value
Retrieve the current value of an input:
$this->bridge('/profile')
->value('[data-testid="email-input"]'); // Returns "user@example.com"Get Attribute Value
Retrieve an attribute value from an element:
$this->bridge('/products')
->attribute('[data-testid="product-image"]', 'alt'); // Returns "Product Name"Get Page Content
Retrieve the full HTML content of the page:
$html = $this->bridge('/')
->content();Get Current URL
Retrieve the current page URL:
$url = $this->bridge('/dashboard')
->url();Advanced Interactions
Hover
Hover over an element:
$this->bridge('/menu')
->hover('[data-testid="dropdown-trigger"]')
->assertVisible('[data-testid="dropdown-menu"]');Drag and Drop
Drag an element to a target:
$this->bridge('/kanban')
->drag('[data-testid="task-1"]', '[data-testid="done-column"]');Keyboard Input
Send keyboard events to an element:
// Type text with keyboard
$this->bridge('/editor')
->keys('[data-testid="editor"]', 'Hello World');
// Keyboard shortcuts
$this->bridge('/editor')
->keys('[data-testid="editor"]', ['{Control}', 'a']); // Select allHold Key During Actions
Perform actions while holding a key:
$this->bridge('/file-manager')
->withKeyDown('Shift', function ($page) {
$page->click('[data-testid="file-1"]')
->click('[data-testid="file-5"]'); // Multi-select
});Working with iFrames
Interact with content inside iframes:
$this->bridge('/embedded')
->withinIframe('[data-testid="iframe-container"]', function ($page) {
$page->fill('[data-testid="name"]', 'John Doe')
->click('[data-testid="submit"]');
});JavaScript Execution
Execute JavaScript on the page:
// Get a value
$title = $this->bridge('/')
->script('document.title');
// Modify the page
$this->bridge('/')
->script('document.body.style.backgroundColor = "red"');In-Page Navigation
Navigate within the same browser context (maintains state):
$this->bridge('/login')
->typeSlowly('[data-testid="email"]', 'user@example.com', 20)
->typeSlowly('[data-testid="password"]', 'password', 20)
->click('[data-testid="login-button"]')
->navigate('/dashboard') // Navigates without losing session
->assertSee('Welcome');Complete Example
Here's a complete test file demonstrating various patterns:
<?php
declare(strict_types=1);
use App\Models\User;
use App\Models\Product;
describe('E-commerce checkout flow', function () {
beforeEach(function () {
// Setup test data in Laravel
$this->user = User::factory()->create();
$this->product = Product::factory()->create([
'name' => 'Test Product',
'price' => 29.99,
]);
});
test('guest can browse products', function () {
$this->bridge('/products')
->assertSee('Test Product')
->assertSee('$29.99');
});
test('guest can add product to cart', function () {
$this->bridge('/products')
->click("[data-testid=\"product-{$this->product->id}\"]")
->assertPathContains('/products/')
->click('[data-testid="add-to-cart"]')
->wait(1)
->assertSee('Added to cart');
});
test('user can complete checkout', function () {
$this->bridge('/login')
->waitForEvent('networkidle')
->click('[data-testid="email"]')
->typeSlowly('[data-testid="email"]', $this->user->email, 20)
->typeSlowly('[data-testid="password"]', 'password', 20)
->click('[data-testid="login-button"]')
->waitForEvent('networkidle')
->assertPathContains('/dashboard');
$this->bridge('/cart')
->click('[data-testid="checkout-button"]')
->waitForEvent('networkidle')
->typeSlowly('[data-testid="card-number"]', '4242424242424242', 20)
->typeSlowly('[data-testid="card-expiry"]', '12/25', 20)
->typeSlowly('[data-testid="card-cvc"]', '123', 20)
->click('[data-testid="pay-button"]')
->waitForEvent('networkidle')
->assertSee('Order confirmed');
});
});