symfony/panther

PHP 和 Symfony 的浏览器测试和网页抓取库。

v2.1.1 2023-12-03 22:17 UTC

README

PHP 和 Symfony 的浏览器测试和网页抓取库

CI

Panther 是一个方便的独立库,用于抓取网站并使用真实浏览器运行端到端测试。

Panther 功能强大。它利用 W3C 的 WebDriver 协议 来驱动原生浏览器,如 Google Chrome 和 Firefox。

Panther 非常易于使用,因为它实现了 Symfony 流行的 BrowserKitDomCrawler API,并包含您测试应用程序所需的所有功能。如果您曾经为 Symfony 应用创建过 功能测试,那么它将听起来非常熟悉:因为 API 完全相同!请注意,Panther 可以用于任何 PHP 项目,因为它是一个独立的库。

Panther 自动找到您本地的 Chrome 或 Firefox 安装并启动它们,因此您不需要在计算机上安装任何其他东西,不需要 Selenium 服务器!

在测试模式下,Panther 会自动使用 PHP 内置的 web 服务器 启动您的应用程序。您可以专注于编写测试或网页抓取场景,Panther 将处理其他所有事情。

功能

与您习惯的测试和网页抓取库不同,Panther

  • 执行网页中包含的 JavaScript 代码
  • 支持 Chrome(或 Firefox)实现的全部内容
  • 允许截图
  • 可以等待异步加载的元素出现
  • 让您在加载页面的上下文中运行自己的 JS 代码或 XPath 查询
  • 支持自定义 Selenium 服务器 安装
  • 支持包括 SauceLabsBrowserStack 在内的远程浏览器测试服务

文档

安装 Panther

使用 Composer 在项目中安装 Panther。如果您只想将 Panther 用于测试,而不是在生产环境中进行网页抓取,则可能希望使用 --dev 标志

composer req symfony/panther

composer req --dev symfony/panther

安装 ChromeDriver 和 geckodriver

Panther 使用 WebDriver 协议来控制用于抓取网站的浏览器。

在所有系统上,您都可以使用 dbrekelmans/browser-driver-installer 在本地安装 ChromeDriver 和 geckodriver

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

Panther 将自动检测并使用存储在 drivers/ 目录中的驱动程序。

或者,您可以使用操作系统的包管理器来安装它们。

在 Ubuntu 上运行

apt-get install chromium-chromedriver firefox-geckodriver

在 Mac 上使用 Homebrew

brew install chromedriver geckodriver

在 Windows 上使用 chocolatey

choco install chromedriver selenium-gecko-driver

最后,您可以手动下载 ChromeDriver(适用于Chromium或Chrome)和 GeckoDriver(适用于Firefox),并将它们放在您的 PATH 或项目的 drivers/ 目录中的任何位置。

注册PHPUnit扩展

如果您打算使用Panther测试您的应用程序,我们强烈建议注册Panther PHPUnit扩展。虽然不是强制性的,但这个扩展通过提高性能并允许使用交互式调试模式来显著提高测试体验。

当使用扩展并结合 PANTHER_ERROR_SCREENSHOT_DIR 环境变量时,使用Panther客户端失败或出错的测试(在客户端创建之后)将自动截图以帮助调试。

要注册Panther扩展,请将以下行添加到 phpunit.xml.dist

<!-- phpunit.xml.dist -->
<extensions>
    <extension class="Symfony\Component\Panther\ServerExtension" />
</extensions>

没有扩展,Panther用于运行测试应用程序的Web服务器在需要时启动,并在调用 tearDownAfterClass() 时停止。另一方面,当注册扩展时,Web服务器仅在使用完所有测试后才会停止。

基本用法

<?php

use Symfony\Component\Panther\Client;

require __DIR__.'/vendor/autoload.php'; // Composer's autoloader

$client = Client::createChromeClient();
// Or, if you care about the open web and prefer to use Firefox
$client = Client::createFirefoxClient();

