symfony / panthere
Requires
- php: >=8.0
- ext-dom: *
- ext-libxml: *
- php-webdriver/webdriver: ^1.8.2
- symfony/browser-kit: ^5.3 || ^6.0 || ^7.0
- symfony/dependency-injection: ^5.3 || ^6.0 || ^7.0
- symfony/deprecation-contracts: ^2.4 || ^3
- symfony/dom-crawler: ^5.3 || ^6.0 || ^7.0
- symfony/http-client: ^5.3 || ^6.0 || ^7.0
- symfony/http-kernel: ^5.3 || ^6.0 || ^7.0
- symfony/process: ^5.3 || ^6.0 || ^7.0
Requires (Dev)
- symfony/css-selector: ^5.3 || ^6.0 || ^7.0
- symfony/framework-bundle: ^5.3 || ^6.0 || ^7.0
- symfony/mime: ^5.3 || ^6.0 || ^7.0
- symfony/phpunit-bridge: ^5.3 || ^6.0 || ^7.0
README
PHP 和 Symfony 的浏览器测试和网页抓取库。
Panther 是一个方便的独立库,用于抓取网站和运行端到端测试 使用真实浏览器。
Panther 非常强大。它利用 W3C 的 WebDriver 协议 来驱动原生浏览器,如 Google Chrome 和 Firefox。
Panther 非常易于使用,因为它实现了 Symfony 流行的 BrowserKit 和 DomCrawler API,并包含了您测试应用程序所需的所有功能。如果您曾经为 Symfony 应用创建过功能测试,这听起来会很熟悉:因为 API 完全相同!请记住,Panther 可以在每一个 PHP 项目中使用,因为它是一个独立的库。
Panther 会自动找到您本地安装的 Chrome 或 Firefox 并启动它们,因此您不需要在您的计算机上安装任何其他东西,不需要 Selenium 服务器!
在测试模式下,Panther 会使用 PHP 内置的 web 服务器 自动启动您的应用程序。您可以专注于编写测试或网页抓取场景,而 Panther 将处理其他所有事情。
功能
与您通常使用的测试和网页抓取库不同,Panther
- 执行网页中包含的 JavaScript 代码
- 支持 Chrome(或 Firefox)实现的全部功能
- 允许截图
- 可以等待异步加载的元素出现
- 允许您在加载的页面的上下文中运行自己的 JS 代码或 XPath 查询
- 支持自定义 Selenium 服务器 安装
- 支持包括 SauceLabs 和 BrowserStack 在内的远程浏览器测试服务
文档
安装 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进行爬取。为了提供您习惯的所有测试工具,它扩展了PHPUnit的TestCase
。
如果您正在测试一个Symfony应用程序,PantherTestCase
会自动扩展WebTestCase
类。这意味着您可以轻松创建功能测试,可以直接执行应用程序的内核并访问所有现有服务。在这种情况下,您可以使用Symfony与Panther一起提供的所有爬取测试断言。
<?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的Client
和Crawler
实现。与Panther的本地客户端不同,这些替代客户端不支持JavaScript、CSS和屏幕截图捕获,但它们非常快速!
有两个替代客户端可用
- 第一个直接操作由
WebTestCase
提供的Symfony内核。这是最快速的客户端,但仅适用于Symfony应用程序。 - 第二个利用了Symfony的HttpBrowser。它在Symfony的内核和Panther的测试客户端之间起到中间作用。HttpBrowser使用Symfony的HttpClient组件发送真实的HTTP请求。它速度快,能够浏览任何网页,而不仅仅是应用程序的网页。然而,HttpBrowser不支持JavaScript和其他高级功能,因为它完全是用PHP编写的。这个客户端即使在非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提供了一个便捷的方式来测试具有实时功能的应用程序,这些应用程序使用Mercure、WebSocket和类似技术。
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,因此它已经有了一个广泛的文档。
- 有关
Client
类,请参阅BrowserKit文档。 - 有关
Crawler
类,请参阅DomCrawler文档。 - 有关WebDriver,请参阅PHP WebDriver文档。
环境变量
以下环境变量可以设置以更改一些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服务器使用的主机和/或端口,请将hostname
和port
传递给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集成
Panther与GitHub Actions开箱即用。以下是一个用于运行Panther测试的最小.github/workflows/panther.yml
文件
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开箱即用。以下是一个用于运行Panther测试的最小.travis.yml
文件
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运行Panther测试的最小.gitlab-ci.yml
文件
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开箱即用。以下是一个用于运行Panther测试的最小appveyor.yml
文件
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
与其他测试工具的使用
如果您想与其他测试工具(如LiipFunctionalTestBundle)一起使用Panther,或者如果您只需要使用不同的基础类,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
) - 在select中选择无效的选项
欢迎Pull Requests来填补剩余的空缺!
故障排除
与Bootstrap 5一起运行
如果您正在使用Bootstrap 5,则您可能存在测试问题。Bootstrap 5实现了一个滚动效果,这往往会误导Panther。
为了解决这个问题,我们建议您通过将Bootstrap 5的$enable-smooth-scroll变量设置为false来禁用此效果,在您的样式文件中。
$enable-smooth-scroll: false;
保存Panther
许多野生猫科动物物种都面临着高度威胁。如果您喜欢这个软件,请通过向Panthera组织捐款来帮助拯救(真正的)美洲豹。
致谢
由Kévin Dunglas创建。由Les-Tilleuls.coop赞助。
Panther建立在PHP WebDriver和其他一些FOSS库之上。它受到了Nightwatch.js的启发,这是一个基于WebDriver的JavaScript测试工具。