[DomCrawler] fix the axes handling in a bc way
The previous fix in #11548 for handling XPath axes was not backward compatible. In previous Symfony versions the Crawler handled nodes by holding a "fake root node". This must be taken into account when evaluating (relativizing) XPath expressions.
This commit is contained in:
parent
61763452b7
commit
d26040f356
@ -245,7 +245,9 @@ class Crawler extends \SplObjectStorage
|
|||||||
public function addNodeList(\DOMNodeList $nodes)
|
public function addNodeList(\DOMNodeList $nodes)
|
||||||
{
|
{
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$this->addNode($node);
|
if ($node instanceof \DOMNode) {
|
||||||
|
$this->addNode($node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -834,18 +836,22 @@ class Crawler extends \SplObjectStorage
|
|||||||
// BC for Symfony 2.4 and lower were elements were adding in a fake _root parent
|
// BC for Symfony 2.4 and lower were elements were adding in a fake _root parent
|
||||||
if (0 === strpos($expression, '/_root/')) {
|
if (0 === strpos($expression, '/_root/')) {
|
||||||
$expression = './'.substr($expression, 7);
|
$expression = './'.substr($expression, 7);
|
||||||
|
} elseif (0 === strpos($expression, 'self::*/')) {
|
||||||
|
$expression = './'.substr($expression, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add prefix before absolute element selector
|
// add prefix before absolute element selector
|
||||||
if (empty($expression)) {
|
if (empty($expression)) {
|
||||||
$expression = $nonMatchingExpression;
|
$expression = $nonMatchingExpression;
|
||||||
} elseif (0 === strpos($expression, '//')) {
|
} elseif (0 === strpos($expression, '//')) {
|
||||||
$expression = 'descendant-or-self::' . substr($expression, 2);
|
$expression = 'descendant-or-self::'.substr($expression, 2);
|
||||||
} elseif (0 === strpos($expression, './/')) {
|
} elseif (0 === strpos($expression, './/')) {
|
||||||
$expression = 'descendant-or-self::' . substr($expression, 3);
|
$expression = 'descendant-or-self::'.substr($expression, 3);
|
||||||
} elseif (0 === strpos($expression, './')) {
|
} elseif (0 === strpos($expression, './')) {
|
||||||
$expression = 'self::' . substr($expression, 2);
|
$expression = 'self::'.substr($expression, 2);
|
||||||
} elseif ('/' === $expression[0]) {
|
} elseif (0 === strpos($expression, 'child::')) {
|
||||||
|
$expression = 'self::'.substr($expression, 7);
|
||||||
|
} elseif ('/' === $expression[0] || 0 === strpos($expression, 'self::')) {
|
||||||
// the only direct child in Symfony 2.4 and lower is _root, which is already handled previously
|
// the only direct child in Symfony 2.4 and lower is _root, which is already handled previously
|
||||||
// so let's drop the expression entirely
|
// so let's drop the expression entirely
|
||||||
$expression = $nonMatchingExpression;
|
$expression = $nonMatchingExpression;
|
||||||
@ -853,9 +859,12 @@ class Crawler extends \SplObjectStorage
|
|||||||
// '.' is the fake root element in Symfony 2.4 and lower, which is excluded from results
|
// '.' is the fake root element in Symfony 2.4 and lower, which is excluded from results
|
||||||
$expression = $nonMatchingExpression;
|
$expression = $nonMatchingExpression;
|
||||||
} elseif (0 === strpos($expression, 'descendant::')) {
|
} elseif (0 === strpos($expression, 'descendant::')) {
|
||||||
$expression = 'descendant-or-self::' . substr($expression, strlen('descendant::'));
|
$expression = 'descendant-or-self::'.substr($expression, strlen('descendant::'));
|
||||||
} elseif (!preg_match('/^(ancestor|ancestor-or-self|attribute|child|descendant-or-self|following|following-sibling|parent|preceding|preceding-sibling|self)::/', $expression)) {
|
} elseif (preg_match('/^(ancestor|ancestor-or-self|attribute|following|following-sibling|namespace|parent|preceding|preceding-sibling)::/', $expression)) {
|
||||||
$expression = 'self::' .$expression;
|
// the fake root has no parent, preceding or following nodes and also no attributes (even no namespace attributes)
|
||||||
|
$expression = $nonMatchingExpression;
|
||||||
|
} elseif (0 !== strpos($expression, 'descendant-or-self::')) {
|
||||||
|
$expression = 'self::'.$expression;
|
||||||
}
|
}
|
||||||
$expressions[] = $parenthesis.$expression;
|
$expressions[] = $parenthesis.$expression;
|
||||||
}
|
}
|
||||||
|
@ -388,8 +388,8 @@ EOF
|
|||||||
$this->assertCount(1, $crawler->filterXPath('//body'));
|
$this->assertCount(1, $crawler->filterXPath('//body'));
|
||||||
$this->assertCount(1, $crawler->filterXPath('descendant-or-self::body'));
|
$this->assertCount(1, $crawler->filterXPath('descendant-or-self::body'));
|
||||||
$this->assertCount(1, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('./div'), 'A child selection finds only the current div');
|
$this->assertCount(1, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('./div'), 'A child selection finds only the current div');
|
||||||
$this->assertCount(2, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('descendant::div'), 'A descendant selector matches the current div and its child');
|
$this->assertCount(3, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('descendant::div'), 'A descendant selector matches the current div and its child');
|
||||||
$this->assertCount(2, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('//div'), 'A descendant selector matches the current div and its child');
|
$this->assertCount(3, $crawler->filterXPath('//div[@id="parent"]')->filterXPath('//div'), 'A descendant selector matches the current div and its child');
|
||||||
$this->assertCount(5, $crawler->filterXPath('(//a | //div)//img'));
|
$this->assertCount(5, $crawler->filterXPath('(//a | //div)//img'));
|
||||||
$this->assertCount(7, $crawler->filterXPath('((//a | //div)//img | //ul)'));
|
$this->assertCount(7, $crawler->filterXPath('((//a | //div)//img | //ul)'));
|
||||||
$this->assertCount(7, $crawler->filterXPath('( ( //a | //div )//img | //ul )'));
|
$this->assertCount(7, $crawler->filterXPath('( ( //a | //div )//img | //ul )'));
|
||||||
@ -411,72 +411,104 @@ EOF
|
|||||||
$this->assertCount(3, $crawler->filterXPath('//body')->filterXPath('//button')->parents(), '->filterXpath() preserves parents when chained');
|
$this->assertCount(3, $crawler->filterXPath('//body')->filterXPath('//button')->parents(), '->filterXpath() preserves parents when chained');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFilterXPathWithFakeRoot()
|
||||||
|
{
|
||||||
|
$crawler = $this->createTestCrawler();
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('.'), '->filterXPath() returns an empty result if the XPath references the fake root node');
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('/_root'), '->filterXPath() returns an empty result if the XPath references the fake root node');
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('self::*'), '->filterXPath() returns an empty result if the XPath references the fake root node');
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('self::_root'), '->filterXPath() returns an empty result if the XPath references the fake root node');
|
||||||
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithAncestorAxis()
|
public function testFilterXPathWithAncestorAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
||||||
|
|
||||||
$this->assertCount(2, $crawler->filterXPath('ancestor::*'));
|
$this->assertCount(0, $crawler->filterXPath('ancestor::*'), 'The fake root node has no ancestor nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithAncestorOrSelfAxis()
|
public function testFilterXPathWithAncestorOrSelfAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
||||||
|
|
||||||
$this->assertCount(3, $crawler->filterXPath('ancestor-or-self::*'));
|
$this->assertCount(0, $crawler->filterXPath('ancestor-or-self::*'), 'The fake root node has no ancestor nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithAttributeAxis()
|
public function testFilterXPathWithAttributeAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
||||||
|
|
||||||
$this->assertCount(2, $crawler->filterXPath('attribute::*'));
|
$this->assertCount(0, $crawler->filterXPath('attribute::*'), 'The fake root node has no attribute nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterXPathWithAttributeAxisAfterElementAxis()
|
||||||
|
{
|
||||||
|
$this->assertCount(3, $this->createTestCrawler()->filterXPath('//form/button/attribute::*'), '->filterXPath() handles attribute axes properly when they are preceded by an element filtering axis');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithChildAxis()
|
public function testFilterXPathWithChildAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//body');
|
$crawler = $this->createTestCrawler()->filterXPath('//div[@id="parent"]');
|
||||||
|
|
||||||
$this->assertCount(2, $crawler->filterXPath('child::input'));
|
$this->assertCount(1, $crawler->filterXPath('child::div'), 'A child selection finds only the current div');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithFollowingAxis()
|
public function testFilterXPathWithFollowingAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//a');
|
$crawler = $this->createTestCrawler()->filterXPath('//a');
|
||||||
|
|
||||||
$this->assertCount(3, $crawler->filterXPath('following::div'));
|
$this->assertCount(0, $crawler->filterXPath('following::div'), 'The fake root node has no following nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithFollowingSiblingAxis()
|
public function testFilterXPathWithFollowingSiblingAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//a');
|
$crawler = $this->createTestCrawler()->filterXPath('//a');
|
||||||
|
|
||||||
$this->assertCount(2, $crawler->filterXPath('following-sibling::div'));
|
$this->assertCount(0, $crawler->filterXPath('following-sibling::div'), 'The fake root node has no following nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterXPathWithNamespaceAxis()
|
||||||
|
{
|
||||||
|
$crawler = $this->createTestCrawler()->filterXPath('//button');
|
||||||
|
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('namespace::*'), 'The fake root node has no namespace nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterXPathWithNamespaceAxisAfterElementAxis()
|
||||||
|
{
|
||||||
|
$crawler = $this->createTestCrawler()->filterXPath('//div[@id="parent"]/namespace::*');
|
||||||
|
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('namespace::*'), 'Namespace axes cannot be requested');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithParentAxis()
|
public function testFilterXPathWithParentAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//button');
|
$crawler = $this->createTestCrawler()->filterXPath('//button');
|
||||||
|
|
||||||
$this->assertEquals('foo', $crawler->filterXPath('parent::*')->attr('action'));
|
$this->assertCount(0, $crawler->filterXPath('parent::*'), 'The fake root node has no parent nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithPrecedingAxis()
|
public function testFilterXPathWithPrecedingAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
||||||
|
|
||||||
$this->assertCount(13, $crawler->filterXPath('preceding::*'));
|
$this->assertCount(0, $crawler->filterXPath('preceding::*'), 'The fake root node has no preceding nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithPrecedingSiblingAxis()
|
public function testFilterXPathWithPrecedingSiblingAxis()
|
||||||
{
|
{
|
||||||
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
$crawler = $this->createTestCrawler()->filterXPath('//form');
|
||||||
|
|
||||||
$this->assertCount(9, $crawler->filterXPath('preceding-sibling::*'));
|
$this->assertCount(0, $crawler->filterXPath('preceding-sibling::*'), 'The fake root node has no preceding nodes');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterXPathWithSelfAxes()
|
public function testFilterXPathWithSelfAxes()
|
||||||
{
|
{
|
||||||
$this->assertCount(1, $this->createTestCrawler()->filterXPath('self::*'));
|
$crawler = $this->createTestCrawler()->filterXPath('//a');
|
||||||
|
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('self::a'), 'The fake root node has no "real" element name');
|
||||||
|
$this->assertCount(0, $crawler->filterXPath('self::a/img'), 'The fake root node has no "real" element name');
|
||||||
|
$this->assertCount(9, $crawler->filterXPath('self::*/a'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -844,6 +876,7 @@ HTML;
|
|||||||
</ul>
|
</ul>
|
||||||
<div id="parent">
|
<div id="parent">
|
||||||
<div id="child"></div>
|
<div id="child"></div>
|
||||||
|
<div id="child2" xmlns:foo="http://example.com"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sibling"><img /></div>
|
<div id="sibling"><img /></div>
|
||||||
</body>
|
</body>
|
||||||
|
Reference in New Issue
Block a user