$client->request('GET', 'https://api-platform.com'); // Yes, this website is 100% written in JavaScript
$client->clickLink('Getting started');

// Wait for an element to be present in the DOM (even if hidden)
$crawler = $client->waitFor('#installing-the-framework');
// Alternatively, wait for an element to be visible
$crawler = $client->waitForVisibility('#installing-the-framework');

echo $crawler->filter('#installing-the-framework')->text();
$client->takeScreenshot('screen.png'); // Yeah, screenshot!

测试用法

PantherTestCase 类允许您轻松编写端到端测试。它将自动使用内置PHP Web服务器启动您的应用程序,并让您使用Panther爬取它。为了提供您习惯的所有测试工具,它扩展了PHPUnitTestCase

如果您正在测试一个Symfony应用程序,PantherTestCase 将自动扩展 WebTestCase。这意味着您可以轻松创建功能测试,这些测试可以直接执行应用程序的内核并访问所有现有服务。在这种情况下,您可以使用Panther与Symfony提供的所有爬虫测试断言

<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class E2eTest extends PantherTestCase
{
    public function testMyApp(): void
    {
        $client = static::createPantherClient(); // Your app is automatically started using the built-in web server
        $client->request('GET', '/mypage');

        // Use any PHPUnit assertion, including the ones provided by Symfony
        $this->assertPageTitleContains('My Title');
        $this->assertSelectorTextContains('#main', 'My body');
        
        // Or the one provided by Panther
        $this->assertSelectorIsEnabled('.search');
        $this->assertSelectorIsDisabled('[type="submit"]');
        $this->assertSelectorIsVisible('.errors');
        $this->assertSelectorIsNotVisible('.loading');
        $this->assertSelectorAttributeContains('.price', 'data-old-price', '42');
        $this->assertSelectorAttributeNotContains('.price', 'data-old-price', '36');

        // Use waitForX methods to wait until some asynchronous process finish
        $client->waitFor('.popin'); // wait for element to be attached to the DOM
        $client->waitForStaleness('.popin'); // wait for element to be removed from the DOM
        $client->waitForVisibility('.loader'); // wait for element of the DOM to become visible
        $client->waitForInvisibility('.loader'); // wait for element of the DOM to become hidden
        $client->waitForElementToContain('.total', '25 €'); // wait for text to be inserted in the element content
        $client->waitForElementToNotContain('.promotion', '5%'); // wait for text to be removed from the element content
        $client->waitForEnabled('[type="submit"]'); // wait for the button to become enabled 
        $client->waitForDisabled('[type="submit"]'); // wait for  the button to become disabled 
        $client->waitForAttributeToContain('.price', 'data-old-price', '25 €'); // wait for the attribute to contain content
        $client->waitForAttributeToNotContain('.price', 'data-old-price', '25 €'); // wait for the attribute to not contain content
        
        // Let's predict the future
        $this->assertSelectorWillExist('.popin'); // element will be attached to the DOM
        $this->assertSelectorWillNotExist('.popin'); // element will be removed from the DOM
        $this->assertSelectorWillBeVisible('.loader'); // element will be visible
        $this->assertSelectorWillNotBeVisible('.loader'); // element will not be visible
        $this->assertSelectorWillContain('.total', '€25'); // text will be inserted in the element content
        $this->assertSelectorWillNotContain('.promotion', '5%'); // text will be removed from the element content
        $this->assertSelectorWillBeEnabled('[type="submit"]'); // button will be enabled 
        $this->assertSelectorWillBeDisabled('[type="submit"]'); // button will be disabled 
        $this->assertSelectorAttributeWillContain('.price', 'data-old-price', '€25'); // attribute will contain content
        $this->assertSelectorAttributeWillNotContain('.price', 'data-old-price', '€25'); // attribute will not contain content
    }
}

要运行此测试

bin/phpunit tests/E2eTest.php

一种多态的猫科动物

