PHPackages                             amoifr/pickle-panther-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. amoifr/pickle-panther-bundle

ActiveSymfony-bundle[Testing &amp; Quality](/categories/testing)

amoifr/pickle-panther-bundle
============================

YAML-driven E2E testing engine for Symfony on top of Panther: write browser scenarios in (FR/EN) near-natural language, mapped to PHP sentences, with an HTML report.

v0.4.0(1mo ago)112↓50%MITPHPPHP &gt;=8.2

Since Jun 11Pushed 1mo agoCompare

[ Source](https://github.com/Amoifr/pickle-panther-bundle)[ Packagist](https://packagist.org/packages/amoifr/pickle-panther-bundle)[ RSS](/packages/amoifr-pickle-panther-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (4)Dependencies (23)Versions (7)Used By (0)

 [![PicklePanther](assets/logo.png)](assets/logo.png)

PicklePantherBundle
===================

[](#picklepantherbundle)

A YAML-driven end-to-end testing engine for Symfony, built on top of [Symfony Panther](https://github.com/symfony/panther).

Write browser scenarios in near-natural language (French **or** English), mapped to PHP "sentences", and get a self-contained HTML report — without a line of Behat/Gherkin glue.

```
# tests/E2E/Scenario/homepage.yaml
scenarios:
  - nom: "La page d'accueil se charge"
    contexte:
      navigateur: desktop          # desktop | mobile
    etapes:
      - action: "Visite la page avec l'[/]"          # value written inline in the brackets
      - action: "Vérifie que le texte [text] est présent dans le sélecteur [selector]"
        args: { text: "Bienvenue", selector: "h1" }   # or listed explicitly under args
```

```
final class HomepageTest extends BasePantherTest
{
    public function testHomepage(): void
    {
        $this->createScenarioRunner()->runTest(__DIR__.'/Scenario/homepage.yaml');
    }
}
```

How it works
------------

[](#how-it-works)

PieceRole**Scenario YAML**Lists scenarios and their ordered steps (sentences + args).**`#[Sentence]` providers**Plain services whose methods are tagged with the sentence(s) they implement.**`SentenceRegistry`**Collects every provider and builds the sentence → method map.**`ScenarioRunner`**Parses a scenario file, applies its context, and runs each step.**`BasePantherTest`**The test case you extend; manages the browser and exposes `createScenarioRunner()`.**`AuthenticatorInterface`**Project-specific login, invoked when a scenario asks for an identity.**`HtmlReporter` + `HtmlReportExtension`**Accumulate step results and write `var/pickle-panther/report.html`.Installation
------------

[](#installation)

```
composer require --dev amoifr/pickle-panther-bundle
```

Register the bundle for the `test` environment (`config/bundles.php`):

```
return [
    // ...
    Amoifr\PicklePantherBundle\PicklePantherBundle::class => ['test' => true],
];
```

### Browser &amp; driver (required, on the machine that runs the tests)

[](#browser--driver-required-on-the-machine-that-runs-the-tests)

PicklePanther drives a **real, local browser** through Symfony Panther — there is no remote/Selenium mode. Two binaries must therefore be present **on the same machine/container where PHPUnit runs**:

1. **Chrome or Chromium** — the actual browser that gets launched.
2. **`chromedriver`** — the WebDriver server Panther talks to.

**Their major versions must match** (e.g. Chrome `149.x` ↔ chromedriver `149.x`). A mismatch makes the browser session fail to start (`session not created: This version of ChromeDriver only supports Chrome version N`).

Install a matching `chromedriver` the easy way — it lands in `./drivers/`, which Panther auto-detects (it searches `./drivers` and `./vendor/bin`):

```
composer require --dev dbrekelmans/bdi
vendor/bin/bdi detect drivers
```

Or point to an existing binary explicitly:

```
# .env.test (or .env.test.local)
PANTHER_CHROME_DRIVER_BINARY=drivers/chromedriver
```

> **Docker / where the browser must live.** Because the browser is launched *locally* by the test process, it must be reachable from wherever PHPUnit runs:
>
> - PHPUnit **inside a container** → install Chrome **and** chromedriver **in that container** (a browser on the host is not usable from inside the container).
> - PHPUnit **on the host** → install them on the host. To still exercise an app served elsewhere (e.g. a Dockerised app on `https://localhost:444`), point the tests at it with `PANTHER_EXTERNAL_BASE_URI` instead of letting Panther start its own web server.

> **HTTPS target with a self-signed certificate.** Pass the flag through `PANTHER_CHROME_ARGUMENTS` (read by Panther's `ChromeManager`), e.g. `PANTHER_CHROME_ARGUMENTS=--ignore-certificate-errors`. Chrome arguments set only via PHPUnit/bundle *capabilities* are **not** forwarded to the launched browser, so the certificate prompt would otherwise block the page.

### PHPUnit configuration

[](#phpunit-configuration)

Register Panther's web server and the report extension in `phpunit.xml.dist`:

```

```

> `output_dir` (extension parameter / `PICKLE_PANTHER_OUTPUT_DIR`) should match `pickle_panther.report.output_dir` so screenshots and the report land together.

Configuration
-------------

[](#configuration)

All keys are optional; defaults are shown.

```
# config/packages/pickle_panther.yaml  (test environment)
pickle_panther:
    locale: fr                         # default DSL language (fr|en) — matching is bilingual anyway
    debug: false                       # true => screenshot after every step (= E2E_DEBUG=1)
    scenarios_dir: '%kernel.project_dir%/tests/E2E/Scenario'
    report:
        enabled: true
        output_dir: '%kernel.project_dir%/var/pickle-panther'
    browser:
        headless: true
        chrome_args: []                # extra Chrome args appended to the defaults
        desktop: { width: 1920, height: 1080, user_agent: '...' }
        mobile:  { width: 375,  height: 812, pixel_ratio: 3, user_agent: '...' }

    # Optional: enable the built-in form-login authenticator (see below).
    auth:
        login_path: /login
        logout_path: /logout
        form_selector: 'form'
        email_field: '_username'
        password_field: '_password'
        roles:
            admin: { email: '%env(E2E_ADMIN_EMAIL)%', password: '%env(E2E_ADMIN_PASSWORD)%' }
            user:  { email: '%env(E2E_USER_EMAIL)%',  password: '%env(E2E_USER_PASSWORD)%' }
```

Keep credentials out of the repository — read them from environment variables.

> **Where to put the file.** If the bundle is only enabled in `dev`/`test`(`config/bundles.php`), put the config under `config/packages/test/` (not the root `config/packages/`): a root file is also loaded in `prod`, where the bundle is absent, and Symfony would fail with *"no extension able to load pickle\_panther"*.

### Real-world example

[](#real-world-example)

A complete `config/packages/test/pickle_panther.yaml`, including registering project-specific sentence providers and a French-named role map:

```
pickle_panther:
    locale: fr
    debug: false                       # true => a screenshot after every step
    report:
        enabled: true
        # Under public/ so the web server serves it (e.g. /tests/report.html).
        output_dir: '%kernel.project_dir%/public/tests'
    browser:
        headless: false                # headed (a display is available in CI/Docker)
        chrome_args:
            - '--disable-audio-output'
            - '--disable-accelerated-video-decode'
        desktop: { width: 1920, height: 1080 }
        mobile:  { width: 375, height: 812, pixel_ratio: 3.0 }
    auth:
        login_path: /fr/login
        logout_path: /fr/logout
        form_selector: 'form[name="login_form"]'
        email_field: 'login_form[email]'
        password_field: 'login_form[plainPassword]'
        roles:                         # keys are the scenario `identifié`/`identified` values
            utilisateur: { email: '%env(E2E_USER_EMAIL)%',  password: '%env(E2E_USER_PASSWORD)%' }
            admin:       { email: '%env(E2E_ADMIN_EMAIL)%', password: '%env(E2E_ADMIN_PASSWORD)%' }

# Project-specific sentence providers (test namespace) are auto-tagged.
services:
    App\Tests\E2E\Sentence\:
        resource: '%kernel.project_dir%/tests/E2E/Sentence/'
        autowire: true
        autoconfigure: true
        public: true
```

Writing scenarios
-----------------

[](#writing-scenarios)

A scenario file holds a `scenarios` list. Keys are bilingual:

FrenchEnglish`nom``name``description``description``contexte``context``navigateur` (`desktop`/`mobile`)`browser``identifié``identified``etapes``steps``action``action``titre``title``args``args`### Two ways to pass arguments

[](#two-ways-to-pass-arguments)

**1. Placeholder + `args` (explicit).** The action reuses the registered sentence verbatim and values are listed under `args`:

```
- action: "Click the element [selector] with JavaScript"
  args: { selector: "#go-page2" }
```

`args` are matched to the method parameters **by name** when the keys match the parameter names (the natural case, since placeholders mirror parameter names); otherwise they are passed **positionally** in declaration order.

**2. Inline values (concise).** Write the value directly inside the brackets and drop `args` — values are bound to the method parameters **positionally, in placeholder order**:

```
- action: "Click the element [#go-page2] with JavaScript"
```

The exact-placeholder form always wins the lookup, so the two styles coexist freely. Limitation: an inline value must not contain a closing bracket `]`(e.g. CSS attribute selectors like `[data-x="y"]`) — use the explicit `args:`form for those.

### Bundled sentences

[](#bundled-sentences)

`CommonSentences` (navigation, clicking, typing, waiting, assertions) and `AdminSentences` (generic back-office menus/datagrids) ship enabled. Browse them for the exact sentence strings — each method is annotated with its FR and EN `#[Sentence]`.

To list every available sentence (bundled **and** your own) without reading the code, run the console command — it introspects the registered providers:

```
# Markdown to stdout (optionally filtered by --locale and written with --output)
bin/console pickle-panther:sentences
bin/console pickle-panther:sentences --locale=en --output=docs/sentences.md
```

### Adding your own sentences

[](#adding-your-own-sentences)

Create a provider; autoconfiguration registers it automatically:

```
use Amoifr\PicklePantherBundle\Attribute\Sentence;
use Amoifr\PicklePantherBundle\Sentence\AbstractSentenceProvider;

final class CheckoutSentences extends AbstractSentenceProvider
{
    #[Sentence('Ajoute le produit [sku] au panier', 'fr')]
    #[Sentence('Add product [sku] to the cart', 'en')]
    public function addToCart(string $sku): void
    {
        $this->client()->clickLink(/* ... */);
        $this->testCase()->assertSelectorExists('.cart-item');
    }
}
```

`$this->client()` is the current Panther client; `$this->testCase()` exposes the PHPUnit assertions.

Authentication
--------------

[](#authentication)

Authentication is project-specific, so it is pluggable.

- **Form login out of the box:** set `pickle_panther.auth` (above). The bundle wires a `FormLoginAuthenticator` and aliases it to `AuthenticatorInterface`.
- **Custom flow:** implement `AuthenticatorInterface` (or extend `FormLoginAuthenticator`) and alias your service:

    ```
    services:
        App\Tests\E2E\MyAuthenticator: ~
        Amoifr\PicklePantherBundle\Auth\AuthenticatorInterface:
            alias: App\Tests\E2E\MyAuthenticator
    ```

A scenario then requests a logged-in context:

```
- nom: "Espace admin"
  contexte:
    identifié: admin        # passed to AuthenticatorInterface::authenticate('admin', $client)
  etapes:
    - action: "Visite la page avec l'[url]"
      args: { url: /admin }
```

The HTML report
---------------

[](#the-html-report)

After the suite finishes, `HtmlReportExtension` writes a multi-page report next to `/report.html`:

- **`report.html`** — a home page listing each scenario YAML file as a link, with a status icon and per-file scenario/step counts.
- **`report-N.html`** — one page per YAML file, containing all its scenarios and steps (status, timing, context badges, screenshots), with a breadcrumb back to the home page.

Screenshots resolve relatively (`captures/…`), so serving the output directory (or opening `report.html`) is enough. Set `pickle_panther.debug: true` (or run with `E2E_DEBUG=1`) to capture a screenshot on **every** step, not just failures.

Each `report-N.html` page also has a **slideshow popin** to review the captures as a slide deck: the image is centered, the step detail (scenario, title, action with highlighted arguments, OK/FAIL status) sits below with a step counter (e.g. `4 / 48`). Open it from the "Diaporama des captures" button or by clicking any capture thumbnail; navigate with the on-screen arrows or the keyboard (←/→), close with the × button, a backdrop click or `Esc`. It shines with `debug` enabled (a capture per step) but also works with failure-only captures.

 [![HTML report preview](assets/report-preview.png)](assets/report-preview.png)

Requirements
------------

[](#requirements)

- PHP `>= 8.2`
- Symfony `^7.1 || ^8.0`
- Chrome/Chromium **and** a version-matched `chromedriver`, both installed on the machine/container that runs the tests (see [Browser &amp; driver](#browser--driver-required-on-the-machine-that-runs-the-tests)).

Author
------

[](#author)

**Pascal CESCON** ([@Amoifr](https://github.com/Amoifr)) ·

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md). This project follows [Semantic Versioning](https://semver.org/).

License
-------

[](#license)

Released under the [MIT License](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance92

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

5

Last Release

39d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/277e4b07fc014cb5ef471444ba29b187832005ca7a8103d499218bbd4c89f3ed?d=identicon)[Amoifr](/maintainers/Amoifr)

---

Top Contributors

[![Amoifr](https://avatars.githubusercontent.com/u/31698966?v=4)](https://github.com/Amoifr "Amoifr (14 commits)")

---

Tags

testingsymfonyBDDyamle2ePanther

### Embed Badge

![Health badge](/badges/amoifr-pickle-panther-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/amoifr-pickle-panther-bundle/health.svg)](https://phpackages.com/packages/amoifr-pickle-panther-bundle)
```

###  Alternatives

[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M215](/packages/sulu-sulu)[chameleon-system/chameleon-base

The Chameleon System core.

1028.7k5](/packages/chameleon-system-chameleon-base)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1616.4k14](/packages/2lenet-crudit-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
