lullabot/mink-selenium2-driver
A Selenium 2 (WebDriver) browser driver for Mink, letting PHP acceptance tests control real browsers via WebDriver. Maintained by Lullabot for Drupal and Behat testing, it runs JavaScript-driven scenarios and integrates with Mink sessions and selectors.
This is a drop-in replacement for behat/mink-selenium2-driver, not a new API. Lullabot forked the abandoned upstream to keep it working on PHP 8.1+ (Drupal core is the main consumer). The driver class keeps the original FQCN, so your code and config never mention Lullabot anywhere.
composer remove --dev behat/mink-selenium2-driver # never install both
composer require --dev lullabot/mink-selenium2-driver
# typical trio for a Behat suite
composer require --dev behat/mink behat/mink-extension behat/behat
Confirm the swap really happened:
composer show lullabot/mink-selenium2-driver # check the `replace` clause lists behat/mink-selenium2-driver
First use case — decide if you need it at all. A plain Laravel app should use Dusk; it talks to ChromeDriver via php-webdriver directly and integrates with the framework. Reach for this package only when:
.feature files, or a suite ported from Drupal), or.feature spec shared across a Laravel backend and a Drupal/legacy frontend.Where to look first: vendor/lullabot/mink-selenium2-driver/src/Selenium2Driver.php (methods getWebDriver(), getScreenshot(), setTimeouts(), attachFile()) and vendor/behat/mink-extension/behat.yml.dist for the config schema.
Minimal working setup:
# behat.yml
default:
suites:
web:
contexts: [App\Tests\Behat\FeatureContext]
extensions:
Behat\MinkExtension:
base_url: http://127.0.0.1:8000
default_session: selenium2
javascript_session: selenium2
selenium2:
wd_host: http://127.0.0.1:4444
browser: chrome
capabilities:
browserName: chrome
"goog:chromeOptions":
args: ["--headless=new", "--no-sandbox", "--disable-dev-shm-usage"]
docker run -d -p 4444:4444 -p 7900:7900 selenium/standalone-chrome:latest
php artisan serve --port=8000 &
vendor/bin/behat --tags=@javascript
Session without Behat. Useful inside a Pest/PHPUnit test or an Artisan command when you want Mink's portable element API:
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Session;
$driver = new Selenium2Driver('chrome', [
'goog:chromeOptions' => ['args' => ['--headless=new', '--no-sandbox']],
], 'http://127.0.0.1:4444');
$session = new Session($driver);
$session->start();
$session->visit('http://localhost:8000/login');
$session->getPage()->fillField('email', '[email protected]');
$session->getPage()->pressButton('Sign in');
Tag-driven JS. Keep default_session: selenium2 off for speed and attach @javascript only to features that need a real browser; MinkExtension switches to the Selenium session automatically.
Waiting. Never sleep(). Prefer Mink's built-ins, which poll:
$session->wait(5000, "document.querySelector('[data-testid=toast]') !== null");
$button = $session->getPage()->waitFor(10, fn ($page) => $page->findButton('Save'));
Escape hatch to the raw WebDriver — this is the 20% of cases Mink doesn't model (alerts, logs, multi-tab, CDP):
$wd = $session->getDriver()->getWebDriver(); // php-webdriver RemoteWebDriver
$wd->switchTo()->alert()->accept();
$wd->switchTo()->window($wd->getWindowHandles()[1]);
$entries = $wd->manage()->getLog('browser'); // needs goog:loggingPrefs capability
Screenshots on failure — the highest-value hook you'll write, since this driver has no built-in artifact handling:
use Behat\Behat\Hook\Scope\AfterStepScope;
/** @AfterStep */
public function screenshotOnFailure(AfterStepScope $scope): void
{
if ($scope->getTestResult()->isPassed()) {
return;
}
$dir = storage_path('app/behat');
$name = preg_replace('/[^a-z0-9]+/i', '-', $scope->getStep()->getText());
file_put_contents("$dir/{$name}.png", $this->getSession()->getDriver()->getScreenshot());
}
getScreenshot() returns decoded PNG bytes, not base64 — write it straight to disk and attach it in CI.
Page objects over selectors. Wrap Mink's NodeElement in small classes with data-testid lookups ($page->find('css', '[data-testid=cart-total]')). CSS through find('css', …) is far more stable against Blade/Vue re-renders than XPath, and XPath is where 80% of flaky locators in ported Drupal suites come from.
DB state. Point the suite at a dedicated connection — --env=testing with sqlite-in-memory works only if your Behat process and php artisan serve share it, which they won't. Use a real MySQL/Postgres test DB and RefreshDatabase-equivalent truncation between scenarios.
Extension point. Subclass Selenium2Driver to override getWebDriver() (e.g. to add default capabilities or retry connection) and register your class in your own DI instead of relying on MinkExtension.
wd_host and the /wd/hub suffix. Selenium 4 dropped the JSON Wire endpoint; http://localhost:4444/wd/hub may 404. Use http://selenium:4444 (no path) with a current php-webdriver. This is the #1 cause of "cannot connect to Selenium".localhost:4444 inside your PHP container is not the Selenium container. Use the Compose service name, and remember the PHP process is the client — Selenium does not need to reach your app, but the browser does, so base_url must be resolvable from inside the Selenium container (often http://nginx or host.docker.internal, not 127.0.0.1).attachFile() resolves paths on the browser host, not the PHP host. In Docker, bind-mount your fixtures into the Selenium container or uploads silently fail with a confusing "file not found".chromeOptions/chrome (old) vs goog:chromeOptions (W3C). Selenium 4 + php-webdriver ≥1.12 expects the prefixed form; mixing them produces a browser that launches but ignores --headless.NodeElement handles. After an AJAX action, re-query from $session->getPage() — don't cache elements in properties.Behat\Mink\Driver\Selenium2Driver, leaving the upstream package installed (directly or transitively) gives you duplicate-class or autoload-ordering surprises. composer why behat/mink-selenium2-driver after install; if anything still pulls it, add an explicit replace in your root composer.json.--no-sandbox --disable-dev-shm-usage --window-size=1440,900. Without the last one, responsive breakpoints silently flip your layout and locators vanish.selenium/standalone-chrome and watch via VNC (http://localhost:7900, password secret), or use selenium/standalone-chrome-debug for an interactive session. On failure, $wd->getCurrentURL(), $wd->getPageSource(), and getLog('browser') usually identify the issue faster than re-running..env config, no Artisan command, no RefreshDatabase, no exception rendering. Budget for a FeatureContext base class that handles bootstrapping, DB resets, and artifact capture — that glue is the real cost of choosing Mink over Dusk.composer.json, keep your driver usage confined to one class in your test suite, and watch upstream — if behat/mink-selenium2-driver ever resumes releases, swapping back should be a one-line change. Do not build bespoke features on top of this driver's internals.How can I help you explore Laravel packages today?