Panther还为您提供了即时访问其他基于BrowserKit的ClientCrawler实现。与Panther的本地客户端不同,这些替代客户端不支持JavaScript、CSS和截图捕获,但它们是超级快速的!

有两个替代客户端可用

  • 第一个直接操作由WebTestCase提供的Symfony内核。这是最快速的客户端,但仅适用于Symfony应用程序。
  • 第二个利用了Symfony的HttpBrowser。它是Symfony内核和Panther测试客户端之间的中间体。HttpBrowser使用Symfony的HttpClient组件发送真实的HTTP请求。它速度快,可以浏览任何网页,而不仅仅是测试应用程序的网页。然而,由于它完全用PHP编写,HttpBrowser不支持JavaScript和其他高级功能。这个客户端甚至适用于非Symfony应用程序!

有趣的是,这3个客户端实现了完全相同的API,因此您只需调用适当的工厂方法即可从一种切换到另一种,这为每个测试案例都提供了一个很好的权衡(我需要JavaScript吗?我需要通过外部SSO服务器进行身份验证吗?我想访问当前请求的内核吗?……等等)。

以下是获取这些客户端实例的方法

<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Panther\Client;

class E2eTest extends PantherTestCase
{
    public function testMyApp(): void
    {
        $symfonyClient = static::createClient(); // A cute kitty: Symfony's functional test tool
        $httpBrowserClient = static::createHttpBrowserClient(); // An agile lynx: HttpBrowser
        $pantherClient = static::createPantherClient(); // A majestic Panther
        $firefoxClient = static::createPantherClient(['browser' => static::FIREFOX]); // A splendid Firefox
        // Both HttpBrowser and Panther benefits from the built-in HTTP server

        $customChromeClient = Client::createChromeClient(null, null, [], 'https://example.com'); // Create a custom Chrome client
        $customFirefoxClient = Client::createFirefoxClient(null, null, [], 'https://example.com'); // Create a custom Firefox client
        $customSeleniumClient = Client::createSeleniumClient('http://127.0.0.1:4444/wd/hub', null, 'https://example.com'); // Create a custom Selenium client
        // When initializing a custom client, the integrated web server IS NOT started automatically.
        // Use PantherTestCase::startWebServer() or WebServerManager if you want to start it manually.

        // enjoy the same API for the 3 felines
        // $*client->request('GET', '...')

        $kernel = static::createKernel(); // If you are testing a Symfony app, you also have access to the kernel

        // ...
    }
}

使用Mercure或WebSocket创建隔离的浏览器以测试应用程序

Panther 提供了一种方便的方法来测试具有实时能力并使用 MercureWebSocket 以及类似技术的应用程序。

PantherTestCase::createAdditionalPantherClient() 创建额外的、隔离的浏览器,它们可以相互交互。例如,这可以用来测试一个同时有多个用户连接的聊天应用程序。

<?php

use Symfony\Component\Panther\PantherTestCase;

class ChatTest extends PantherTestCase
{
    public function testChat(): void
    {
        $client1 = self::createPantherClient();
        $client1->request('GET', '/chat'); 
 
        // Connect a 2nd user using an isolated browser and say hi!
        $client2 = self::createAdditionalPantherClient();
        $client2->request('GET', '/chat');
        $client2->submitForm('Post message', ['message' => 'Hi folks 👋😻']);

        // Wait for the message to be received by the first client
        $client1->waitFor('.message');

        // Symfony Assertions are always executed in the **primary** browser
        $this->assertSelectorTextContains('.message', 'Hi folks 👋😻');
    }
}

访问浏览器控制台日志

如有需要,您可以使用 Panther 访问控制台的内容

<?php

use Symfony\Component\Panther\PantherTestCase;

