Browser Testing
Browser testing is an essential part of modern web development, allowing you to ensure that your application works correctly across different browsers and devices. Pest provides a simple and elegant way to write browser tests. Here is an example of how to write a browser test using Pest:
1it('may welcome the user', function () {2 $page = visit('/');3 4 $page->assertSee('Welcome');5});
This is a basic example of a browser test that checks whether the homepage contains the text "Welcome". However, Pest's browser testing capabilities go well beyond this simple example. You may use various methods to interact with the page, such as clicking buttons, filling out forms, and navigating between pages.
Here is an example of a more complex browser test, written in a Laravel application, that checks whether a user can sign in:
1it('may sign in the user', function () { 2 Event::fake(); 3 4 User::factory()->create([ // assumes RefreshDatabase trait is used on Pest.php... 5 'email' => 'nuno@laravel.com', 6 'password' => 'password', 7 ]); 8 9 $page = visit('/')->on()->mobile()->firefox();10 11 $page->click('Sign In')12 ->assertUrlIs('/login')13 ->assertSee('Sign In to Your Account')14 ->fill('email', 'nuno@laravel.com')15 ->fill('password', 'password')16 ->click('Submit')17 ->assertSee('Dashboard');18 19 $this->assertAuthenticated();20 21 Event::assertDispatched(UserLoggedIn::class);22});
As you can see, you may leverage the full power of Laravel's testing capabilities — database refreshing, event faking, and authentication assertions — while also performing real browser testing.
Getting Started
To get started with browser testing in Pest, require the Pest Browser plugin via Composer and install Playwright:
1composer require pestphp/pest-plugin-browser --dev2 3npm install playwright@latest4npx playwright install
Finally, you should add tests/Browser/Screenshots to your .gitignore file to avoid committing screenshots taken during browser tests.
Running Browser Tests
Running browser tests is similar to running regular Pest tests:
1./vendor/bin/pest
We recommend running tests in parallel using the --parallel option to speed up the execution:
1./vendor/bin/pest --parallel
For debugging purposes, you may run the tests in a headed mode and pause the execution at the end of the failed test run:
1./vendor/bin/pest --debug
Visiting Pages
The visit() method is used to navigate to a specific URL in your browser test. It provides various methods to interact with the page:
1test('example', function () {2 $page = visit('/');3 4 $page->assertSee('Welcome');5});
Using Other Browsers
By default, the visit() method uses Chrome as the browser. However, if you wish to use a different browser, you may specify it using the --browser option when running your tests:
1./vendor/bin/pest --browser firefox2./vendor/bin/pest --browser safari
If you wish to use a different browser by default without specifying it on the command line, you may set it in your Pest.php configuration file:
1pest()->browser()->inFirefox();2pest()->browser()->inSafari();
Using Other Devices
By default, the visit() method uses a desktop viewport. However, you may specify a mobile viewport by chaining the mobile() method onto the on() method:
1$page = visit('/')->on()->mobile();
If you wish to use a specific device, you may use the on() method and chain it with a device method such as macbook14() or iPhone14Pro():
1$page = visit('/')->on()->iPhone14Pro();
Using Dark Mode
By default, Pest enforces a light color scheme. However, you may specify a dark color scheme using the inDarkMode() method:
1$page = visit('/')->inDarkMode();
Visiting Multiple Pages
You may visit multiple pages simultaneously by passing an array of URLs to the visit() method. This is convenient for testing scenarios where you need to interact with multiple pages at once:
1$pages = visit(['/', '/about']); 2 3$pages->assertNoSmoke() 4 ->assertNoAccessibilityIssues() 5 ->assertNoConsoleLogs() 6 ->assertNoJavaScriptErrors(); 7 8[$homePage, $aboutPage] = $pages; 9 10$homePage->assertSee('Welcome to our website');11$aboutPage->assertSee('About Us');
Navigation
After visiting a page, you may navigate to other pages using the navigate() method. This method allows you to navigate to a different URL while keeping the current browser context:
1$page = visit('/');2 3$page->navigate('/about')4 ->assertSee('About Us');
Locating Elements
You may locate elements in the DOM using text or CSS selectors. Pest provides a simple syntax for doing so:
1// Clicks the first link with the text "Login" 2$page->click('Login'); 3 4// Clicks the first element with the class "btn-primary" 5$page->click('.btn-primary'); 6 7// Clicks the element with the data-test attribute "login" 8$page->click('@login'); 9 10// Clicks the element with the ID "submit-button"11$page->click('#submit-button');12 13// etc...
Configuring Timeouts
Sometimes elements may take time to appear on the page. By default, Pest waits 5 seconds before timing out. However, you may configure the default timeout for browser tests in your Pest.php configuration file:
1pest()->browser()->timeout(10000);
Configuring the Default User Agent
By default, the user agent matches the browser you are running your tests in, such as Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/133.0.6943.16 Safari/537.36.
Sometimes you may wish to override the browser's user agent for all of your tests. To accomplish this, you may configure it in your Pest.php configuration file:
1pest()->browser()->userAgent('CustomUserAgent');
Configuring the Default Host
By default, the server binds to 127.0.0.1 for all browser tests. However, you may wish to override the host when testing subdomain applications. To accomplish this, you may configure it in your Pest.php configuration file:
1pest()->browser()->withHost('some-subdomain.localhost');
Geolocation
Sometimes you may need to define where the browser believes it is physically located on the earth. To accomplish this, you may use the geolocation() method, which takes a latitude and longitude, sets the geolocation permission in the browser, and then makes the coordinates available via JavaScript's getCurrentPosition API:
1$page = visit('/')2 ->geolocation(39.399872, -8.224454);3 4$page->assertSee('Portugal');
You may also define one of several specific preset cities, which will configure the browser's geolocation, timezone, and locale:
1$page = visit('/')2 ->from()->losAngeles();3 4$page->assertSee('Los Angeles');5$page->assertSee('America/Los_Angeles');6$page->assertSee('en-US');
Configuring Locale
You may set the locale for your test requests using the withLocale method. This is particularly convenient for testing multilingual applications:
1$page = visit('/')->withLocale('fr-FR');2 3$page->assertSee('Bienvenue');
Configuring Timezone
You may set the timezone for your test requests using the withTimezone method. This is helpful for testing date and time displays across different time zones:
1$page = visit('/')->withTimezone('America/New_York');2 3$page->assertSee('EST');
Configuring User Agent
You may set the User-Agent header for your test requests using the withUserAgent method. This is helpful for testing how your application responds to different types of clients, such as mobile browsers or bots:
1$page = visit('/')->withUserAgent('Googlebot');2 3$page->assertSee('Welcome, bot!');
Configuring Host
You may set the host for your test server using the withHost method. This is helpful for testing subdomains, or where different hosts serve different content:
1$page = visit('/dashboard')->withHost('some-subdomain.localhost');2 3$page->assertSee('Welcome to Some Subdomain');
Table of Contents
Available Assertions
assertTitle assertTitleContains assertSee assertDontSee assertSeeIn assertDontSeeIn assertSeeAnythingIn assertSeeNothingIn assertCount assertScript assertSourceHas assertSourceMissing assertSeeLink assertDontSeeLink assertChecked assertNotChecked assertIndeterminate assertRadioSelected assertRadioNotSelected assertSelected assertNotSelected assertValue assertValueIsNot assertAttribute assertAttributeMissing assertAttributeContains assertAttributeDoesntContain assertAriaAttribute assertDataAttribute assertVisible assertPresent assertNotPresent assertMissing assertEnabled assertDisabled assertButtonEnabled assertButtonDisabled assertUrlIs assertSchemeIs assertSchemeIsNot assertHostIs assertHostIsNot assertPortIs assertPortIsNot assertPathBeginsWith assertPathEndsWith assertPathContains assertPathIs assertPathIsNot assertQueryStringHas assertQueryStringMissing assertFragmentIs assertFragmentBeginsWith assertFragmentIsNot assertNoSmoke assertNoConsoleLogs assertNoJavaScriptErrors assertNoAccessibilityIssues assertScreenshotMatches
Element Interactions
click text attribute keys withKeyDown type typeSlowly select append clear radio check uncheck attach press pressAndWaitFor drag hover submit value withinFrame resize script content url wait waitForKey
Debugging Tests
Element Assertions
assertTitle
The assertTitle method asserts that the page title matches the given text:
1$page->assertTitle('Home Page');
assertTitleContains
The assertTitleContains method asserts that the page title contains the given text:
1$page->assertTitleContains('Home');
assertSee
The assertSee method asserts that the given text is present on the page:
1$page->assertSee('Welcome to our website');
assertDontSee
The assertDontSee method asserts that the given text is not present on the page:
1$page->assertDontSee('Error occurred');
assertSeeIn
The assertSeeIn method asserts that the given text is present within the selector:
1$page->assertSeeIn('.header', 'Welcome');
assertDontSeeIn
The assertDontSeeIn method asserts that the given text is not present within the selector:
1$page->assertDontSeeIn('.error-container', 'Error occurred');
assertSeeAnythingIn
The assertSeeAnythingIn method asserts that any text is present within the selector:
1$page->assertSeeAnythingIn('.content');
assertSeeNothingIn
The assertSeeNothingIn method asserts that no text is present within the selector:
1$page->assertSeeNothingIn('.empty-container');
assertCount
The assertCount method asserts that a given element is present a given number of times:
1$page->assertCount('.item', 5);
assertScript
The assertScript method asserts that the given JavaScript expression evaluates to the given value:
1$page->assertScript('document.title', 'Home Page');2$page->assertScript('document.querySelector(".btn").disabled', true);
assertSourceHas
The assertSourceHas method asserts that the given source code is present on the page:
1$page->assertSourceHas('<h1>Welcome</h1>');
assertSourceMissing
The assertSourceMissing method asserts that the given source code is not present on the page:
1$page->assertSourceMissing('<div class="error">');
assertSeeLink
The assertSeeLink method asserts that the given link is present on the page:
1$page->assertSeeLink('About Us');
assertDontSeeLink
The assertDontSeeLink method asserts that the given link is not present on the page:
1$page->assertDontSeeLink('Admin Panel');
assertChecked
The assertChecked method asserts that the given checkbox is checked:
1$page->assertChecked('terms');2$page->assertChecked('color', 'blue'); // For checkbox with specific value
assertNotChecked
The assertNotChecked method asserts that the given checkbox is not checked:
1$page->assertNotChecked('newsletter');2$page->assertNotChecked('color', 'red'); // For checkbox with specific value
assertIndeterminate
The assertIndeterminate method asserts that the given checkbox is in an indeterminate state:
1$page->assertIndeterminate('partial-selection');
assertRadioSelected
The assertRadioSelected method asserts that the given radio field is selected:
1$page->assertRadioSelected('size', 'large');
assertRadioNotSelected
The assertRadioNotSelected method asserts that the given radio field is not selected:
1$page->assertRadioNotSelected('size', 'small');
assertSelected
The assertSelected method asserts that the given dropdown has the given value selected:
1$page->assertSelected('country', 'US');
assertNotSelected
The assertNotSelected method asserts that the given dropdown does not have the given value selected:
1$page->assertNotSelected('country', 'UK');
assertValue
The assertValue method asserts that the element matching the given selector has the given value:
1$page->assertValue('input[name=email]', 'test@example.com');
assertValueIsNot
The assertValueIsNot method asserts that the element matching the given selector does not have the given value:
1$page->assertValueIsNot('input[name=email]', 'invalid@example.com');
assertAttribute
The assertAttribute method asserts that the element matching the given selector has the given value in the provided attribute:
1$page->assertAttribute('img', 'alt', 'Profile Picture');
assertAttributeMissing
The assertAttributeMissing method asserts that the element matching the given selector is missing the provided attribute:
1$page->assertAttributeMissing('button', 'disabled');
assertAttributeContains
The assertAttributeContains method asserts that the element matching the given selector contains the given value in the provided attribute:
1$page->assertAttributeContains('div', 'class', 'container');
assertAttributeDoesntContain
The assertAttributeDoesntContain method asserts that the element matching the given selector does not contain the given value in the provided attribute:
1$page->assertAttributeDoesntContain('div', 'class', 'hidden');
assertAriaAttribute
The assertAriaAttribute method asserts that the element matching the given selector has the given value in the provided aria attribute:
1$page->assertAriaAttribute('button', 'label', 'Close');
assertDataAttribute
The assertDataAttribute method asserts that the element matching the given selector has the given value in the provided data attribute:
1$page->assertDataAttribute('div', 'id', '123');
assertVisible
The assertVisible method asserts that the element matching the given selector is visible:
1$page->assertVisible('.alert');
assertPresent
The assertPresent method asserts that the element matching the given selector is present in the DOM:
1$page->assertPresent('form');
assertNotPresent
The assertNotPresent method asserts that the element matching the given selector is not present in the DOM:
1$page->assertNotPresent('.error-message');
assertMissing
The assertMissing method asserts that the element matching the given selector is not visible:
1$page->assertMissing('.hidden-element');
assertEnabled
The assertEnabled method asserts that the given field is enabled:
1$page->assertEnabled('email');
assertDisabled
The assertDisabled method asserts that the given field is disabled:
1$page->assertDisabled('submit');
assertButtonEnabled
The assertButtonEnabled method asserts that the given button is enabled:
1$page->assertButtonEnabled('Save');
assertButtonDisabled
The assertButtonDisabled method asserts that the given button is disabled:
1$page->assertButtonDisabled('Submit');
URL Assertions
assertUrlIs
The assertUrlIs method asserts that the current URL matches the given string:
1$page->assertUrlIs('https://example.com/home');
assertSchemeIs
The assertSchemeIs method asserts that the current URL scheme matches the given scheme:
1$page->assertSchemeIs('https');
assertSchemeIsNot
The assertSchemeIsNot method asserts that the current URL scheme does not match the given scheme:
1$page->assertSchemeIsNot('http');
assertHostIs
The assertHostIs method asserts that the current URL host matches the given host:
1$page->assertHostIs('example.com');
assertHostIsNot
The assertHostIsNot method asserts that the current URL host does not match the given host:
1$page->assertHostIsNot('wrong-domain.com');
assertPortIs
The assertPortIs method asserts that the current URL port matches the given port:
1$page->assertPortIs('443');
assertPortIsNot
The assertPortIsNot method asserts that the current URL port does not match the given port:
1$page->assertPortIsNot('8080');
assertPathBeginsWith
The assertPathBeginsWith method asserts that the current URL path begins with the given path:
1$page->assertPathBeginsWith('/users');
assertPathEndsWith
The assertPathEndsWith method asserts that the current URL path ends with the given path:
1$page->assertPathEndsWith('/profile');
assertPathContains
The assertPathContains method asserts that the current URL path contains the given path:
1$page->assertPathContains('settings');
assertPathIs
The assertPathIs method asserts that the current path matches the given path:
1$page->assertPathIs('/dashboard');
assertPathIsNot
The assertPathIsNot method asserts that the current path does not match the given path:
1$page->assertPathIsNot('/login');
assertQueryStringHas
The assertQueryStringHas method asserts that the given query string parameter is present and has a given value:
1$page->assertQueryStringHas('page');2$page->assertQueryStringHas('page', '2');
assertQueryStringMissing
The assertQueryStringMissing method asserts that the given query string parameter is missing:
1$page->assertQueryStringMissing('page');
assertFragmentIs
The assertFragmentIs method asserts that the URL's current hash fragment matches the given fragment:
1$page->assertFragmentIs('section-2');
assertFragmentBeginsWith
The assertFragmentBeginsWith method asserts that the URL's current hash fragment begins with the given fragment:
1$page->assertFragmentBeginsWith('section');
assertFragmentIsNot
The assertFragmentIsNot method asserts that the URL's current hash fragment does not match the given fragment:
1$page->assertFragmentIsNot('wrong-section');
Console Assertions
assertNoSmoke
The assertNoSmoke method asserts there are no console logs or JavaScript errors on the page:
1$page->assertNoSmoke();
assertNoConsoleLogs
The assertNoConsoleLogs method asserts there are no console logs on the page:
1$page->assertNoConsoleLogs();
assertNoJavaScriptErrors
The assertNoJavaScriptErrors method asserts there are no JavaScript errors on the page:
1$page->assertNoJavaScriptErrors();
assertNoAccessibilityIssues
The assertNoAccessibilityIssues method asserts there are no "serious" accessibility issues on the page:
1$page->assertNoAccessibilityIssues();
By default, the level is 1 (serious). However, you may change it to one of the following levels:
10. Critical21. Serious32. Moderate43. Minor
- The level 0 (critical) only reports issues that cause severe barriers for individuals with disabilities. The organization may be subject to legal action if these issues are not addressed.
- The level 1 (serious) includes all critical issues (level 0) and adds issues that significantly impact accessibility. The organization may be subject to legal action if these issues are not addressed.
- The level 2 (moderate) includes all serious issues (level 1) and adds issues that moderately affect accessibility. The end-user would appreciate the fix, but it is not a barrier.
- The level 3 (minor) includes all moderate issues (level 2) and adds issues that have a minor impact on accessibility. These issues are often related to best practices and do not significantly affect the user experience.
Screenshot Assertions
assertScreenshotMatches
The assertScreenshotMatches method asserts that the screenshot matches the expected image:
1$page->assertScreenshotMatches();2$page->assertScreenshotMatches(true, true); // Full page, show diff
Element Interactions
click
The click method clicks the link with the given text:
1$page->click('Login');
text
The text method gets the text of the element matching the given selector:
1$text = $page->text('.header');
attribute
The attribute method gets the given attribute from the element matching the given selector:
1$alt = $page->attribute('img', 'alt');
keys
The keys method sends the given keys to the element matching the given selector:
1$page->keys('input[name=password]', 'secret');2$page->keys('input[name=password]', ['{Control}', 'a']); // Keyboard shortcuts
withKeyDown
The withKeyDown method executes the given callback while a key is held down:
1$page->withKeyDown('Shift', function () use ($page): void {2 $page->keys('#input', ['KeyA', 'KeyB', 'KeyC']);3}); // writes "ABC"
Note: To respect held keys such as
Shift, use key codes likeKeyA,KeyB, andKeyC—'a'always types a lowercase "a" and'A'always types an uppercase "A", regardless of modifiers.
type
The type method types the given value in the given field:
1$page->type('email', 'test@example.com');
typeSlowly
The typeSlowly method types the given value in the given field slowly, like a user:
1$page->typeSlowly('email', 'test@example.com');
select
The select method selects the given value in the given field:
1$page->select('country', 'US');2$page->select('interests', ['music', 'sports']); // Multiple select
append
The append method types the given value in the given field without clearing it:
1$page->append('description', ' Additional information.');
clear
The clear method clears the given field:
1$page->clear('search');
radio
The radio method selects the given value of a radio button field:
1$page->radio('size', 'large');
check
The check method checks the given checkbox:
1$page->check('terms');2$page->check('color', 'blue'); // For checkbox with specific value
uncheck
The uncheck method unchecks the given checkbox:
1$page->uncheck('newsletter');2$page->uncheck('color', 'red'); // For checkbox with specific value
attach
The attach method attaches the given file to the field:
1$page->attach('avatar', '/path/to/image.jpg');
press
The press method presses the button with the given text or name:
1$page->press('Submit');
pressAndWaitFor
The pressAndWaitFor method presses the button with the given text or name and waits for a specified amount of time:
1$page->pressAndWaitFor('Submit', 2); // Wait for 2 seconds
drag
The drag method drags an element to another element using selectors:
1$page->drag('#item', '#target');
hover
The hover method hovers over the given element:
1$page->hover('#item');
submit
The submit method submits the first form found on the page:
1$page->submit();
value
The value method gets the value of the element matching the given selector:
1$value = $page->value('input[name=email]');
withinFrame
The withinFrame method allows you to interact with elements inside an iframe:
1use Pest\Browser\Api\AwaitableWebpage;2 3$page->withinFrame('.iframe-container', function (AwaitableWebpage $page) {4 $page->type('frame-input', 'Hello iframe')5 ->click('frame-button');6});
resize
The resize method adjusts the size of the browser window:
1$page->resize(1280, 720);
script
The script method executes a script in the context of the page:
1$result = $page->script('document.title');
content
The content method gets the page's content:
1$html = $page->content();
url
The url method gets the page's URL:
1$currentUrl = $page->url();
wait
The wait method pauses for the given number of seconds:
1$page->wait(2); // Wait for 2 seconds
waitForKey
The waitForKey method opens the current page URL in the default web browser and waits for a key press:
1$page->waitForKey(); // Useful for debugging
Debugging Tests
Sometimes you may wish to debug your browser tests. Pest provides a convenient way to do this through the --debug option, which opens the browser window and pauses the execution of the test when it fails. You may then inspect the page and see what went wrong:
1./vendor/bin/pest --debug
Alternatively, you may use the debug() method in your test. It will limit execution to this test (like using only()), pause the execution, and open the browser window:
1$page->debug();
You may also take a screenshot of the current page using the screenshot() method, which is convenient for visual debugging:
1$page->screenshot();2$page->screenshot(fullPage: true);3$page->screenshot(filename: 'custom-name');
Note: If you do not pass a filename, the test name will be used as the filename.
You may also take a screenshot of a specific element using the screenshotElement() method:
1$page->screenshotElement('#my-element');
You may also use the tinker() method to open a Tinker session in the context of the current page, allowing you to interact with the page using PHP code:
1$page->tinker();
You may also run your tests with the --headed option to open the browser window:
1./vendor/bin/pest --headed
If you wish to run the tests in a headed mode by default, you may set it in your Pest.php configuration file:
1pest()->browser()->headed();
Continuous Integration
You may refer to Pest's Continuous Integration documentation for more information on how to run your browser tests in a CI environment.
However, if you are using GitHub Actions, you should add the following steps to your workflow file:
1- uses: actions/setup-node@v42 with:3 node-version: lts/*4 5- name: Install dependencies6 run: npm ci7 8- name: Install Playwright Browsers9 run: npx playwright install --with-deps
Now, let's look at how the Agent plugin gives your AI coding agents a single command to verify a change actually works — running inside your full test suite, and, with this plugin installed, driving a real browser too: Agent →