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

Mink Panther Driver Laravel Package

robertfausk/mink-panther-driver

Integrates Symfony Panther with Mink, adding a real-browser driver for Behat and other Mink-based acceptance tests. It uses Panther’s WebDriver/Chrome support to run JavaScript-heavy scenarios with fast, reliable browser automation.

Deep Wiki
Context7

Getting Started

robertfausk/mink-panther-driver is a thin adapter: it implements Mink’s DriverInterface on top of a Symfony\Component\Panther\Client, so Behat/Mink scenarios drive a real Chrome/Firefox instead of Goutte or Selenium Grid. It is not a test framework — Mink still owns sessions, selectors, and assertions. For a Laravel app this is a black-box E2E driver pointed at php artisan serve, Sail, or Valet.

Prerequisites: PHP 8.1+, Composer, Chrome/Chromium. Panther talks WebDriver and can launch chromedriver itself.

composer require --dev robertfausk/mink-panther-driver \
    behat/behat behat/mink-extension behat/mink-goutte-driver

Look first at:

  • vendor/robertfausk/mink-panther-driver/README.md — Behat wiring and options are version-specific.
  • vendor/robertfausk/mink-panther-driver/src/Driver/PantherDriver.php — the source of truth for which Mink methods are implemented and which throw UnsupportedDriverActionException.
  • composer show robertfausk/mink-panther-driver — confirm the installed version and its Panther/Symfony constraints.

Minimal smoke test:

<?php
require __DIR__.'/vendor/autoload.php';

use Behat\Mink\Session;
use RobertFausk\MinkPantherDriver\Driver\PantherDriver;
use Symfony\Component\Panther\Client;

$client = Client::createChromeClient(
    null,
    ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage']
);

$session = new Session(new PantherDriver($client));
$session->start();

$session->visit('http://127.0.0.1:8000/');
echo $session->getCurrentUrl(), PHP_EOL;
echo $session->getPage()->find('css', 'h1')?->getText(), PHP_EOL;

$session->stop();

First real use case — keep Goutte as the fast default session and route only @javascript scenarios to Panther:

# behat.yml
default:
  extensions:
    Behat\MinkExtension:
      base_url: http://127.0.0.1:8000
      default_session: goutte
      javascript_session: panther
      sessions:
        goutte:
          goutte: ~
        panther:
          panther: ~
    # Enable the package's Behat extension here (see its README) so the
    # `panther` driver factory above is registered with MinkExtension.

Then run php artisan serve --host=127.0.0.1 --port=8000 and vendor/bin/behat --format=pretty. The base URL host must match exactly what the browser sees, or Laravel session cookies will not stick.

Implementation Patterns

Two-session Behat setup. Use Goutte/BrowserKit for content and redirect assertions, Panther only for scenarios tagged @javascript. This keeps suite time sane; a Chrome boot costs seconds per session.

@javascript
Scenario: Customer can log in
  Given I am on "/login"
  When I fill in "Email" with "[email protected]"
  And I fill in "Password" with "secret"
  And I press "Log in"
  Then I should see "Dashboard"

Registering the driver yourself. If the installed version has no Behat extension, add a DriverFactory and Behat extension in your test namespace:

namespace App\Behat;

use Behat\MinkExtension\ServiceContainer\Driver\DriverFactory;
use RobertFausk\MinkPantherDriver\Driver\PantherDriver;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Panther\Client;

final class PantherFactory implements DriverFactory
{
    public function getDriverName(): string { return 'panther'; }
    public function supportsJavascript(): bool { return true; }

    public function buildDriver(array $config): PantherDriver
    {
        return new PantherDriver(Client::createChromeClient(
            $config['chromedriver'] ?? null,
            $config['arguments'] ?? ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage']
        ));
    }

    public function configure(ArrayNodeDefinition $builder): void
    {
        $builder->children()
            ->scalarNode('chromedriver')->defaultNull()->end()
            ->variableNode('arguments')->defaultValue([])->end()
        ->end();
    }
}
namespace App\Behat;

use Behat\MinkExtension\ServiceContainer\MinkExtension;
use Behat\Testwork\ServiceContainer\Extension;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class PantherMinkExtension implements Extension
{
    public function getConfigKey(): string { return 'panther_mink'; }

    public function initialize(ExtensionManager $extensionManager): void
    {
        $mink = $extensionManager->getExtension('mink');
        if ($mink instanceof MinkExtension) {
            $mink->registerDriver('panther', new PantherFactory());
        }
    }

    public function configure(ArrayNodeDefinition $builder): void {}
    public function load(ContainerBuilder $container, array $config): void {}
    public function process(ContainerBuilder $container): void {}
}

Add App\Behat\PantherMinkExtension: ~ under extensions in behat.yml.

Waiting is a first-class pattern. Mink does not auto-wait; write one step and reuse it everywhere:

/** @When I wait for :selector to be visible */
public function waitForSelector(string $selector): void
{
    $session = $this->getSession();
    $driver = $session->getDriver();

    if ($driver instanceof PantherDriver) {
        $driver->getClient()->waitForVisibility($selector, 10_000);
        return;
    }

    $session->wait(10_000, sprintf(
        "(function(){var e=document.querySelector(%s);return e && e.offsetParent !== null;})()",
        json_encode($selector)
    ));
}

Escape to Panther for things Mink lacks. The driver should expose the underlying client (e.g. getClient()). Use it for Panther’s waitFor* family, screenshots, and Chrome DevTools scripts; prefer Mink’s executeScript()/evaluateScript() for simple JS when implemented.