class ConsoleTest extends PantherTestCase
{
    public function testConsole(): void
    {
        $client = self::createPantherClient(
            [],
            [],
            [
                'capabilities' => [
                    'goog:loggingPrefs' => [
                        'browser' => 'ALL', // calls to console.* methods
                        'performance' => 'ALL', // performance data
                    ],
                ],
            ]
        );

        $client->request('GET', '/');
        $consoleLogs = $client->getWebDriver()->manage()->getLog('browser'); // console logs 
        $performanceLogs = $client->getWebDriver()->manage()->getLog('performance'); // performance logs
    }
}

传递参数给 ChromeDriver

如有需要,您可以配置传递给 chromedriver 二进制文件的参数

<?php

use Symfony\Component\Panther\PantherTestCase;

class MyTest extends PantherTestCase
{
    public function testLogging(): void
    {
        $client = self::createPantherClient(
            [],
            [],
            [
                'chromedriver_arguments' => [
                    '--log-path=myfile.log',
                    '--log-level=DEBUG'
                ],
            ]
        );

        $client->request('GET', '/');
    }
}

检查 WebDriver 连接的状态

使用 Client::ping() 方法检查 WebDriver 连接是否仍然活跃(对长时间运行的任务很有用)。

附加文档

由于 Panther 实现了流行库的 API,它已经有了一份详尽的文档。

环境变量

以下环境变量可以设置以更改 Panther 的某些行为

  • PANTHER_NO_HEADLESS:禁用浏览器的无头模式(将显示测试窗口,对调试很有用)
  • PANTHER_WEB_SERVER_DIR:更改项目的文档根目录(默认为 ./public/,相对路径 必须./ 开头)
  • PANTHER_WEB_SERVER_PORT:更改 web 服务器的端口(默认为 9080
  • PANTHER_WEB_SERVER_ROUTER:使用在每次 HTTP 请求开始时运行的 web 服务器路由脚本
  • PANTHER_EXTERNAL_BASE_URI:使用外部 web 服务器(不会启动 PHP 内置的 web 服务器)
  • PANTHER_APP_ENV:覆盖传递给运行 PHP 应用程序的 web 服务器的 APP_ENV 变量
  • PANTHER_ERROR_SCREENSHOT_DIR:为您的失败/错误截图设置基本目录(例如 ./var/error-screenshots
  • PANTHER_DEVTOOLS:切换浏览器的开发者工具(默认 启用,对调试很有用)
  • PANTHER_ERROR_SCREENSHOT_ATTACH:将上述截图添加到测试输出的.junit 附件格式中

更改内置 Web 服务器的主机名和端口

如果您想更改内置 web 服务器使用的主机名和/或端口,请将 hostnameport 传递给 createPantherClient() 方法的 $options 参数

// ...

$client = self::createPantherClient([
    'hostname' => 'example.com', // Defaults to 127.0.0.1
    'port' => 8080, // Defaults to 9080
]);

Chrome 特定的环境变量

  • PANTHER_NO_SANDBOX:禁用 Chrome 的沙箱(不安全,但允许在容器中使用 Panther)
  • PANTHER_CHROME_ARGUMENTS:自定义 Chrome 参数。您需要设置 PANTHER_NO_HEADLESS 以完全自定义。
  • PANTHER_CHROME_BINARY:使用另一个 google-chrome 二进制文件

Firefox 特定的环境变量

  • PANTHER_FIREFOX_ARGUMENTS:自定义 Firefox 参数。您需要设置 PANTHER_NO_HEADLESS 以完全自定义。
  • PANTHER_FIREFOX_BINARY:使用另一个 firefox 二进制文件

访问隐藏文本

根据规范,WebDriver 实现默认情况下只返回 显示 的文本。当您对 head 标签(如 title)进行筛选时,text() 方法返回空字符串。使用 html() 方法获取标签的完整内容,包括标签本身。

交互模式

Panther可以在测试套件失败后进行暂停。这是一个通过网页浏览器调查问题而真正受欢迎的休息时间。要启用此模式,您需要使用--debug PHPUnit选项,但不能使用无头模式。

$ PANTHER_NO_HEADLESS=1 bin/phpunit --debug

Test 'App\AdminTest::testLogin' started
Error: something is wrong.

Press enter to continue...

要使用交互模式,PHPUnit扩展 必须 已注册。

使用外部Web服务器

有时,重新使用现有的Web服务器配置而不是启动内置的PHP服务器会更方便。为此,设置external_base_uri选项。

<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class E2eTest extends PantherTestCase
{
    public function testMyApp(): void
    {
        $pantherClient = static::createPantherClient(['external_base_uri' => 'https://localhost']);
        // the PHP integrated web server will not be started
    }
}

拥有多域名应用程序

您的PHP/Symfony应用程序可能需要为多个不同的域名提供服务。

由于Panther在测试之间在内存中保存客户端以提高性能,因此如果您使用Panther为不同域名编写多个测试,您将必须在单独的进程中运行测试。

为此,您可以使用原生的@runInSeparateProcess PHPUnit注解。

注意:使用external_base_uri选项并在后台启动自己的Web服务器非常方便,因为Panther不需要在每个测试中都启动和停止您的服务器。可以使用Symfony CLI快速且轻松地完成此操作。

以下是一个使用external_base_uri选项确定客户端使用的域名的示例

<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class FirstDomainTest extends PantherTestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testMyApp(): void
    {
        $pantherClient = static::createPantherClient([
            'external_base_uri' => 'http://mydomain.localhost:8000',
        ]);
        
        // Your tests
    }
}
<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

class SecondDomainTest extends PantherTestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testMyApp(): void
    {
        $pantherClient = static::createPantherClient([
            'external_base_uri' => 'http://anotherdomain.localhost:8000',
        ]);
        
        // Your tests
    }
}

