Two years after the first release of the library, I'm very happy to announce the immediate availability of Panther version 1.0!

Logo of the Symfony Panther project

Panther is a browser testing and web scraping library that relies on the WebDriver W3C specification to manipulate real web browsers. Chrome and Firefox are natively supported while Safari, Edge and Opera can be used with some additional configuration. Cloud testing providers such as Sauce Labs and Browserstack are also supported.

As you know, the Symfony team is working hard to provide a modern and straightforward integration of JavaScript directly in the framework through the UX initiative.

In this context, having a good browser testing library with JavaScript support is a must. Because Panther implements the BrowserKit API, it's straightforward to use it with the rest of the Symfony ecosystem. For instance, existing functional tests can be executed using a real browser through Panther with (almost) no code change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Tests\Controller;

-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+use Symfony\Component\Panther\PantherTestCase;

-class PostControllerTest extends WebTestCase
+class PostControllerTest extends PantherTestCase
 {
     public function testShowPost()
     {
-        $client = static::createClient();
+        $client = static::createPantherClient();
 
         $client->request('GET', '/post/hello-world');
         $this->assertSelectorTextContains('html h1.title', 'Hello World');
    }
 }

This also means that you can test your JavaScript-enhanced Twig templates directly with PHPUnit (and other testing tools)!

Under the hood, Panther automatically starts a web server exposing your application and the browser driver (Selenium Server isn't needed), then uses the PHP WebDriver library (that I have been contributing to maintain and improve to execute the scenario.

During the past two years, we have had 69 people working on stabilizing the library, improving its developer experience and making it faster!

In the last few days, we also dramatically enhanced the testing API, integration with the other components of the framework including Flex and MakerBundle, as well the debugging experience! Let's discover these changes.

Better Onboarding

To improve the initial experience with Panther, we added a Flex recipe generating the needed configuration.

Run the following command to install Panther and its PHPUnit integration in any Symfony project:

1
$ composer req --dev panther phpunit

As indicated by the post-installation message, you also need ChromeDriver or GeckoDriver depending on if you want to run your tests with Google Chrome (by default) or Mozilla Firefox. You can also execute your whole test suite using both browsers to ensure that your project works properly with the two most popular browsers out there.

To install these drivers, use the package manager of your operating system or execute Daniël Brekelmans' Browser Driver Installer, which is now supported out of the box by Panther:

1
2
$ composer require --dev dbrekelmans/bdi
$ vendor/bin/bdi detect drivers

Even better: if you use a compatible Docker definition, you have nothing to do, Flex automatically updates the Dockerfile to install Chromedriver in the image. But we'll come back to this new Docker integration in an upcoming blog post!

Finally, open phpunit.xml.dist and uncomment the snippet added by Flex to register the PHPUnit extension:

1
2
3
<extensions>
    <extension class="Symfony\Component\Panther\ServerExtension" />
</extensions>

Using MakerBundle to Generate Test Classes

We also improved the integration of Panther with MakerBundle! It now contains a dedicated command to generate Panther test skeletons. Use it to create your first Panther test:

1
$ bin/console make:test

When asked for the type of test to generate, choose PantherTestCase.

A test file similar to this one will be generated:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// tests/MyPantherTest.php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class MyPantherTest extends PantherTestCase
{
    public function testSomething(): void
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/');

        $this->assertSelectorTextContains('h1', 'Hello World');
    }
}

Run it with:

1
$ php bin/phpunit

Improving The Debugging Experience

Of course this test doesn't pass: we haven't created any pages in our app yet. But take a look at the contents of the var/error-screenshots/ directory! To help you to debug your tests, Panther now automatically takes a screenshot of the browser window when a test fails:

Error screenshot

There's more! Sometimes it can be convenient to inspect the console logs, to use a JavaScript debugger or to play with the application to understand what the problem is. Run the following command:

1
$ PANTHER_NO_HEADLESS=1 bin/phpunit --debug
Symfony Panther debugging in Chrome

Thanks to the PANTHER_NO_HEADLESS environment variable, Panther opens a real browser window, so you can see what happens. And because the --debug option is enabled (added by Dany Maillard), if the test fails Panther will keep the window open until you hit "enter" in the terminal. This gives you the opportunity to see the application in the exact state it was when the error occurred, and to use the dev tools of the browser to find the issue.

