# Automated Functional E2E Testing with PHPUnit-Bridge and Panther
## RUN TESTS
All tests are in the tests/ directory. For subdirectories use same structure as for templates.
Run all tests:
docker-compose exec myproject-php php bin/phpunit
Run a single test:
docker-compose exec myproject-php php bin/phpunit --debug tests/main/IndexTest.php
Screenshots of errors are stored to `var/error-screenshots/` directory.
TODO: Run with Bootstrap 5
If you are using Bootstrap 5, then you may have a problem with testing. Bootstrap 5 implements a scrolling effect, which tends to mislead Panther. To fix this, we advise you to deactivate this effect by setting the Bootstrap 5 $enable-smooth-scroll variable to false in your style file.
$enable-smooth-scroll: false;
## WRITE TESTS
```php
$client->takeScreenshot('var/screenshots/screen.png');
```
## LINKS
https://symfony.com/doc/current/testing/end_to_end.html
Cool: https://symfony.com/doc/current/testing/end_to_end.html#using-browser-kit-clients
https://symfony.com/doc/current/testing/end_to_end.html#docker-integration
https://github.com/symfony/panther
https://github.com/dbrekelmans/browser-driver-installer
https://github.com/php-webdriver/php-webdriver
## TERMS AND CONCEPTS
Symfony Panther:
Panther is a browser testing and web scraping library built on top of WebDriver. It interacts with your application through a real browser (like Chrome) using the WebDriver protocol. Panther uses the internal Symfony Dev-Webserver by default.
WebDriver Protocol:
WebDriver is a W3C standard for browser automation. It provides a platform- and language-neutral wire protocol for controlling web browsers.
The WebDriver protocol is used by Panther to communicate with the browser.
For each browser a special WebDriver implementation is needed. For Chrome this is the ChromeDriver, for Firefox the GeckoDriver, etc.
The driver needs to match the browser version exactly. Panther uses the `dbrekelmans/bdi` package to install the correct driver.
Our driver for Firefox is stored in `drivers/geckodriver`.
Panther launches the driver in a separate process and communicates with it via the WebDriver protocol on Port 4444 to open a web page or execute browser actions. The first request is `POST /session` to request a new browser session
Panther running inside the myproject-php container makes HTTP requests to the myproject-apache container to interact with the Symfony application.
Example: Panther client requests http://myproject-apache:80 to load a web page.
Panther communicates with the Selenium server (running in the chrome container) via the WebDriver protocol.
The Selenium server listens on port 4444 for WebDriver commands from Panther.
Example: Panther sends a WebDriver command to http://myproject-chrome:4444 to open a web page or execute browser actions.
## INSTALLATION
### Packages
docker-compose exec myproject-php composer require --dev symfony/panther
docker-compose exec myproject-php composer require --dev symfony/phpunit-bridge
docker-compose exec myproject-php composer require --dev dbrekelmans/bdi
### Adjust Docker Environment
There are different environment options. The default way is using a local PHP installation with the builtin Symfony web server.
In this scenario you would have to have a local browser installation
Another scenario is dedicated docker container like selenium/standalone-chrome.
We currently use a setup where Firefox is installed in the PHP docker container. So we added to our PHP Dockerfile:
```dockerfile
# Install firefox + fonts for Panther E2E tests
RUN apk add --no-cache firefox ttf-freefont \
&& mkdir -p /var/www/app/var/log/firefox \
&& apk list --installed | grep firefox \
&& which firefox \
&& find / -name firefox \
&& firefox --version
# Create a wrapper script that runs 'sudo firefox' with all arguments passed
# This is used to fix panther E2E tests. We run the php docker container with the user from the host machine
# Panther seems to need root rights. So we define PANTHER_FIREFOX_BINARY="/usr/local/bin/run_firefox.sh" in .env.test
RUN echo -e '#!/bin/bash\nsudo firefox "$@"' > /usr/local/bin/run_firefox.sh \
&& chmod +x /usr/local/bin/run_firefox.sh
# Add user with sudo capability to run as same user as parent host + to allow panther to run firefox as root
# It looks like this has to bee the last command in the Dockerfile
RUN apk add --no-cache sudo \
&& addgroup -g 1000 usergroup \
&& adduser -D -u 1000 -G usergroup user \
&& echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
```
Note that there was a lot of troubleshooting effort to make this work with our setup of using the docker container with user and group of the host machine. But Firefox inside the PHP container needs root rights to work in headless mode. So the solution was to install sudo, create a wrapper script for Firefox to run it with sudo, and finally use this wrapper script as `PANTHER_FIREFOX_BINARY` in the `.env.test` file.
```dotenv
#.env
# Set same user and group in PHP Docker container as on host (=DEV) machine
# Override this default values in .env.local if your user/group id is different
# Find out with `id -u` and `id -g` in your terminal
USERID=1000
GROUPID=1000
```
```dotenv
# .env.test
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
# This environment variable is used by Symfony to control how deprecation notices are handled. The setting 999999 is unusual; typically, this variable is set to weak to ignore deprecations unless they come from your own code, or max[self]=0 to completely ignore them. Setting it to a large number like 999999 might not have a defined behavior unless it’s a specific requirement or a placeholder for a custom handling mechanism.
SYMFONY_DEPRECATIONS_HELPER=weak
#APP_ENV=test # test = default
#APP_DEBUG=1 # This does not turn off the web debug toolbar in Panther screenshots...
# Panther E2E tests
# @see doc/testing.md
# https://symfony.com/doc/current/testing/end_to_end.html#configuring-panther-through-environment-variables
PANTHER_APP_ENV=panther
PANTHER_EXTERNAL_BASE_URI=http://myproject-apache:80
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
#PANTHER_DEVTOOLS=1 # This en/disables the browser devtools. Default = 1
PANTHER_FIREFOX_BINARY="/usr/local/bin/run_firefox.sh" # @see doc/testing.md
#PANTHER_FIREFOX_ARGUMENTS='--headless --window-size=1200,1600 -setDefaultPref gfx.webrender.all=false -setDefaultPref layer.acceleration'
```
```
# docker-compose.yml
myproject-php:
build: .docker/php
tty: true
working_dir: /var/www/app/
#user: "1000:1000"
user: "${USERID}:${GROUPID}"
volumes:
- .:/var/www/app
# For panther E2E test troubleshooting
# These can't be set in .env, as they are not used inside a symfony context
environment:
MOZ_HEADLESS: "1"
MOZ_LOG: "timestamp,nsHttp:3,nsSocketTransport:3,nsHostResolver:3"
MOZ_LOG_FILE: "/var/www/app/var/log/firefox/firefox.log"
```
docker compose down
docker compose build --no-cache --progress=plain myproject-php
docker compose up -d
Now that firefox is available in the PHP container, we can install the geckodriver with the browser-driver-installer package:
docker-compose exec myproject-php php vendor/bin/bdi detect drivers
## DEBUGGING
It took over 10 hours to sort this out. So here is some debugging documentation...
#### Enable Geckodriver debugging:
Maybe it would also be possible to replace drivers/geckodriver with a shell script that starts geckodriver with the desired options. This way you could add the --log trace option to the command line.
```php
//FirefoxManager::__construct():
// $this->process = new Process([$geckodriverBinary ?: $this->findGeckodriverBinary(), '--port='.$this->options['port']], null, null, null, null);
$command = $geckodriverBinary ?: $this->findGeckodriverBinary();
$shellCommand = sprintf(
'sh -c "%s --log trace > logfile.log 2>&1"',
$command
);
$this->process = new Process(['sh', '-c', $shellCommand]);
```
#### Reduce the timeout
TODO: how to configure this with env / Panther ?
```php
//FirefoxManager::start():
$this->options['connection_timeout_in_ms'] = 5000;
$this->options['request_timeout_in_ms'] = 5000;
```
#### Debug Curl
```php
//HttpCommandExecutor::execute():
...
```
### Other debugging information
Check env settings in container:
docker-compose exec myproject-php env
Connection check:
docker-compose exec myproject-php nc -zv myproject-apache 80
myproject-apache (172.27.0.6:80) open
docker-compose exec myproject-php curl http://myproject-apache:80
Note: the following need a long sleep() statement in FirefoxManager::start()
docker-compose exec myproject-php nc -zv localhost 4444
localhost (127.0.0.1:4444) open -> OK!
Manually run geckodriver to debug:
docker-compose exec myproject-php geckodriver --log debug
1716531385980 geckodriver INFO Listening on 127.0.0.1:4444
docker-compose exec myproject-php php bin/phpunit tests/main/IndexTest.php
RuntimeException: The port 4444 is already in use.
## Stuff that did not work / we did not use
.docker file chrome
chromium \
chromium-chromedriver \
nss \
busybox-extras \ # Add this line to install telnet
docker compose build myproject-php
docker compose exec myproject-chrome google-chrome --version
Google Chrome 125.0.6422.76
mkdir /var/www/app/driver
CHROMEDRIVER_VERSION=$(curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE) \
&& curl -sS -o /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip \
&& unzip /tmp/chromedriver.zip -d driver/ \
&& rm /tmp/chromedriver.zip \
&& chmod +x driver/chromedriver
https://github.com/symfony/panther/issues/505#issuecomment-938582439
add selenium container:
# For panther E2E tests
myproject-chrome:
image: selenium/standalone-chrome:latest
ports:
- 8076:4444
volumes:
- .:/srv/app
php docker file:
firefox-esr \
dbus \
ttf-freefont \
xvfb \
xvfb, dbus and exported DISPLAY are not actually needed to run Firefox in headless mode - just run firefox --headless and it's all.
# Install GeckoDriver
RUN apk add --no-cache firefox-esr \
&& wget -q https://github.com/mozilla/geckodriver/releases/latest/download/geckodriver-linux64.tar.gz \
&& tar -xvzf geckodriver-linux64.tar.gz \
&& chmod +x geckodriver \
&& mv geckodriver /usr/local/bin/ \
&& rm geckodriver-linux64.tar.gz
# Install Geckodriver
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN apk add --no-cache --virtual .build-deps wget \
&& GECKODRIVER_VERSION=$(wget -qO- https://api.github.com/repos/mozilla/geckodriver/releases/latest \
| grep "tag_name" | sed -E 's/.*"([^"]+)".*/\1/') \
&& wget -qO /tmp/geckodriver.tar.gz \
"https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz" \
&& tar -xzf /tmp/geckodriver.tar.gz -C /usr/local/bin/ \
&& rm /tmp/geckodriver.tar.gz \
&& apk del .build-deps