diff --git a/.gitignore b/.gitignore index 8c6c60e5..bd8b4f45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.php_cs.cache /.php_cs +/.phpunit.result.cache /composer.phar /composer.lock /phpunit.xml diff --git a/README.md b/README.md index f1d1935b..5fcb6700 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/symfony/panther.svg?branch=master)](https://travis-ci.org/symfony/panther) [![Build status](https://ci.appveyor.com/api/projects/status/bunoc4ufud4oie45?svg=true)](https://ci.appveyor.com/project/fabpot/panther) -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/9ea7e78c-998a-4489-9815-7449ce8291ef/mini.png)](https://insight.sensiolabs.com/projects/9ea7e78c-998a-4489-9815-7449ce8291ef) +[![SymfonyInsight](https://insight.symfony.com/projects/9ea7e78c-998a-4489-9815-7449ce8291ef/mini.png)](https://insight.symfony.com/projects/9ea7e78c-998a-4489-9815-7449ce8291ef) *Panther* is a convenient standalone library to scrape websites and to run end-to-end tests **using real browsers**. diff --git a/src/DomCrawler/Crawler.php b/src/DomCrawler/Crawler.php index e1fd840e..e3360257 100644 --- a/src/DomCrawler/Crawler.php +++ b/src/DomCrawler/Crawler.php @@ -181,8 +181,12 @@ public function nodeName(): string return $this->getElementOrThrow()->getTagName(); } - public function text($default = null): string + public function text(string $default = null, bool $normalizeWhitespace = true): string { + if (!$normalizeWhitespace) { + throw new \InvalidArgumentException('Panther only supports getting normalized text.'); + } + try { return $this->getElementOrThrow()->getText(); } catch (\InvalidArgumentException $e) { @@ -190,7 +194,7 @@ public function text($default = null): string throw $e; } - return (string) $default; + return $default; } } diff --git a/src/PantherTestCase.php b/src/PantherTestCase.php index da422412..ff8c0af3 100644 --- a/src/PantherTestCase.php +++ b/src/PantherTestCase.php @@ -14,98 +14,46 @@ namespace Symfony\Component\Panther; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\ForwardCompatTestTrait; -use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -use Symfony\Component\Panther\Client as PantherClient; if (\class_exists(WebTestCase::class)) { - if (trait_exists('Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait') && trait_exists('Symfony\Bundle\FrameworkBundle\Test\ForwardCompatTestTrait')) { - abstract class PantherTestCase extends WebTestCase - { - use ForwardCompatTestTrait; - use PantherTestCaseTrait; - use WebTestAssertionsTrait { - assertPageTitleSame as private baseAssertPageTitleSame; - assertPageTitleContains as private baseAssertPageTitleContains; - } - - public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void + if (trait_exists('Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait')) { + if (trait_exists('Symfony\Bundle\FrameworkBundle\Test\ForwardCompatTestTrait')) { + // Symfony 4.3 + abstract class PantherTestCase extends WebTestCase { - $client = self::getClient(); - if ($client instanceof PantherClient) { - self::assertSame($expectedTitle, $client->getTitle()); + use ForwardCompatTestTrait; + use WebTestAssertionsTrait; - return; + private function doTearDown() + { + parent::tearDown(); + self::getClient(null); } - - self::baseAssertPageTitleSame($expectedTitle, $message); } - - public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void + } else { + // Symfony 5 + abstract class PantherTestCase extends WebTestCase { - $client = self::getClient(); - if ($client instanceof PantherClient) { - if (method_exists(self::class, 'assertStringContainsString')) { - self::assertStringContainsString($expectedTitle, $client->getTitle()); - - return; - } - - self::assertContains($expectedTitle, $client->getTitle()); + use WebTestAssertionsTrait; - return; + protected function tearDown(): void + { + parent::tearDown(); + self::getClient(null); } - - self::baseAssertPageTitleContains($expectedTitle, $message); - } - - private function doTearDown() - { - parent::tearDown(); - self::getClient(null); - } - - // Copied from WebTestCase to allow assertions to work with createClient - - /** - * Creates a KernelBrowser. - * - * @param array $options An array of options to pass to the createKernel method - * @param array $server An array of server parameters - * - * @return KernelBrowser A KernelBrowser instance - */ - protected static function createClient(array $options = [], array $server = []) - { - $kernel = static::bootKernel($options); - - try { - /** - * @var KernelBrowser - */ - $client = $kernel->getContainer()->get('test.client'); - } catch (ServiceNotFoundException $e) { - if (class_exists(KernelBrowser::class)) { - throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.'); - } - throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit"'); - } - - $client->setServerParameters($server); - - return self::getClient($client); } } } else { + // Symfony 4.3 and inferior abstract class PantherTestCase extends WebTestCase { use PantherTestCaseTrait; } } } else { + // Without Symfony abstract class PantherTestCase extends TestCase { use PantherTestCaseTrait; diff --git a/src/ServerListener.php b/src/ServerListener.php index 9db2c522..55fee48d 100644 --- a/src/ServerListener.php +++ b/src/ServerListener.php @@ -19,6 +19,11 @@ use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestSuite; +@trigger_error(sprintf('The "%s" class is deprecated since Panther 0.5.3, use "%s" instead.', ServerListener::class, ServerExtension::class), E_USER_DEPRECATED); + +/** + * @deprecated since Panther 0.5.3, use Symfony\Component\Panther\ServerExtension instead. + */ final class ServerListener implements TestListener { use TestListenerDefaultImplementation; diff --git a/src/WebTestAssertionsTrait.php b/src/WebTestAssertionsTrait.php new file mode 100644 index 00000000..a0c695e5 --- /dev/null +++ b/src/WebTestAssertionsTrait.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Symfony\Component\Panther; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait as BaseWebTestAssertionsTrait; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\Panther\Client as PantherClient; + +/** + * Tweaks Symfony's WebTestAssertionsTrait to be compatible with Panther. + * + * @author Kévin Dunglas + */ +trait WebTestAssertionsTrait +{ + use PantherTestCaseTrait; + use BaseWebTestAssertionsTrait { + assertPageTitleSame as private baseAssertPageTitleSame; + assertPageTitleContains as private baseAssertPageTitleContains; + } + + public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void + { + $client = self::getClient(); + if ($client instanceof PantherClient) { + self::assertSame($expectedTitle, $client->getTitle()); + + return; + } + + self::baseAssertPageTitleSame($expectedTitle, $message); + } + + public static function assertPageTitleContains(string $expectedTitle, string $message = ''): void + { + $client = self::getClient(); + if ($client instanceof PantherClient) { + if (method_exists(self::class, 'assertStringContainsString')) { + self::assertStringContainsString($expectedTitle, $client->getTitle()); + + return; + } + + self::assertContains($expectedTitle, $client->getTitle()); + + return; + } + + self::baseAssertPageTitleContains($expectedTitle, $message); + } + + // Copied from WebTestCase to allow assertions to work with createClient + + /** + * Creates a KernelBrowser. + * + * @param array $options An array of options to pass to the createKernel method + * @param array $server An array of server parameters + * + * @return KernelBrowser A KernelBrowser instance + */ + protected static function createClient(array $options = [], array $server = []) + { + $kernel = static::bootKernel($options); + + try { + /** + * @var KernelBrowser + */ + $client = $kernel->getContainer()->get('test.client'); + } catch (ServiceNotFoundException $e) { + if (class_exists(KernelBrowser::class)) { + throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.'); + } + throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit"'); + } + + $client->setServerParameters($server); + + return self::getClient($client); + } +} diff --git a/tests/DomCrawler/CrawlerTest.php b/tests/DomCrawler/CrawlerTest.php index 8bac114a..5e63ad71 100644 --- a/tests/DomCrawler/CrawlerTest.php +++ b/tests/DomCrawler/CrawlerTest.php @@ -16,6 +16,7 @@ use Facebook\WebDriver\WebDriverElement; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Panther\Client; +use Symfony\Component\Panther\Client as PantherClient; use Symfony\Component\Panther\DomCrawler\Image; use Symfony\Component\Panther\DomCrawler\Link; use Symfony\Component\Panther\Tests\TestCase; @@ -47,7 +48,7 @@ public function testGetUri(callable $clientFactory): void public function testHtml(callable $clientFactory): void { $crawler = $this->request($clientFactory, '/basic.html'); - $this->assertContains('A basic page', $crawler->html()); + $this->assertStringContainsString('A basic page', $crawler->html()); } /** @@ -314,4 +315,24 @@ public function testHtmlDefault(callable $clientFactory): void $crawler = $this->request($clientFactory, '/basic.html'); $this->assertSame('default', $crawler->filter('header')->html('default')); } + + /** + * @dataProvider clientFactoryProvider + */ + public function testNormalizeText(callable $clientFactory, string $clientClass): void + { + if (PantherClient::class !== $clientClass) { + $this->markTestSkipped('Need https://github.com/symfony/symfony/pull/34151'); + } + + $crawler = $this->request($clientFactory, '/normalize.html'); + $this->assertSame('Foo Bar Baz', $crawler->filter('#normalize')->text()); + } + + public function testDoNotNormalizeText() + { + $this->expectException(\InvalidArgumentException::class); + + self::createPantherClient()->request('GET', self::$baseUri.'/normalize.html')->filter('#normalize')->text(null, false); + } } diff --git a/tests/DomCrawler/Field/FileFormFieldTest.php b/tests/DomCrawler/Field/FileFormFieldTest.php index aa39b7a0..fba61698 100644 --- a/tests/DomCrawler/Field/FileFormFieldTest.php +++ b/tests/DomCrawler/Field/FileFormFieldTest.php @@ -23,6 +23,17 @@ class FileFormFieldTest extends TestCase { private static $invalidUploadFileName = 'narf.txt'; + private function assertValueContains($needle, $haystack): void + { + if (\is_string($haystack)) { + $this->assertStringContainsString($needle, $haystack); + + return; + } + + $this->assertContains($needle, $haystack); + } + /** * @dataProvider clientFactoryProvider */ @@ -36,7 +47,7 @@ public function testFileUploadWithUpload(callable $clientFactory) $this->assertInstanceOf(FileFormField::class, $fileFormField); $fileFormField->upload($this->getUploadFilePath(self::$uploadFileName)); - $this->assertContains(self::$uploadFileName, $form['file_upload']->getValue()); + $this->assertValueContains(self::$uploadFileName, $form['file_upload']->getValue()); } /** @@ -52,13 +63,15 @@ public function testFileUploadWithSetValue(callable $clientFactory) $this->assertInstanceOf(FileFormField::class, $fileFormField); $fileFormField->setValue($this->getUploadFilePath(self::$uploadFileName)); - $this->assertContains(self::$uploadFileName, $form['file_upload']->getValue()); + $this->assertValueContains(self::$uploadFileName, $form['file_upload']->getValue()); } /** * @dataProvider clientFactoryProvider + * + * @param mixed $class */ - public function testFileUploadWithSetFilePath(callable $clientFactory) + public function testFileUploadWithSetFilePath(callable $clientFactory, $class) { $crawler = $this->request($clientFactory, '/file-form-field.html'); $form = $crawler->filter('form')->form(); @@ -68,10 +81,10 @@ public function testFileUploadWithSetFilePath(callable $clientFactory) $this->assertInstanceOf(FileFormField::class, $fileFormField); $fileFormField->setFilePath($this->getUploadFilePath(self::$uploadFileName)); - $this->assertContains(self::$uploadFileName, $form['file_upload']->getValue()); + $this->assertValueContains(self::$uploadFileName, $form['file_upload']->getValue()); $fileFormField->setFilePath($this->getUploadFilePath(self::$anotherUploadFileName)); - $this->assertContains(self::$anotherUploadFileName, $form['file_upload']->getValue()); + $this->assertValueContains(self::$anotherUploadFileName, $form['file_upload']->getValue()); } /** diff --git a/tests/ProcessManager/ChromeManagerTest.php b/tests/ProcessManager/ChromeManagerTest.php index d5bd8767..5333f135 100644 --- a/tests/ProcessManager/ChromeManagerTest.php +++ b/tests/ProcessManager/ChromeManagerTest.php @@ -29,12 +29,11 @@ public function testRun() $manager->quit(); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage The port 9515 is already in use. - */ public function testAlreadyRunning() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The port 9515 is already in use.'); + $driver1 = new ChromeManager(); $driver1->start(); diff --git a/tests/ProcessManager/WebServerManagerTest.php b/tests/ProcessManager/WebServerManagerTest.php index d23ecaca..1efc2a91 100644 --- a/tests/ProcessManager/WebServerManagerTest.php +++ b/tests/ProcessManager/WebServerManagerTest.php @@ -13,8 +13,8 @@ namespace Symfony\Component\Panther\Tests\ProcessManager; -use PHPUnit\Framework\TestCase; use Symfony\Component\Panther\ProcessManager\WebServerManager; +use Symfony\Component\Panther\Tests\TestCase; /** * @author Kévin Dunglas @@ -25,17 +25,16 @@ public function testRun() { $server = new WebServerManager(__DIR__.'/../fixtures/', '127.0.0.1', 1234); $server->start(); - $this->assertContains('Hello', file_get_contents('http://127.0.0.1:1234/basic.html')); + $this->assertStringContainsString('Hello', (string) file_get_contents('http://127.0.0.1:1234/basic.html')); $server->quit(); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage The port 1234 is already in use. - */ public function testAlreadyRunning() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The port 1234 is already in use.'); + $server1 = new WebServerManager(__DIR__.'/../fixtures/', '127.0.0.1', 1234); $server1->start(); diff --git a/tests/ServerListenerTest.php b/tests/ServerListenerTest.php index 643db76d..c10e6cf9 100644 --- a/tests/ServerListenerTest.php +++ b/tests/ServerListenerTest.php @@ -19,6 +19,9 @@ use Symfony\Component\Panther\PantherTestCase; use Symfony\Component\Panther\ServerListener; +/** + * @group legacy + */ class ServerListenerTest extends TestCase { public static function tearDownAfterClass(): void diff --git a/tests/TestCase.php b/tests/TestCase.php index 85c565e8..fcd7267c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -27,6 +27,20 @@ abstract class TestCase extends PantherTestCase protected static $anotherUploadFileName = 'another-file.txt'; protected static $webServerDir = __DIR__.'/fixtures'; + /** + * Compatibility layer with PHPUnit 7. + */ + public static function assertStringContainsString(string $needle, string $haystack, string $message = ''): void + { + if (!method_exists(PantherTestCase::class, 'assertStringContainsString')) { + self::assertContains($needle, $haystack, $message); + + return; + } + + parent::assertStringContainsString($needle, $haystack, $message); + } + public function clientFactoryProvider(): array { // Tests must pass with both Panther and Goutte diff --git a/tests/WebDriver/WebDriverCheckBoxTest.php b/tests/WebDriver/WebDriverCheckBoxTest.php index 5a8b2a84..51aaf681 100644 --- a/tests/WebDriver/WebDriverCheckBoxTest.php +++ b/tests/WebDriver/WebDriverCheckBoxTest.php @@ -13,6 +13,8 @@ namespace Symfony\Component\Panther\Tests\WebDriver; +use Facebook\WebDriver\Exception\NoSuchElementException; +use Facebook\WebDriver\Exception\UnsupportedOperationException; use Symfony\Component\Panther\Tests\TestCase; use Symfony\Component\Panther\WebDriver\WebDriverCheckbox; @@ -102,11 +104,10 @@ public function selectByValueDataProvider() ]; } - /** - * @expectedException \Facebook\WebDriver\Exception\NoSuchElementException - */ public function testWebDriverCheckboxSelectByValueInvalid() { + $this->expectException(NoSuchElementException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="checkbox"]')->getElement(0); @@ -142,11 +143,10 @@ public function selectByIndexDataProvider() ]; } - /** - * @expectedException \Facebook\WebDriver\Exception\NoSuchElementException - */ public function testWebDriverCheckboxSelectByIndexInvalid() { + $this->expectException(NoSuchElementException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="checkbox"]')->getElement(0); @@ -260,55 +260,50 @@ public function testWebDriverCheckboxDeselectByVisiblePartialText() $this->assertEmpty($c->getAllSelectedOptions()); } - /** - * @expectedException \Facebook\WebDriver\Exception\UnsupportedOperationException - */ public function testWebDriverCheckboxDeselectAllRadio() { + $this->expectException(UnsupportedOperationException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="radio"]')->getElement(0); $c = new WebDriverCheckbox($element); $c->deselectAll(); } - /** - * @expectedException \Facebook\WebDriver\Exception\UnsupportedOperationException - */ public function testWebDriverCheckboxDeselectByIndexRadio() { + $this->expectException(UnsupportedOperationException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="radio"]')->getElement(0); $c = new WebDriverCheckbox($element); $c->deselectByIndex(0); } - /** - * @expectedException \Facebook\WebDriver\Exception\UnsupportedOperationException - */ public function testWebDriverCheckboxDeselectByValueRadio() { + $this->expectException(UnsupportedOperationException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="radio"]')->getElement(0); $c = new WebDriverCheckbox($element); $c->deselectByValue('val'); } - /** - * @expectedException \Facebook\WebDriver\Exception\UnsupportedOperationException - */ public function testWebDriverCheckboxDeselectByVisibleTextRadio() { + $this->expectException(UnsupportedOperationException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="radio"]')->getElement(0); $c = new WebDriverCheckbox($element); $c->deselectByVisibleText('AB'); } - /** - * @expectedException \Facebook\WebDriver\Exception\UnsupportedOperationException - */ public function testWebDriverCheckboxDeselectByVisiblePartialTextRadio() { + $this->expectException(UnsupportedOperationException::class); + $crawler = self::createPantherClient()->request('GET', self::$baseUri.'/form.html'); $element = $crawler->filterXPath('//input[@type="radio"]')->getElement(0); $c = new WebDriverCheckbox($element); diff --git a/tests/fixtures/normalize.html b/tests/fixtures/normalize.html new file mode 100644 index 00000000..60bb6b91 --- /dev/null +++ b/tests/fixtures/normalize.html @@ -0,0 +1,14 @@ + + + + + Normalize + + +
Foo + + Bar +Baz +
+ +