使用代理

要使用代理服务器,设置以下环境变量:PANTHER_CHROME_ARGUMENTS='--proxy-server=socks://127.0.0.1:9050'

接受自签名SSL证书

要强制Chrome接受无效和自签名的证书,设置以下环境变量:PANTHER_CHROME_ARGUMENTS='--ignore-certificate-errors' 此选项不安全,仅在生产环境中使用它进行测试,决不在生产中使用(例如,用于网络爬虫)。

对于Firefox,实例化客户端如下

$client = Client::createFirefoxClient(null, null, ['capabilities' => ['acceptInsecureCerts' => true]]);

Docker集成

这是一个可以同时运行Chrome和Firefox的Panther的最小Docker镜像。

FROM php:alpine

# Chromium and ChromeDriver
ENV PANTHER_NO_SANDBOX 1
# Not mandatory, but recommended
ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage'
RUN apk add --no-cache chromium chromium-chromedriver

# Firefox and GeckoDriver (optional)
ARG GECKODRIVER_VERSION=0.28.0
RUN apk add --no-cache firefox libzip-dev; \
    docker-php-ext-install zip
RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz; \
    tar -zxf geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/bin; \
    rm geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz

使用docker build . -t myproject构建它。使用docker run -it -v "$PWD":/srv/myproject -w /srv/myproject myproject bin/phpunit运行它。

GitHub Actions集成

GitHub Actions配合使用,Panther开箱即用。以下是一个最小的.github/workflows/panther.yml文件,用于运行Panther测试。

name: Run Panther tests

on: [ push, pull_request ]

jobs:
  tests:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Install dependencies
        run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

      - name: Run test suite
        run: bin/phpunit

Travis CI集成

如果您添加Chrome插件,Panther将与Travis CI无缝配合。以下是一个最小的.travis.yml文件,用于运行Panther测试。

language: php
addons:
  # If you don't use Chrome, or Firefox, remove the corresponding line
  chrome: stable
  firefox: latest

php:
  - 8.0

script:
  - bin/phpunit

Gitlab CI集成

以下是一个最小的.gitlab-ci.yml文件,用于使用Gitlab CI运行Panther测试。

image: ubuntu

