zenstruck/browser
A Laravel-friendly browser testing toolkit built on Symfony BrowserKit and Panther. Easily crawl pages, click links, submit forms, assert on HTML, and drive real headless browsers—great for end-to-end tests and fluent, expressive UI assertions.
Installation:
composer require zenstruck/browser --dev
Add the PHPUnit extension to phpunit.xml:
<extensions>
<extension class="Zenstruck\Browser\Test\BrowserExtension" />
</extensions>
Basic Test Setup:
Extend your test class with HasBrowser trait and ensure it extends either:
Symfony\Bundle\FrameworkBundle\Test\KernelTestCase (for KernelBrowser)Symfony\Component\Panther\PantherTestCase (for PantherBrowser)use Zenstruck\Browser\Test\HasBrowser;
class MyTest extends KernelTestCase
{
use HasBrowser;
public function testBasicPageVisit()
{
$this->browser()
->visit('/')
->assertSee('Welcome');
}
}
Test a simple form submission:
public function testFormSubmission()
{
$this->browser()
->visit('/contact')
->fillField('name', 'John Doe')
->fillField('email', 'john@example.com')
->click('Submit')
->assertSee('Thank you, John Doe');
}
public function testAuthenticatedAccess()
{
$user = $this->createTestUser(); // Your user factory method
$this->browser()
->actingAs($user)
->visit('/dashboard')
->assertSee('Welcome, ' . $user->getUsername());
}
public function testApiResponse()
{
$this->browser()
->post('/api/users', HttpOptions::json(['name' => 'Alice']))
->assertJson()
->assertJsonMatches('id', 1)
->assertJsonMatches('name', 'Alice');
}
public function testDynamicContent()
{
$this->pantherBrowser()
->visit('/dashboard')
->waitFor('.user-menu') // Wait for JS-rendered element
->click('.user-menu')
->assertSee('Logout');
}
public function testFormValidation()
{
$this->browser()
->visit('/register')
->fillField('email', 'invalid-email')
->click('Submit')
->assertSee('The email must be a valid email address.');
}
public function testRedirectAfterLogin()
{
$this->browser()
->visit('/login')
->fillField('email', 'user@example.com')
->fillField('password', 'password')
->click('Login')
->assertRedirectedTo('/dashboard');
}
zenstruck/foundryuse Zenstruck\Foundry\Test\Factories;
class UserTest extends KernelTestCase
{
use HasBrowser, Factories;
public function testUserCreation()
{
$user = UserFactory::new()->create();
$this->browser()
->actingAs($user)
->visit('/profile')
->assertSee($user->getEmail());
}
}
Extend the Browser class to add domain-specific assertions:
class CustomBrowser extends Browser
{
public function assertFlashMessage(string $message)
{
$this->assertSeeIn('.alert', $message);
}
}
// Usage in test:
$this->browser()->assertFlashMessage('Success!');
Use environment variables to control behavior:
// .env.test
BROWSER_SOURCE_DIR=/tmp/browser_sources
BROWSER_CATCH_EXCEPTIONS=false
dd() and dump()public function testDebugResponse()
{
$this->browser()
->visit('/complex-page')
->dump('body') // Dump HTML body
->dd('data.users[0].name'); // Dump and die for JSON response
}
Authentication Issues:
LogicException: Cannot create the remember-me cookie, ensure:
is_granted()).->withProfiling() or framework.profiler.collect: true in config/packages/test.php).PantherBrowser Slowness:
KernelBrowser.Redirect Handling:
->interceptRedirects() to inspect redirect responses.$this->browser()
->visit('/login')
->interceptRedirects()
->click('Logout')
->assertRedirected();
Exception Handling:
->throwExceptions() if you need expectException().$this->browser()
->throwExceptions()
->visit('/admin'); // Fails if unauthorized
Kernel Rebooting:
->disableReboot() for performance-critical tests (e.g., API suites).->enableReboot() if needed.JMESPath Dependencies:
mtdowling/jmespath.php for JSON assertions. Install with:
composer require --dev mtdowling/jmespath.php
Save Source for Inspection:
$this->browser()
->visit('/problem-page')
->saveSource('debug.html'); // Saves to `var/browser/source/debug.html`
Inspect Cookies:
$this->browser()->use(function (CookieJar $cookieJar) {
var_dump($cookieJar->all());
});
Check Profiler Data:
$this->browser()
->withProfiling()
->visit('/slow-endpoint')
->use(function (RequestDataCollector $collector) {
echo $collector->getResponseTime();
});
Enable Browser Extension Artifacts:
BrowserExtension auto-saves screenshots/source on test failures. Configure the output directory via:
BROWSER_SOURCE_DIR=/path/to/custom/dir
Custom Browser Classes:
Extend KernelBrowser or PantherBrowser to add domain logic:
class AdminBrowser extends KernelBrowser
{
public function loginAsAdmin()
{
return $this
->visit('/admin/login')
->fillField('email', 'admin@example.com')
->fillField('password', 'admin')
->click('Login');
}
}
Override Default Assertions:
Use PHPUnit’s @method annotations to document custom methods:
/**
* @method static self assertAdminPanelVisible()
*/
class AdminTestBrowser extends KernelBrowser
{
public function assertAdminPanelVisible()
{
$this->assertSeeElement('#admin-panel');
}
}
Hook into Browser Lifecycle:
Use the use() method to inject logic before/after actions:
$this->browser()->use(function (Browser $browser) {
$browser->getClient()->getCookieJar()->clear();
});
Mock External Services: Combine with Symfony’s HTTP client for mocking APIs:
$this->browser()
->use(function (Client $client) {
$client->addHandler(new MockApiHandler());
})
->visit('/api-dependent-page');
How can I help you explore Laravel packages today?