amoifr/pickle-panther-bundle
Composer 安装命令:
composer require --dev 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.
README 文档
README
PicklePantherBundle
A YAML-driven end-to-end testing engine for Symfony, built on top of 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
| Piece | Role |
|---|---|
| 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
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 & 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:
- Chrome or Chromium — the actual browser that gets launched.
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 withPANTHER_EXTERNAL_BASE_URIinstead 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'sChromeManager), 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
Register Panther's web server and the report extension in phpunit.xml.dist:
<extensions> <bootstrap class="Symfony\Component\Panther\ServerExtension"/> <bootstrap class="Amoifr\PicklePantherBundle\Report\HtmlReportExtension"> <parameter name="output_dir" value="var/pickle-panther"/> </bootstrap> </extensions> <php> <env name="PANTHER_WEB_SERVER_DIR" value="./public"/> <env name="PICKLE_PANTHER_OUTPUT_DIR" value="./var/pickle-panther"/> <!-- Recommended in CI/Docker: --> <env name="PANTHER_NO_SANDBOX" value="1"/> </php>
output_dir(extension parameter /PICKLE_PANTHER_OUTPUT_DIR) should matchpickle_panther.report.output_dirso screenshots and the report land together.
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 underconfig/packages/test/(not the rootconfig/packages/): a root file is also loaded inprod, where the bundle is absent, and Symfony would fail with "no extension able to load pickle_panther".
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
A scenario file holds a scenarios list. Keys are bilingual:
| French | English |
|---|---|
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
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
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
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 is project-specific, so it is pluggable.
-
Form login out of the box: set
pickle_panther.auth(above). The bundle wires aFormLoginAuthenticatorand aliases it toAuthenticatorInterface. -
Custom flow: implement
AuthenticatorInterface(or extendFormLoginAuthenticator) 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
After the suite finishes, HtmlReportExtension writes a multi-page report next
to <output_dir>/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.
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 & driver).
Author
Pascal CESCON (@Amoifr) · pascal.cescon@gmail.com
Changelog
See CHANGELOG.md. This project follows Semantic Versioning.
License
Released under the MIT License.
统计信息
- 总下载量: 9
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 10
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-11