before_script:
  - apt-get update
  - apt-get install software-properties-common -y
  - ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
  - apt-get install curl wget php php-cli php7.4 php7.4-common php7.4-curl php7.4-intl php7.4-xml php7.4-opcache php7.4-mbstring php7.4-zip libfontconfig1 fontconfig libxrender-dev libfreetype6 libxrender1 zlib1g-dev xvfb chromium-chromedriver firefox-geckodriver -y -qq
  - export PANTHER_NO_SANDBOX=1
  - export PANTHER_WEB_SERVER_PORT=9080
  - php -r "copy('https://getcomposer.org.cn/installer', 'composer-setup.php');"
  - php composer-setup.php --install-dir=/usr/local/bin --filename=composer
  - php -r "unlink('composer-setup.php');"
  - composer install

test:
  script:
    - bin/phpunit

AppVeyor集成

只要安装了Google Chrome,Panther就可以与AppVeyor无缝配合。以下是一个最小的appveyor.yml文件,用于运行Panther测试。

build: false
platform: x86
clone_folder: c:\projects\myproject

cache:
  - '%LOCALAPPDATA%\Composer\files'

install:
  - ps: Set-Service wuauserv -StartupType Manual
  - cinst -y php composer googlechrome chromedriver firfox selenium-gecko-driver
  - refreshenv
  - cd c:\tools\php80
  - copy php.ini-production php.ini /Y
  - echo date.timezone="UTC" >> php.ini
  - echo extension_dir=ext >> php.ini
  - echo extension=php_openssl.dll >> php.ini
  - echo extension=php_mbstring.dll >> php.ini
  - echo extension=php_curl.dll >> php.ini
  - echo memory_limit=3G >> php.ini
  - cd %APPVEYOR_BUILD_FOLDER%
  - composer install --no-interaction --no-progress

test_script:
  - cd %APPVEYOR_BUILD_FOLDER%
  - php bin\phpunit

与其他测试工具的使用

如果您想使用Panther与像LiipFunctionalTestBundle这样的其他测试工具,或者如果您只需要使用不同的基类,Panther可以满足您的需求。它为您提供了Symfony\Component\Panther\PantherTestCaseTrait,您可以使用它来使用一些Panther的奇妙功能来增强现有的测试基础设施。

<?php

namespace App\Tests\Controller;

use Liip\FunctionalTestBundle\Test\WebTestCase;
use Symfony\Component\Panther\PantherTestCaseTrait;

class DefaultControllerTest extends WebTestCase
{
    use PantherTestCaseTrait; // this is the magic. Panther is now available.

    public function testWithFixtures(): void
    {
        $this->loadFixtures([]); // load your fixtures
        $client = self::createPantherClient(); // create your panther client

        $client->request('GET', '/');
    }
}

限制

以下功能目前不受支持

  • 爬取XML文档(仅支持HTML)
  • 更新现有文档(浏览器主要用于消费数据,而不是创建网页)
  • 使用多维PHP数组语法设置表单值
  • 返回\DOMElement实例的方法(因为此库内部使用WebDriverElement
  • 在下拉列表中选择无效选项

欢迎提交Pull Requests以填补剩余的差距!

故障排除

与Bootstrap 5一起运行

如果您正在使用 Bootstrap 5,那么您可能会遇到测试问题。Bootstrap 5 实现了一个滚动效果,这往往会误导 Panther。

为了解决这个问题,我们建议您在您的样式文件中将 Bootstrap 5 的 $enable-smooth-scroll 变量设置为 false 来禁用此效果。

$enable-smooth-scroll: false;

拯救 Panthers

许多野生猫科动物物种都面临着高度威胁。如果您喜欢这款软件,请通过向 Panthera 组织捐赠 来帮助拯救(真正的)美洲豹。

致谢

Kévin Dunglas 创建。由 Les-Tilleuls.coop 赞助。

Panther 是基于 PHP WebDriver其他几个 FOSS 库 构建的。它受到了 Nightwatch.js 的启发,这是一个基于 WebDriver 的 JavaScript 测试工具。