$driver = $this->getSession()->getDriver();
if ($driver instanceof PantherDriver) {
    $client = $driver->getClient();
    $client->waitFor('.data-table tbody tr');
    $client->executeScript('window.scrollTo(0, document.body.scrollHeight)');
}

Failure diagnostics hook. Capture a screenshot and browser console on the first failing step:

use Behat\Behat\Hook\Scope\AfterStepScope;

/** @AfterStep */
public function screenshotOnFailure(AfterStepScope $scope): void
{
    if ($scope->getTestResult()->isPassed()) {
        return;
    }

    $driver = $this->getSession()->getDriver();
    if (!$driver instanceof PantherDriver) {
        return;
    }

    @mkdir('build/behat', 0777, true);
    $driver->getClient()->takeScreenshot(sprintf(
        'build/behat/%s-%s.png',
        date('His'),
        md5((string) $scope->getStep()->getText())
    ));

    foreach ($driver->getClient()->getWebDriver()->manage()->getLog('browser') as $entry) {
        fwrite(STDERR, sprintf("[browser] %s\n", json_encode($entry)));
    }
}

Laravel database and server lifecycle. Behat and php artisan serve are separate PHP processes, so an in-memory SQLite database will not be shared. Use a file-backed SQLite database or a dedicated MySQL schema, and refresh it before the suite:

/** @BeforeSuite */
public static function prepareApp(): void
{
    exec('touch database/testing.sqlite');
    exec('php artisan migrate:fresh --seed --env=testing --force 2>&1', $output, $status);
    if ($status !== 0) {
        throw new RuntimeException(implode("\n", $output));
    }
}

In CI, start the server as a background job and let Behat connect:

php artisan migrate:fresh --seed --env=testing
php artisan serve --host=127.0.0.1 --port=8000 --env=testing &
vendor/bin/behat --format=pretty --out=std

Keep selectors in page objects. Mink’s NodeElement and DocumentElement make small context helper classes cheap. Centralizing CSS/XPath also isolates you from driver quirks.

Subclass the driver as an extension point. Since the driver is thin, subclassing PantherDriver is often easier than forking: override click() to add a short wait, or implement a missing Mink method against getClient()/getWebDriver().

Gotchas and Tips

  • No implicit wait is the daily pain. Assertions run immediately. Every dynamic UI interaction needs an explicit DOM condition; document.readyState === 'complete' is not enough for Inertia/Vue/React. Wait on a visible element or app-specific marker.
  • Real browsers do not expose HTTP response headers cleanly. Expect getStatusCode()/getResponseHeaders() to be unsupported or unreliable. Assert status codes, redirects, and download responses with a Goutte/BrowserKit session instead.
  • Element staleness after navigation. Do not hold a NodeElement across a click that navigates. Re-query from $session->getPage() after every page change.
  • Reset sessions between scenarios. Mink sessions and Chrome cookies/localStorage can survive across scenarios. Verify MinkExtension resets the Panther session; otherwise add a @BeforeScenario hook that calls $session->reset() or creates a fresh client. Cookie bleed is a common source of flaky auth tests.
  • @javascript must map to a JavaScript session. If javascript_session is not configured, Mink silently uses the default Goutte session and JS assertions fail in confusing ways.
  • Host consistency matters in Laravel. Use the same host (127.0.0.1 vs localhost) in base_url, the browser, and the server. Laravel’s encrypted session cookie is domain-bound; mixing hosts logs the user out mid-scenario.
  • Separate-process DB isolation. :memory: SQLite cannot be shared with artisan serve. Use database/testing.sqlite or a test MySQL database and migrate/seed before the suite.
  • Docker/CI Chrome flags. Always pass --no-sandbox --disable-dev-shm-usage in containers. Pin Chrome and chromedriver versions where possible; auto-download depends on network and the Panther version.
  • Driver API gaps. Check PantherDriver source for attachFile, setBasicAuth, resizeWindow, getResponseHeaders, cookie methods, etc. File uploads are particularly brittle with hidden/styled inputs — WebDriver can send keys to the underlying input[type=file], but Mink’s attachFileToField may not find it.
  • Dialogs block the driver. alert/confirm must be accepted or dismissed via $client->getWebDriver()->switchTo()->alert(); Mink has no high-level API for this.
  • Downloads need Chrome prefs. Mink has no download abstraction. If you must test downloads, configure a download directory through Panther’s Chrome options and poll the filesystem.
  • Low bus factor. Twelve stars and an unknown repository URL means limited community support and possible lag behind Mink/Panther/Symfony releases. Pin an exact version, wrap driver construction in your own factory, and read the source before upgrading. The 2025-04-03 release is current but small.
  • Symfony components in Laravel. Panther pulls Symfony DomCrawler/BrowserKit/HttpClient. Laravel already depends on Symfony components; check composer why-not if a conflict appears, and keep this package in require-dev.
  • Debugging stack. Use PANTHER_NO_HEADLESS=1 locally to watch the browser, PANTHER_CHROME_DRIVER_BINARY to pin chromedriver, and screenshots plus manage()->getLog('browser') for console errors. On CI failures, preserve build/behat artifacts.
  • Do not run every scenario in Panther. Chrome per scenario is slow and memory-hungry, and parallelism multiplies that. Keep a fast Goutte default and reserve Panther for JavaScript, file upload, and browser-API scenarios.
  • Verdict. This driver is worthwhile only if your team already standardizes on Behat/Mink and wants Panther’s self-managed browser instead of Selenium Grid. For a Laravel-only stack, Laravel Dusk or direct Panther + PHPUnit is fewer moving parts; for Gherkin-centric teams, this package is a reasonable, thin bridge.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core