Improved Async Testing

In addition to the assertions provided by PHPUnit and by Symfony, Panther includes numerous assertions dedicated to testing JavaScript and CSS powered websites. Using these assertions, it's easy to execute scenarios such as "this element must appear after a click on this button", "this text must be hidden before this user action occurs", "this button must be disabled while the form isn't valid", etc.

In Panther 1.0, we added new assertions allowing you to easily assert that something will happen in the future.

Let's assume that Symfony UX is installed. If that's not the case, just type composer require ux. Don't forget to follow instructions displayed by the Flex recipe to finish the installation and start the Webpack Encore development server. Find more details on the stimulus-bridge documentation.

Then create a Stimulus controller:

1
2
3
4
5
6
7
8
9
// assets/controllers/hello_controller.js
export default class extends Controller {
    connect() {
        setTimeout(
            () => this.element.textContent = 'This will replace the content of the div after 1 second!',
            1000
        );
    }
}

And here is the corresponding Twig template for some page accessible via /hello:

1
2
{# templates/hello/hello.html.twig #}
<div data-controller="hello">Initial content</div>

The text "Initial content" will be displayed for 1 second, then replaced by the Stimulus controller. We use setTimeout() for the sake of simplicity, but in real-world applications your controller will probably fetch() data from an API Platform endpoint or wait for data coming from a Mercure hub.

How can we test that the text is really replaced? By using Panther of course!

Until Panther 1.0, here is the test you would write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class HelloControllerTest extends PantherTestCase
{
    public function testSomething(): void
    {
        $client = static::createPantherClient();
        $client->request('GET', '/hello');

        $this->assertSelectorTextContains('div[data-controller="hello"]', 'Initial content');

        // Wait for the text to change
        $client->waitForElementToContain('div[data-controller="hello"]', 'This will replace the content of the div after 1 second!');
        // Then assert
        $this->assertSelectorTextContains('div[data-controller="hello"]', 'This will replace the content of the div after 1 second!');
    }
}

As you can see, having to use the client to wait for the result of the asynchronous operation before asserting is repetitive and cumbersome.

In Panther 1.0, Grégory Copin added a bunch of new assertions that dramatically improve the developer experience. Here is how it looks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class HelloControllerTest extends PantherTestCase
{
    public function testSomething(): void
    {
        static::createPantherClient()->request('GET', '/hello');

        $this->assertSelectorTextContains('div[data-controller="hello"]', 'Initial content');
        $this->assertSelectorWillContain('div[data-controller="hello"]', 'This will replace the content of the div after 1 second!');
    }
}

Much better, isn't it?

A Growing Community

The community is also starting to use Panther as a foundation for other awesome libraries. I want to mention some of them, which I find particularly interesting:

  • Arachnid Web Crawler is a library that crawls your site using Panther to extract SEO-related information;
  • zenstruck/browser is a nice library built on top of BrowserKit and Panther providing an expressive and fluent interface to write your integration and end-to-end tests;
  • Blackfire PHP SDK allows adding performance assertions to your tests using the famous Blackfire profiler, and now has native support for Panther;
  • BehatPantherExtension is an extension adding support for Panther to the Behat testing framework.

Kudos to the authors of these libraries!

Save the Real Panthers and Help The Project

While we enjoy using Panther to test our webapps, big cats in the wild are on the verge of extinction. If you or your company use Panther (the library, which is entirely free) and make some money with it, please donate to Panthera, an NGO acting to try to save the real wild cats.

To keep the good work going, also consider sponsoring me and the Symfony project!

By the way, do you know why the cat in the Panther logo isn't black? No, like the f in Symfony, it's not an error! Panther's original name was Panthère (the French word). And in French, panthère means (more or less) leopard! Before moving the project to the Symfony organization, we decided to remove the accent and the final e for convenience (it was a heated debate!) but the logo stayed.

Actually, that's not the whole story! Leopard can be of several colors. So why a dull yellow with black spot marks? This is because of a local peculiarity of the Lille area (a French city where several Symfony developers live, including Fabien). To learn the whole story, you'll have to come to a conference and ask!

Published in #Symfony