diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index 8e3b5728c3..ad8c47982e 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG ----- * Added `Form::getName()` method. +* Added `Crawler::matches()` method. +* Added `Crawler::closest()` method. +* Added `Crawler::outerHtml()` method. 4.3.0 ----- diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 017a48751b..6454669352 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -427,6 +427,45 @@ class Crawler implements \Countable, \IteratorAggregate return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild)); } + public function matches(string $selector): bool + { + if (!$this->nodes) { + return false; + } + + $converter = $this->createCssSelectorConverter(); + $xpath = $converter->toXPath($selector, 'self::'); + + return 0 !== $this->filterRelativeXPath($xpath)->count(); + } + + /** + * Return first parents (heading toward the document root) of the Element that matches the provided selector. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + * + * @throws \InvalidArgumentException When current node is empty + */ + public function closest(string $selector): ?self + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $domNode = $this->getNode(0); + + while (XML_ELEMENT_NODE === $domNode->nodeType) { + $node = $this->createSubCrawler($domNode); + if ($node->matches($selector)) { + return $node; + } + + $domNode = $node->getNode(0)->parentNode; + } + + return null; + } + /** * Returns the next siblings nodes of the current selection. * @@ -609,6 +648,22 @@ class Crawler implements \Countable, \IteratorAggregate return $html; } + public function outerHtml(): string + { + if (!\count($this)) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $owner = $node->ownerDocument; + + if (null !== $this->html5Parser && '' === $owner->saveXML($owner->childNodes[0])) { + $owner = $this->html5Parser; + } + + return $owner->saveHTML($node); + } + /** * Evaluates an XPath expression. * diff --git a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php index c17ac333f3..5703982ac0 100644 --- a/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/AbstractCrawlerTest.php @@ -880,6 +880,105 @@ HTML; } } + public function provideMatchTests() + { + yield ['#foo', true, '#foo']; + yield ['#foo', true, '.foo']; + yield ['#foo', true, '.other']; + yield ['#foo', false, '.bar']; + + yield ['#bar', true, '#bar']; + yield ['#bar', true, '.bar']; + yield ['#bar', true, '.other']; + yield ['#bar', false, '.foo']; + } + + /** @dataProvider provideMatchTests */ + public function testMatch(string $mainNodeSelector, bool $expected, string $selector) + { + $html = <<<'HTML' + + +
+
+
+
+
+ + +HTML; + + $crawler = $this->createCrawler($this->getDoctype().$html); + $node = $crawler->filter($mainNodeSelector); + $this->assertSame($expected, $node->matches($selector)); + } + + public function testClosest() + { + $html = <<<'HTML' + + +
+
+
+
+
+
+
+
+
+
+
+
+ + +HTML; + + $crawler = $this->createCrawler($this->getDoctype().$html); + $foo = $crawler->filter('#foo'); + + $newFoo = $foo->closest('#foo'); + $this->assertInstanceOf(Crawler::class, $newFoo); + $this->assertSame('newFoo ok', $newFoo->attr('class')); + + $lorem1 = $foo->closest('.lorem1'); + $this->assertInstanceOf(Crawler::class, $lorem1); + $this->assertSame('lorem1 ok', $lorem1->attr('class')); + + $lorem2 = $foo->closest('.lorem2'); + $this->assertInstanceOf(Crawler::class, $lorem2); + $this->assertSame('lorem2 ok', $lorem2->attr('class')); + + $lorem3 = $foo->closest('.lorem3'); + $this->assertNull($lorem3); + + $notFound = $foo->closest('.not-found'); + $this->assertNull($notFound); + } + + public function testOuterHtml() + { + $html = <<<'HTML' + + +
+ + + +HTML; + + $crawler = $this->createCrawler($this->getDoctype().$html); + $bar = $crawler->filter('ul'); + $output = $bar->outerHtml(); + $output = str_replace([' ', "\n"], '', $output); + $expected = ''; + $this->assertSame($expected, $output); + } + public function testNextAll() { $crawler = $this->createTestCrawler()->filterXPath('//li')->eq(1);