Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Webdriver Laravel Package

lullabot/php-webdriver

A PHP client for the WebDriver protocol (Selenium), maintained by Lullabot for browser automation and functional testing. Fork of php-webdriver with fixes suited to Drupal and Behat-based test suites.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the Lullabot-maintained fork of php-webdriver/webdriver — the low-level W3C WebDriver client that sits underneath Laravel Dusk. You reach for it directly when Dusk's CSS-selector-first API isn't enough: raw capability control (mobile emulation, headless Chrome flags, goog:loggingPrefs), multi-tab/iframe work, file uploads against non-form inputs, or driving a browser against a non-Laravel target from a Laravel test suite.

Minimal steps:

composer require --dev lullabot/php-webdriver:^2.0.7

Pin ^2.0.7 (not ^2.0.6) if you run PHP 8.4/8.5 — v2.0.7 is the first release that drops the deprecated curl_close() call, so it's the version that won't spray Deprecated: notices on PHP 8.5 or trip PHPUnit's failOnDeprecation.

The namespace is unchanged (Facebook\WebDriver\*), so any existing snippet or Dusk internals keep working. Before installing, run composer why php-webdriver/webdriver — Dusk pulls the upstream package, and you only want one copy of the client resolving Facebook\WebDriver\Remote\RemoteWebDriver.

Start a browser:

docker run --rm -p 4444:4444 -p 7900:7900 selenium/standalone-chrome:latest

First use case — a single spec that proves the plumbing works:

use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\WebDriverBy;

$driver = RemoteWebDriver::create('http://localhost:4444', DesiredCapabilities::chrome());
$driver->get(config('app.url').'/login');
$driver->findElement(WebDriverBy::id('email'))->sendKeys('[email protected]');
$driver->findElement(WebDriverBy::cssSelector('button[type=submit]'))->click();

$this->assertStringContainsString('Dashboard', $driver->getTitle());
$driver->quit();

Where to look first in the source: Remote/RemoteWebDriver.php (session lifecycle), WebDriverBy.php (locator strategy), Support/WebDriverWait.php (the only correct way to wait), and Remote/RemoteWebElement.php (which methods throw StaleElementReferenceException).

Implementation Patterns

Fixture-based base test case. Wrap session creation and teardown once so every spec is clean. $driver->quit() must be in tearDown, not in the happy path — a failed assertion otherwise strands a session until the Selenium container hits its slot limit and starts returning session not created.

protected function setUp(): void
{
    parent::setUp();
    $this->driver = RemoteWebDriver::create(
        env('SELENIUM_URL', 'http://selenium:4444'),
        DesiredCapabilities::chrome()
    );
    $this->driver->manage()->window()->setSize(new WebDriverDimension(1440, 900));
}

protected function tearDown(): void
{
    $this->driver?->quit();
    parent::tearDown();
}

Explicit waits, never sleep(). WebDriverWait polls until a closure returns truthy, and throws TimeoutException with a useful message. Sleeping is the single biggest source of flaky, slow suites.

$el = (new WebDriverWait($driver, 10))
    ->until(fn ($d) => $d->findElement(WebDriverBy::cssSelector('.alert-success')));

Page objects. Keep selectors out of test bodies. A small class per screen with open(), fillLogin() and assertLoggedIn() turns "the login button moved" into a one-line change instead of a 40-test rewrite. This also makes the suite readable to the QA/product folks reviewing PRs.

Screenshot-on-failure. In tearDown, check $this->hasFailed() (PHPUnit) and call $this->driver->takeScreenshot($path) before quitting. In CI, publish those paths as build artifacts — Selenium traces rarely tell you what the user saw.

Overriding Dusk's driver. If you need this fork's capabilities inside Dusk rather than beside it, Dusk resolves Facebook\WebDriver\Remote\RemoteWebDriver::create() internally, so the supported lever is Dusk's ChromeOptions/desiredCapabilities overrides in DuskTestCase::driver(), not swapping the client. Keep raw php-webdriver for the tests Dusk can't express cleanly.

Headless in CI / containers. Build capabilities explicitly rather than relying on the driver's defaults:

$options = new ChromeOptions();
$options->addArguments(['--headless=new', '--no-sandbox', '--disable-dev-shm-usage', '--window-size=1440,900']);
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY_W3C, $options);

--disable-dev-shm-usage is mandatory in Docker unless you raised /dev/shm; without it Chrome crashes at random under parallel load and you'll blame your tests.

Diagnostics via browser console. Enable goog:loggingPrefs with ['browser' => 'ALL'] in capabilities, then $driver->manage()->getLog('browser') in tearDown. JS errors that never surface as failed assertions become visible.

Parallelism. One RemoteWebDriver per process. Give PHPUnit/Paratest N forks and point them at Selenium Grid or a selenium/standalone-chrome with enough slots; never share a driver instance between tests.

Gotchas and Tips

v2.0.7 is a maintenance release — no API changes, no breaking changes. It removes a curl_close() call that does nothing and is deprecated in PHP 8.5, repairs the coverage config, and adds PHP 8.4 to the test matrix. Practically: bumping from 2.0.6 is risk-free and is what you want before your CI image moves to PHP 8.5. The deprecation was cosmetic (curl handles are freed when the object is GC'd), so don't hunt for a behavioural change in HTTP transport — there isn't one. Note the coverage fix is upstream-only and changes nothing in the installed package.

Session leaks are the number-one operational failure. Always quit() in finally/tearDown. Symptoms of a leak: session not created: maximum number of active sessions reached, or tests that pass alone and hang in the suite.

close() ≠ quit(). close() closes the current tab; if it was the last tab the session is destroyed but the driver object still thinks it's alive and the next call throws no such session. Just use quit().

StaleElementReferenceException is normal, not a bug. Any DOM re-render invalidates previously-found elements. Re-find inside the wait callback rather than hoisting the element reference out of the loop.

Don't mix implicit and explicit waits. Setting $driver->manage()->timeouts()->implicitlyWait(5) and using WebDriverWait produces unpredictable, additive timeouts that are miserable to debug. Pick WebDriverWait exclusively.

Driver/browser version drift. Selenium 4.6+ ships Selenium Manager, which downloads matching ChromeDriver automatically — so pointing at http://localhost:4444 and letting it work beats pinning a chromedriver binary in PATH. If you do pin, bump selenium/standalone-chrome and ChromeDriver together.

Element lookup is an HTTP round-trip. findElements() returns everything in one call; a loop of findElement() on a 200-row table is 200 round-trips. Batch it, or drop to executeScript() for read-only assertions where you don't need the element API.

Composer resolution with Dusk. If composer why php-webdriver/webdriver still lists the upstream package after adding this fork, you have two copies and an ambiguous RemoteWebDriver class. Resolve it at the root composer.json with a replace/conflict entry, or scope the fork to a dedicated browser-test package — the class name collision is the failure mode, not the install.

Debugging a stuck call. Enable the Selenium container's VNC/SE_VNC_NO_PASSWORD view to watch the browser in real time; a command that "hangs" is usually a click landing on an overlay. For protocol-level detail, goog:loggingPrefs plus Selenium's --log-level=DEBUG tells you the exact wire command and response.

Extension points. RemoteWebDriver::create() accepts a custom WebDriverCommandExecutor, which is the seam for request signing, custom retry logic, or an HTTP proxy. Element/page-object abstractions are yours to build — the package deliberately stays a thin transport, so don't expect higher-level ergonomics.

Keep the pin current. This fork exists precisely to track PHP and Chrome releases faster than upstream; treat it as a moving dependency and re-run the browser suite on PHP minor bumps rather than freezing it for a year.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums