From a04ecc3753e1765279305a46a4e938c3c76c1b3d Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Fri, 22 Mar 2024 10:24:35 +1100 Subject: [PATCH 1/6] Unit tests that need to pass for issue 49 --- tests/Issues/Issue49Test.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Issues/Issue49Test.php diff --git a/tests/Issues/Issue49Test.php b/tests/Issues/Issue49Test.php new file mode 100644 index 0000000..2ccabfd --- /dev/null +++ b/tests/Issues/Issue49Test.php @@ -0,0 +1,31 @@ +', 'div'); + + /* Check if the DOMNode or its children matches */ + $this->assertTrue($q->is(':text')); + $this->assertCount(2, $q->find(':text')); + + $textNode = $q->find('div')->contents()->eq(0); + $this->assertTrue($textNode->is(':text')); + } + + public function testCheckingForEmptyTextInputs(): void + { + $q = html5qp('
Sample
', 'div'); + + /* Check if the DOMNode or its children matches */ + $this->assertFalse($q->is(':text')); + $this->assertCount(0, $q->find(':text')); + + /* check if a text node matches */ + $textNode = $q->find('div')->contents()->eq(0); + $this->assertFalse($textNode->is(':text')); + } +} \ No newline at end of file From 9a1908324c200ec5ad990ef17791e1e371b72538 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Fri, 21 Aug 2026 14:39:07 +1000 Subject: [PATCH 2/6] Fix :text pseudo-class and selector fatals on non-element nodes Two defects, per issue #49. 1. Running any selector against a match set that contained a non-element node (text, comment, CDATA, processing instruction) fataled, because the traverser assumed every node was a DOMElement and called element-only methods such as getElementsByTagName() and tagName on it. Non-element nodes now simply do not match an element selector: - DOMTraverser::matchesSimpleSelector() returns FALSE for any node that is not a DOMElement. matchesSelector(), matchesSimpleSelector() and combine() take a DOMNode rather than a DOMElement so they can make that decision instead of raising a TypeError. - initialMatchOnElement(), initialMatchOnID() and initialMatchOnClasses() skip nodes that cannot hold elements. - PseudoClass::elementMatches() and Util::matchesAttribute[NS]() guard against non-elements as well, since they are reachable directly. initialMatchOnElement() also now captures the node itself when the element selector is the wildcard, which is what initialMatchOnID() and initialMatchOnClasses() already do for their own selectors. 2. The :text pseudo-class matched anything with type="text". It now follows jQuery, matching an input whose type attribute is absent (text is an input's default type) or is text, compared case-insensitively. It has never indicated whether a node is a text node. Fixed in both the current engine (CSS\DOMTraverser) and the legacy engine (CSS\QueryPathEventHandler) that remove() and replaceAll() still use, so the two agree. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 + src/CSS/DOMTraverser.php | 46 ++++++-- src/CSS/DOMTraverser/PseudoClass.php | 38 +++++++ src/CSS/DOMTraverser/Util.php | 12 ++ src/CSS/QueryPathEventHandler.php | 31 +++++ tests/Issues/Issue49Test.php | 163 ++++++++++++++++++++++++++- 6 files changed, 284 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac1c5e7..d5b8f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ QueryPath Changelog - Fix processing instructions gaining an extra `?` each time an HTML-parsed document was serialized, so `` came back out of `html()`, `innerHTML()`, `innerXML()`, `innerXHTML()`, `xml()`, `html5()`, `innerHTML5()`, and `writeXML()` as ``. libxml's HTML parser keeps the closing `?` as part of the node's data, unlike its XML parser and the Masterminds HTML5 parser, so QueryPath now strips it on load. `DOMProcessingInstruction::$data` consequently no longer has a stray `?` on the end for documents read with `htmlqp()` or `qp()` on an `.html` file. Note that this normalisation applies only to documents QueryPath parses itself, and that taking the underlying `DOMDocument` out of QueryPath and calling libxml's own `saveHTML()` on it will emit `` without the terminator, since libxml's HTML serializer never adds one - Add `QueryPath\Document`, a `DOMDocument` subclass QueryPath parses into. The type is how QueryPath records that a document's processing instruction data does not carry the closing `?`, which cannot be determined by inspecting the document afterwards. It travels with the document, so every route to a second `DOMQuery` over one document -- iteration, `add()`, `remove()`, `replaceAll()`, `branch()`, `QueryPath::with()`, and the bundled extensions -- serializes it correctly. A `DOMDocument` supplied by the caller is a plain `DOMDocument`, makes no such promise, and is still serialized exactly as it was handed over +- Fix fatal error when running a CSS selector against a match set that contains non-element nodes (text, comment, CDATA + or processing instruction). Those nodes now simply do not match, instead of calling element-only DOM methods on them. +- Fix the `:text` pseudo-class so it matches jQuery: it selects `input` elements whose `type` attribute is absent or is + `text` (case-insensitively). It never indicated, and still does not indicate, whether a node is a text node. + # 4.1.0 - Update composer.json to mark library as PHP 8.4 compatible diff --git a/src/CSS/DOMTraverser.php b/src/CSS/DOMTraverser.php index 7c6f09c..f86afdc 100644 --- a/src/CSS/DOMTraverser.php +++ b/src/CSS/DOMTraverser.php @@ -7,6 +7,7 @@ use DOMDocument; use DOMElement; +use DOMNode; use DOMNodeList; use DOMXPath; use QueryPath\CSS\DOMTraverser\Util; @@ -375,14 +376,14 @@ public function matches() * absolutely huge selectors or for versions of PHP tuned to * strictly limit recursion depth. * - * @param DOMElement $node + * @param DOMNode $node * The DOMNode to check. * @param $selector * * @return boolean * A boolean TRUE if the node matches, false otherwise. */ - public function matchesSelector(DOMElement $node, $selector) + public function matchesSelector(DOMNode $node, $selector) { return $this->matchesSimpleSelector($node, $selector, 0); } @@ -394,7 +395,7 @@ public function matchesSelector(DOMElement $node, $selector) * this checks only a simple selector (plus an optional * combinator). * - * @param DOMElement $node + * @param DOMNode $node * @param $selectors * @param $index * @@ -402,8 +403,16 @@ public function matchesSelector(DOMElement $node, $selector) * A boolean TRUE if the node matches, false otherwise. * @throws NotImplementedException */ - public function matchesSimpleSelector(DOMElement $node, $selectors, $index) + public function matchesSimpleSelector(DOMNode $node, $selectors, $index) { + // Selectors only ever match elements. A match set may legitimately + // contain text, comment, CDATA or processing instruction nodes (e.g. + // from contents()), and those simply do not match -- rather than + // blowing up on the element-only DOM API used below. + if (! $node instanceof DOMElement) { + return false; + } + $selector = $selectors[$index]; // A set-level pseudo-class on a non-subject simple selector has already @@ -465,7 +474,7 @@ public function matchesSimpleSelector(DOMElement $node, $selectors, $index) * @return boolean * TRUE if the next selector(s) match. */ - public function combine(DOMElement $node, $selectors, $index) + public function combine(DOMNode $node, $selectors, $index) { $selector = $selectors[$index]; //$this->debug(implode(' ', $selectors)); @@ -684,6 +693,11 @@ protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $ // Now we try to find any matching IDs. /** @var DOMElement $node */ foreach ($matches as $node) { + // Non-element nodes have neither attributes nor element children. + if (! $node instanceof DOMElement) { + continue; + } + if ($node->getAttribute('id') === $id) { $found->offsetSet($node); } @@ -728,6 +742,11 @@ protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStor // Now we try to find any matching IDs. /** @var DOMElement $node */ foreach ($matches as $node) { + // Non-element nodes have neither attributes nor element children. + if (! $node instanceof DOMElement) { + continue; + } + // Refactor me! if ($node->hasAttribute('class')) { $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class'))); @@ -793,12 +812,19 @@ protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStor $element = '*'; } $found = $this->newMatches(); - /** @var DOMDocument $node */ + /** @var DOMDocument|DOMElement $node */ foreach ($matches as $node) { - // Capture the case where the initial element is the root element. - if ($node->tagName === $element - || ($element === '*' && $node->parentNode instanceof DOMDocument)) { - $found->offsetSet($node); + // Only elements and documents can contain elements. Text, comment, + // CDATA and processing instruction nodes never match, and do not + // support the element-only API used below. + if (! $node instanceof DOMElement && ! $node instanceof DOMDocument) { + continue; + } + + // Capture the case where the node itself matches the element. + if ($node instanceof DOMElement + && ($element === '*' || $node->tagName === $element)) { + $found->attach($node); } $nl = $node->getElementsByTagName($element); if (! empty($nl) && $nl instanceof DOMNodeList) { diff --git a/src/CSS/DOMTraverser/PseudoClass.php b/src/CSS/DOMTraverser/PseudoClass.php index a36d346..567d074 100644 --- a/src/CSS/DOMTraverser/PseudoClass.php +++ b/src/CSS/DOMTraverser/PseudoClass.php @@ -12,6 +12,7 @@ namespace QueryPath\CSS\DOMTraverser; +use DOMElement; use QueryPath\CSS\DOMTraverser; use QueryPath\CSS\NotImplementedException; use QueryPath\CSS\EventHandler; @@ -46,6 +47,13 @@ class PseudoClass */ public function elementMatches($pseudoclass, $node, $scope, $value = null) { + // Pseudo-classes are only ever satisfied by elements. Text, comment, + // CDATA and processing instruction nodes have no tag name, attributes + // or element children, so they can never match. + if (! $node instanceof DOMElement) { + return false; + } + $name = strtolower($pseudoclass); // Need to handle known pseudoclasses. switch ($name) { @@ -154,6 +162,8 @@ public function elementMatches($pseudoclass, $node, $scope, $value = null) case 'checked': return Util::matchesAttribute($node, $name); case 'text': + return $this->isTextInput($node); + case 'radio': case 'checkbox': case 'file': @@ -220,6 +230,34 @@ protected function lang($node, $value) return false; } + /** + * Provides jQuery pseudoclass ':text'. + * + * This mirrors jQuery, where `:text` selects `input` elements of type text + * -- that is, an `input` whose `type` attribute is either absent (`text` is + * the default type of an `input`) or is `text`, matched case-insensitively. + * + * It does NOT indicate whether the node is a text node. + * + * @param DOMElement $node + * + * @return bool + * @see https://api.jquery.com/text-selector/ + */ + protected function isTextInput($node): bool + { + if (strtolower($node->localName) !== 'input') { + return false; + } + + // An input with no type attribute defaults to a text input. + if (! $node->hasAttribute('type')) { + return true; + } + + return strtolower($node->getAttribute('type')) === 'text'; + } + /** * Provides jQuery pseudoclass ':header'. * diff --git a/src/CSS/DOMTraverser/Util.php b/src/CSS/DOMTraverser/Util.php index 6a3ddac..9a89e99 100644 --- a/src/CSS/DOMTraverser/Util.php +++ b/src/CSS/DOMTraverser/Util.php @@ -7,6 +7,7 @@ namespace QueryPath\CSS\DOMTraverser; +use DOMElement; use DOMNode; use QueryPath\CSS\EventHandler; use QueryPath\CSS\ParseException; @@ -365,6 +366,12 @@ private static function comparePaths(array $a, array $b): int */ public static function matchesAttribute($node, $name, $value = null, $operation = EventHandler::IS_EXACTLY): bool { + // Only elements have attributes. Text, comment, CDATA and processing + // instruction nodes can never match an attribute selector. + if (! $node instanceof DOMElement) { + return false; + } + if (! $node->hasAttribute($name)) { return false; } @@ -386,6 +393,11 @@ public static function matchesAttributeNS( $value = null, $operation = EventHandler::IS_EXACTLY ) { + // Only elements have attributes. + if (! $node instanceof DOMElement) { + return false; + } + if (! $node->hasAttributeNS($nsuri, $name)) { return false; } diff --git a/src/CSS/QueryPathEventHandler.php b/src/CSS/QueryPathEventHandler.php index 13e6b43..76a1802 100644 --- a/src/CSS/QueryPathEventHandler.php +++ b/src/CSS/QueryPathEventHandler.php @@ -357,6 +357,35 @@ public function attribute($name, $value = null, $operation = EventHandler::IS_EX $this->findAnyElement = false; } + /** + * Helper function for the jQuery ':text' pseudo-class. + * + * As in jQuery, ':text' selects `input` elements of type text -- that is, an + * `input` whose `type` attribute is either absent (`text` is the default + * type of an `input`) or is `text`, matched case-insensitively. It does NOT + * indicate whether the node is a text node. + * + * @see https://api.jquery.com/text-selector/ + */ + protected function textInput() + { + $found = new SplObjectStorage(); + $matches = $this->candidateList(); + foreach ($matches as $item) { + if (strtolower($item->localName) !== 'input') { + continue; + } + + // An input with no type attribute defaults to a text input. + if (! $item->hasAttribute('type') || strtolower($item->getAttribute('type')) === 'text') { + $found->attach($item); + } + } + + $this->matches = $found; + $this->findAnyElement = false; + } + /** * Helper function to find all elements with exact matches. * @@ -553,6 +582,8 @@ public function pseudoClass($name, $value = null) $this->attribute($name); break; case 'text': + $this->textInput(); + break; case 'radio': case 'checkbox': case 'file': diff --git a/tests/Issues/Issue49Test.php b/tests/Issues/Issue49Test.php index 2ccabfd..9cf32b7 100644 --- a/tests/Issues/Issue49Test.php +++ b/tests/Issues/Issue49Test.php @@ -2,8 +2,42 @@ namespace QueryPathTests; +use DOMCdataSection; +use DOMComment; +use DOMProcessingInstruction; +use DOMText; + class Issue49Test extends TestCase { + protected const INPUT_HTML = '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
'; + + /** + * Get the ID of every element in the match set. + * + * @param \QueryPath\DOMQuery $query + * + * @return array + */ + protected function ids($query): array + { + $ids = []; + foreach ($query as $item) { + $ids[] = $item->attr('id'); + } + sort($ids); + + return $ids; + } + public function testCheckingForMatchingTextInputs(): void { $q = html5qp('
', 'div'); @@ -28,4 +62,131 @@ public function testCheckingForEmptyTextInputs(): void $textNode = $q->find('div')->contents()->eq(0); $this->assertFalse($textNode->is(':text')); } -} \ No newline at end of file + + /** + * As in jQuery, ':text' matches an `input` whose type is absent or 'text' + * (case-insensitively), and nothing else. + * + * @see https://api.jquery.com/text-selector/ + */ + public function testTextSelectorOnlyMatchesTextInputs(): void + { + $q = html5qp(self::INPUT_HTML, 'div'); + + $this->assertSame(['a', 'b', 'c'], $this->ids($q->find(':text'))); + } + + public function testTextSelectorMatchesTheInputItself(): void + { + $this->assertTrue(html5qp('
', 'input')->is(':text')); + $this->assertTrue(html5qp('
', 'input')->is(':text')); + $this->assertTrue(html5qp('
', 'input')->is(':text')); + + $this->assertFalse(html5qp('
', 'input')->is(':text')); + $this->assertFalse(html5qp('
', 'input')->is(':text')); + $this->assertFalse(html5qp('
', 'textarea')->is(':text')); + $this->assertFalse(html5qp('
', 'button')->is(':text')); + } + + /** + * remove() runs the selector through the legacy CSS engine, which must agree + * with find(). + */ + public function testTextSelectorInTheLegacyEngine(): void + { + $q = html5qp(self::INPUT_HTML, 'div'); + + $this->assertSame(['a', 'b', 'c'], $this->ids($q->remove(':text'))); + $this->assertCount(0, $q->find(':text')); + } + + /** + * Any selector run against a match set holding a text node must return a + * sane result rather than fataling on the element-only DOM API. + */ + public function testSelectorsAgainstATextNodeDoNotThrow(): void + { + $textNode = html5qp('
SampleChild
', 'div') + ->contents() + ->eq(0); + + $this->assertInstanceOf(DOMText::class, $textNode->get(0)); + + $this->assertFalse($textNode->is('*')); + $this->assertFalse($textNode->is('span')); + $this->assertFalse($textNode->is('.wrap')); + $this->assertFalse($textNode->is('#wrap')); + $this->assertFalse($textNode->is('[class]')); + $this->assertFalse($textNode->is('[class="wrap"]')); + $this->assertFalse($textNode->is(':first-child')); + $this->assertFalse($textNode->is('div span')); + + $this->assertCount(0, $textNode->find('*')); + $this->assertCount(0, $textNode->find('span')); + $this->assertCount(0, $textNode->find('.wrap')); + $this->assertCount(0, $textNode->find('#wrap')); + $this->assertCount(0, $textNode->find('[class]')); + $this->assertCount(0, $textNode->filter('*')); + } + + public function testSelectorsAgainstACommentNodeDoNotThrow(): void + { + $comment = html5qp('
Child
', 'div') + ->contents() + ->eq(0); + + $this->assertInstanceOf(DOMComment::class, $comment->get(0)); + + $this->assertFalse($comment->is('*')); + $this->assertFalse($comment->is('span')); + $this->assertFalse($comment->is('.wrap')); + $this->assertFalse($comment->is('#wrap')); + $this->assertFalse($comment->is('[class]')); + $this->assertFalse($comment->is(':text')); + + $this->assertCount(0, $comment->find('*')); + $this->assertCount(0, $comment->find('span')); + $this->assertCount(0, $comment->find('[class]')); + } + + public function testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow(): void + { + $contents = qp( + 'Text', + 'root' + )->contents(); + + $cdata = $contents->eq(0); + $pi = $contents->eq(1); + + $this->assertInstanceOf(DOMCdataSection::class, $cdata->get(0)); + $this->assertInstanceOf(DOMProcessingInstruction::class, $pi->get(0)); + + foreach ([$cdata, $pi] as $node) { + $this->assertFalse($node->is('*')); + $this->assertFalse($node->is('child')); + $this->assertFalse($node->is('.c')); + $this->assertFalse($node->is('#i')); + $this->assertFalse($node->is('[class]')); + + $this->assertCount(0, $node->find('*')); + $this->assertCount(0, $node->find('child')); + } + } + + /** + * A match set mixing elements with non-element nodes must still match the + * elements it holds. + */ + public function testMixedNodeMatchSetStillMatchesItsElements(): void + { + $contents = html5qp('
SampleChild
', 'div')->contents(); + + $this->assertCount(2, $contents); + $this->assertSame(['s'], $this->ids($contents->find('span'))); + $this->assertSame(['s'], $this->ids($contents->find('.x'))); + $this->assertSame(['s'], $this->ids($contents->find('#s'))); + $this->assertSame(['s'], $this->ids($contents->filter('span'))); + $this->assertTrue($contents->is('.x')); + } +} From 0bd2161539a9d18bde59c243778d3ae0cbd35df4 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Sat, 22 Aug 2026 04:21:22 +1000 Subject: [PATCH 3/6] Test :text against the collection rather than its descendants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assertion held the
and expected is(':text') to be true, which only worked because is() ran a descendant search. #72 makes is() test the elements in the match set, as jQuery does, so that assertion would flip to false. Rewritten so it does not depend on which semantics are in force: the containment question is asked with has(), which is what it always meant, and is() is asked of the inputs themselves. It passes both with and without #72. Also renamed $textNode to $firstInput in this test. contents()->eq(0) here is the first element, not a text node — the name is accurate in the sibling test below, where the fixture really does hold text. Co-Authored-By: Claude Opus 5 (1M context) --- .phpunit.result.cache | 1 + tests/Issues/Issue49Test.php | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 .phpunit.result.cache diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..49614cc --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":{"QueryPathTests\\Issue49Test::testCheckingForMatchingTextInputs":0.006,"QueryPathTests\\Issue49Test::testCheckingForEmptyTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorOnlyMatchesTextInputs":0.001,"QueryPathTests\\Issue49Test::testTextSelectorMatchesTheInputItself":0.001,"QueryPathTests\\Issue49Test::testTextSelectorInTheLegacyEngine":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstATextNodeDoNotThrow":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstACommentNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow":0,"QueryPathTests\\Issue49Test::testMixedNodeMatchSetStillMatchesItsElements":0}} \ No newline at end of file diff --git a/tests/Issues/Issue49Test.php b/tests/Issues/Issue49Test.php index 9cf32b7..ceabcdc 100644 --- a/tests/Issues/Issue49Test.php +++ b/tests/Issues/Issue49Test.php @@ -42,12 +42,21 @@ public function testCheckingForMatchingTextInputs(): void { $q = html5qp('
', 'div'); - /* Check if the DOMNode or its children matches */ - $this->assertTrue($q->is(':text')); + /* + * The collection holds the
. It is not itself a text input, but it contains two, + * so the containment question is asked with has() and the matches with find(). + */ + $this->assertCount(1, $q->has(':text')); $this->assertCount(2, $q->find(':text')); - $textNode = $q->find('div')->contents()->eq(0); - $this->assertTrue($textNode->is(':text')); + /* The inputs themselves match: an explicit type="text", and an with no type */ + $this->assertTrue($q->find('input')->is(':text')); + $this->assertTrue($q->find('[name="text1"]')->is(':text')); + $this->assertTrue($q->find('[name="text2"]')->is(':text')); + + /* contents() here holds the two elements, not text nodes */ + $firstInput = $q->find('div')->contents()->eq(0); + $this->assertTrue($firstInput->is(':text')); } public function testCheckingForEmptyTextInputs(): void From 026ecce7df8af908867439fe5f9578d489bb760d Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Sat, 22 Aug 2026 04:37:30 +1000 Subject: [PATCH 4/6] Ask find() for descendants and filter() for the set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two assertions reached find() for a node that was already in their own match set, which only worked because find() self-matched. #73 makes find() search descendants only, as jQuery does. Rewritten to ask each question of the method that answers it: find() of a real descendant, filter()/is() of the elements in the set. The mixed-node fixture gains a nested so find() still has something to reach, which keeps the point of the test — that a set holding a text node does not cause a fatal — intact on both sides of the selector. Passes with and without #72/#73. Co-Authored-By: Claude Opus 5 (1M context) --- .phpunit.result.cache | 2 +- tests/Issues/Issue49Test.php | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 49614cc..851153e 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":[],"times":{"QueryPathTests\\Issue49Test::testCheckingForMatchingTextInputs":0.006,"QueryPathTests\\Issue49Test::testCheckingForEmptyTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorOnlyMatchesTextInputs":0.001,"QueryPathTests\\Issue49Test::testTextSelectorMatchesTheInputItself":0.001,"QueryPathTests\\Issue49Test::testTextSelectorInTheLegacyEngine":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstATextNodeDoNotThrow":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstACommentNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow":0,"QueryPathTests\\Issue49Test::testMixedNodeMatchSetStillMatchesItsElements":0}} \ No newline at end of file +{"version":1,"defects":{"QueryPathTests\\DOMQueryTest::testFilterLambda":1,"QueryPathTests\\DOMQueryTest::testEachLambda":1},"times":{"QueryPathTests\\Issue49Test::testCheckingForMatchingTextInputs":0.004,"QueryPathTests\\Issue49Test::testCheckingForEmptyTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorOnlyMatchesTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorMatchesTheInputItself":0.001,"QueryPathTests\\Issue49Test::testTextSelectorInTheLegacyEngine":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstATextNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstACommentNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow":0,"QueryPathTests\\Issue49Test::testMixedNodeMatchSetStillMatchesItsElements":0,"QueryPathTests\\CSS\\DOMTraverserTest::testConstructor":0,"QueryPathTests\\CSS\\DOMTraverserTest::testFind":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatches":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchElement":0.001,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchAttributes":0.002,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchId":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchClasses":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchPseudoClasses":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchPseudoElements":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineAdjacent":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineSibling":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineDirectDescendant":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineAnyDescendant":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMultipleSelectors":0,"QueryPathTests\\CSS\\ParserTest::testElementID":0.003,"QueryPathTests\\CSS\\ParserTest::testElement":0,"QueryPathTests\\CSS\\ParserTest::testElementNS":0,"QueryPathTests\\CSS\\ParserTest::testAnyElement":0,"QueryPathTests\\CSS\\ParserTest::testAnyElementInNS":0,"QueryPathTests\\CSS\\ParserTest::testElementClass":0,"QueryPathTests\\CSS\\ParserTest::testPseudoClass":0,"QueryPathTests\\CSS\\ParserTest::testPseudoElement":0,"QueryPathTests\\CSS\\ParserTest::testDirectDescendant":0,"QueryPathTests\\CSS\\ParserTest::testAnyDescendant":0,"QueryPathTests\\CSS\\ParserTest::testAdjacent":0,"QueryPathTests\\CSS\\ParserTest::testSibling":0,"QueryPathTests\\CSS\\ParserTest::testAnotherSelector":0,"QueryPathTests\\CSS\\ParserTest::testIllegalAttribute":0,"QueryPathTests\\CSS\\ParserTest::testAttribute":0.001,"QueryPathTests\\CSS\\ParserTest::testAttributeNS":0,"QueryPathTests\\CSS\\ParserTest::testIllegalCombinators1":0,"QueryPathTests\\CSS\\ParserTest::testIllegalCombinators2":0,"QueryPathTests\\CSS\\ParserTest::testIllegalID":0,"QueryPathTests\\CSS\\ParserTest::testElementNSClassAndAttribute":0,"QueryPathTests\\CSS\\ParserTest::testAllCombo":0,"QueryPathTests\\CSS\\PseudoClassTest::testUnknownPseudoClass":0,"QueryPathTests\\CSS\\PseudoClassTest::testLang":0,"QueryPathTests\\CSS\\PseudoClassTest::testLangNS":0,"QueryPathTests\\CSS\\PseudoClassTest::testFormType":0,"QueryPathTests\\CSS\\PseudoClassTest::testHasAttribute":0,"QueryPathTests\\CSS\\PseudoClassTest::testHeader":0,"QueryPathTests\\CSS\\PseudoClassTest::testContains":0,"QueryPathTests\\CSS\\PseudoClassTest::testContainsExactly":0,"QueryPathTests\\CSS\\PseudoClassTest::testHas":0,"QueryPathTests\\CSS\\PseudoClassTest::testParent":0,"QueryPathTests\\CSS\\PseudoClassTest::testFirst":0,"QueryPathTests\\CSS\\PseudoClassTest::testLast":0,"QueryPathTests\\CSS\\PseudoClassTest::testNot":0,"QueryPathTests\\CSS\\PseudoClassTest::testEmpty":0,"QueryPathTests\\CSS\\PseudoClassTest::testOnlyChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testLastOfType":0,"QueryPathTests\\CSS\\PseudoClassTest::testFirstOftype":0,"QueryPathTests\\CSS\\PseudoClassTest::testOnlyOfType":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthLastChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #0":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #1":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #2":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #3":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #4":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #5":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #6":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #7":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #8":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #9":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #10":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #11":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #12":0,"QueryPathTests\\CSS\\PseudoClassTest::testEven":0,"QueryPathTests\\CSS\\PseudoClassTest::testOdd":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthOfTypeChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthLastOfTypeChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testRoot":0,"QueryPathTests\\CSS\\PseudoClassTest::testLt":0,"QueryPathTests\\CSS\\PseudoClassTest::testGt":0,"QueryPathTests\\CSS\\PseudoClassTest::testEq":0,"QueryPathTests\\CSS\\PseudoClassTest::testAnyLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testLocalLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testScope":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testGetMatches":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testEmptySelector":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testFailedElementNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementId":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyElementInNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementClass":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testDirectDescendant":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAttribute":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLang":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassEnabledDisabledChecked":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLink":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassXReset":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassRoot":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #0":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #1":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #2":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #3":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #4":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #5":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #6":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #7":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #8":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #9":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #10":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #11":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #12":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #13":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #14":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #15":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #16":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #17":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChildNested":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassOnlyChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassOnlyOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirstChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLastChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthLastChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirstOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthFirstOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLastOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoNthClassLastOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassEmpty":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirst":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLast":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassGT":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLT":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNTH":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFormElements":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassHeader":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassContains":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassContainsExactly":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassHas":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNot":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAdjacent":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnotherSelector":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testSibling":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyDescendant":0,"QueryPathTests\\CSS\\SelectorTest::testElement":0,"QueryPathTests\\CSS\\SelectorTest::testElementNS":0,"QueryPathTests\\CSS\\SelectorTest::testId":0,"QueryPathTests\\CSS\\SelectorTest::testClasses":0,"QueryPathTests\\CSS\\SelectorTest::testAttributes":0,"QueryPathTests\\CSS\\SelectorTest::testAttributesNS":0,"QueryPathTests\\CSS\\SelectorTest::testPseudoClasses":0,"QueryPathTests\\CSS\\SelectorTest::testPseudoElements":0,"QueryPathTests\\CSS\\SelectorTest::testCombinators":0,"QueryPathTests\\CSS\\SelectorTest::testIterator":0,"QueryPathTests\\CSS\\TokenTest::testName":0,"QueryPathTests\\CSS\\UtilTest::testRemoveQuotes":0,"QueryPathTests\\CSS\\UtilTest::testParseAnB":0,"QueryPathTests\\DOMQueryTest::testDOMQueryConstructors":0,"QueryPathTests\\DOMQueryTest::testDOMQueryHtmlConstructors":0,"QueryPathTests\\DOMQueryTest::testHtml5":0.001,"QueryPathTests\\DOMQueryTest::testInnerHtml5":0,"QueryPathTests\\DOMQueryTest::testOptionXMLEncoding":0,"QueryPathTests\\DOMQueryTest::testQPAbstractFactory":0,"QueryPathTests\\DOMQueryTest::testQPAbstractFactoryIterating":0,"QueryPathTests\\DOMQueryTest::testFailedCall":0,"QueryPathTests\\DOMQueryTest::testFailedObjectConstruction":0,"QueryPathTests\\DOMQueryTest::testFailedHTTPLoad":0.001,"QueryPathTests\\DOMQueryTest::testFailedHTTPLoadWithContext":0,"QueryPathTests\\DOMQueryTest::testFailedParseHTMLElement":0,"QueryPathTests\\DOMQueryTest::testFailedParseXMLElement":0,"QueryPathTests\\DOMQueryTest::testIgnoreParserWarnings":0,"QueryPathTests\\DOMQueryTest::testFailedParseNonMarkup":0,"QueryPathTests\\DOMQueryTest::testFailedParseEntity":0,"QueryPathTests\\DOMQueryTest::testReplaceEntitiesOption":0,"QueryPathTests\\DOMQueryTest::testFind":0,"QueryPathTests\\DOMQueryTest::testFindInPlace":0,"QueryPathTests\\DOMQueryTest::testTop":0,"QueryPathTests\\DOMQueryTest::testAttr":0,"QueryPathTests\\DOMQueryTest::testHasAttr":0,"QueryPathTests\\DOMQueryTest::testVal":0,"QueryPathTests\\DOMQueryTest::testCss":0,"QueryPathTests\\DOMQueryTest::testRemoveAttr":0,"QueryPathTests\\DOMQueryTest::testEq":0,"QueryPathTests\\DOMQueryTest::testIs":0,"QueryPathTests\\DOMQueryTest::testIndex":0,"QueryPathTests\\DOMQueryTest::testFilter":0,"QueryPathTests\\DOMQueryTest::testFilterPreg":0,"QueryPathTests\\DOMQueryTest::testFilterLambda":0,"QueryPathTests\\DOMQueryTest::testFilterCallback":0,"QueryPathTests\\DOMQueryTest::testFailedFilterCallback":0,"QueryPathTests\\DOMQueryTest::testFailedMapCallback":0,"QueryPathTests\\DOMQueryTest::testNot":0,"QueryPathTests\\DOMQueryTest::testSlice":0,"QueryPathTests\\DOMQueryTest::testMap":0,"QueryPathTests\\DOMQueryTest::testEach":0,"QueryPathTests\\DOMQueryTest::testEachOnInvalidCallback":0,"QueryPathTests\\DOMQueryTest::testEachLambda":0,"QueryPathTests\\DOMQueryTest::testDeepest":0,"QueryPathTests\\DOMQueryTest::testTag":0,"QueryPathTests\\DOMQueryTest::testAppend":0.001,"QueryPathTests\\DOMQueryTest::testAppendBadMarkup":0,"QueryPathTests\\DOMQueryTest::testAppendBadObject":0,"QueryPathTests\\DOMQueryTest::testAppendTo":0,"QueryPathTests\\DOMQueryTest::testPrepend":0,"QueryPathTests\\DOMQueryTest::testPrependTo":0,"QueryPathTests\\DOMQueryTest::testBefore":0,"QueryPathTests\\DOMQueryTest::testAfter":0,"QueryPathTests\\DOMQueryTest::testInsertBefore":0,"QueryPathTests\\DOMQueryTest::testInsertAfter":0,"QueryPathTests\\DOMQueryTest::testReplaceWith":0,"QueryPathTests\\DOMQueryTest::testReplaceAll":0,"QueryPathTests\\DOMQueryTest::testUnwrap":0,"QueryPathTests\\DOMQueryTest::testFailedUnwrap":0,"QueryPathTests\\DOMQueryTest::testWrap":0.001,"QueryPathTests\\DOMQueryTest::testWrapAll":0.001,"QueryPathTests\\DOMQueryTest::testWrapInner":0,"QueryPathTests\\DOMQueryTest::testRemove":0,"QueryPathTests\\DOMQueryTest::testHasClass":0,"QueryPathTests\\DOMQueryTest::testAddClass":0,"QueryPathTests\\DOMQueryTest::testRemoveClass":0,"QueryPathTests\\DOMQueryTest::testAdd":0,"QueryPathTests\\DOMQueryTest::testEnd":0,"QueryPathTests\\DOMQueryTest::testAndSelf":0,"QueryPathTests\\DOMQueryTest::testChildren":0,"QueryPathTests\\DOMQueryTest::testRemoveChildren":0,"QueryPathTests\\DOMQueryTest::testContents":0,"QueryPathTests\\DOMQueryTest::testNS":0,"QueryPathTests\\DOMQueryTest::testSiblings":0,"QueryPathTests\\DOMQueryTest::testHTML":0.001,"QueryPathTests\\DOMQueryTest::testInnerHTML":0,"QueryPathTests\\DOMQueryTest::testInnerXML":0,"QueryPathTests\\DOMQueryTest::testInnerXHTML":0,"QueryPathTests\\DOMQueryTest::testXML":0,"QueryPathTests\\DOMQueryTest::testXHTML":0,"QueryPathTests\\DOMQueryTest::testWriteXML":0.001,"QueryPathTests\\DOMQueryTest::testWriteXHTML":0,"QueryPathTests\\DOMQueryTest::testFailWriteXML":0,"QueryPathTests\\DOMQueryTest::testFailWriteXHTML":0,"QueryPathTests\\DOMQueryTest::testFailWriteHTML":0,"QueryPathTests\\DOMQueryTest::testWriteHTML":0,"QueryPathTests\\DOMQueryTest::testText":0,"QueryPathTests\\DOMQueryTest::testTextAfter":0,"QueryPathTests\\DOMQueryTest::testTextBefore":0,"QueryPathTests\\DOMQueryTest::testTextImplode":0,"QueryPathTests\\DOMQueryTest::testChildrenText":0,"QueryPathTests\\DOMQueryTest::testNext":0,"QueryPathTests\\DOMQueryTest::testPrev":0,"QueryPathTests\\DOMQueryTest::testNextAll":0,"QueryPathTests\\DOMQueryTest::testPrevAll":0,"QueryPathTests\\DOMQueryTest::testParent":0,"QueryPathTests\\DOMQueryTest::testClosest":0,"QueryPathTests\\DOMQueryTest::testParents":0,"QueryPathTests\\DOMQueryTest::testCloneAll":0,"QueryPathTests\\DOMQueryTest::testBranch":0,"QueryPathTests\\DOMQueryTest::testXpath":0,"QueryPathTests\\DOMQueryTest::test__clone":0,"QueryPathTests\\DOMQueryTest::testStub":0,"QueryPathTests\\DOMQueryTest::testIterator":0,"QueryPathTests\\DOMQueryTest::testModeratelySizedDocument":0.002,"QueryPathTests\\DOMQueryTest::testSize":0,"QueryPathTests\\DOMQueryTest::testCount":0,"QueryPathTests\\DOMQueryTest::testLength":0,"QueryPathTests\\DOMQueryTest::testDocument":0,"QueryPathTests\\DOMQueryTest::testDetach":0,"QueryPathTests\\DOMQueryTest::testAttach":0,"QueryPathTests\\DOMQueryTest::testEmptyElement":0,"QueryPathTests\\DOMQueryTest::testHas":0,"QueryPathTests\\DOMQueryTest::testNextUntil":0,"QueryPathTests\\DOMQueryTest::testPrevUntil":0,"QueryPathTests\\DOMQueryTest::testEven":0,"QueryPathTests\\DOMQueryTest::testOdd":0,"QueryPathTests\\DOMQueryTest::testFirst":0,"QueryPathTests\\DOMQueryTest::testFirstChild":0,"QueryPathTests\\DOMQueryTest::testLast":0,"QueryPathTests\\DOMQueryTest::testLastChild":0,"QueryPathTests\\DOMQueryTest::testParentsUntil":0,"QueryPathTests\\DOMQueryTest::testSort":0,"QueryPathTests\\DOMQueryTest::testRegressionFindOptimizations":0,"QueryPathTests\\DOMQueryTest::testDataURL":0,"QueryPathTests\\DOMQueryTest::testEncodeDataURL":0,"QueryPathTests\\EntitiesTest::testReplaceEntity":0,"QueryPathTests\\EntitiesTest::testReplaceAllEntities":0,"QueryPathTests\\EntitiesTest::testReplaceHexEntities":0,"QueryPathTests\\EntitiesTest::testQPEntityReplacement":0,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-docx-parser\"":0.075,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-manipulation-filter-and-retrieval\"":0.046,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-odt-parser\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-html-document\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-svg-document\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-xml-document\"":0.048,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"generating-rss-feed\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"hello-world\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"iterating-over-matches\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"parsing-php-source\"":0.05,"QueryPathTests\\ExamplesTest::testNetworkExampleListIsAccurate":0,"QueryPathTests\\ExamplesTest::testEveryExampleDirectoryIsRunnable":0,"QueryPathTests\\Extension\\FormatTest::it_formats_tag_text_node":0,"QueryPathTests\\Extension\\FormatTest::it_formats_attribute":0,"QueryPathTests\\Extension\\QPXMLTest::testCDATA":0,"QueryPathTests\\Extension\\QPXMLTest::testComment":0,"QueryPathTests\\Extension\\QPXMLTest::testProcessingInstruction":0,"QueryPathTests\\Extension\\QPXSLTest::testXSLT":0,"QueryPathTests\\ExtensionTest::testExtensions":0,"QueryPathTests\\ExtensionTest::testHasExtension":0,"QueryPathTests\\ExtensionTest::testStubToe":0,"QueryPathTests\\ExtensionTest::testStuble":0,"QueryPathTests\\ExtensionTest::testNoRegistry":0,"QueryPathTests\\ExtensionTest::testExtend":0,"QueryPathTests\\ExtensionTest::testAutoloadExtensions":0,"QueryPathTests\\ExtensionTest::testCallFailure":0,"QueryPathTests\\OptionsTest::testOptions":0,"QueryPathTests\\OptionsTest::testQPOverrideOrder":0,"QueryPathTests\\OptionsTest::testQPHas":0,"QueryPathTests\\OptionsTest::testQPMerge":0,"QueryPathTests\\QueryPathIteratorTest::testCurrent":0,"QueryPathTests\\QueryPathTest::testWith":0,"QueryPathTests\\QueryPathTest::testWithHTML":0,"QueryPathTests\\QueryPathTest::testWithHTML5":0,"QueryPathTests\\QueryPathTest::testWithXML":0,"QueryPathTests\\QueryPathTest::testEnable":0,"QueryPathTests\\XMLIshTest::testXMLishMock":0,"QueryPathTests\\XMLIshTest::testXMLishWithBrokenHTML":0}} \ No newline at end of file diff --git a/tests/Issues/Issue49Test.php b/tests/Issues/Issue49Test.php index ceabcdc..fcc5750 100644 --- a/tests/Issues/Issue49Test.php +++ b/tests/Issues/Issue49Test.php @@ -55,7 +55,7 @@ public function testCheckingForMatchingTextInputs(): void $this->assertTrue($q->find('[name="text2"]')->is(':text')); /* contents() here holds the two elements, not text nodes */ - $firstInput = $q->find('div')->contents()->eq(0); + $firstInput = $q->contents()->eq(0); $this->assertTrue($firstInput->is(':text')); } @@ -189,13 +189,20 @@ public function testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow */ public function testMixedNodeMatchSetStillMatchesItsElements(): void { - $contents = html5qp('
SampleChild
', 'div')->contents(); + $contents = html5qp('
SampleChild
', 'div') + ->contents(); + /* The set holds a text node and an element; neither may cause a fatal. */ $this->assertCount(2, $contents); - $this->assertSame(['s'], $this->ids($contents->find('span'))); - $this->assertSame(['s'], $this->ids($contents->find('.x'))); - $this->assertSame(['s'], $this->ids($contents->find('#s'))); + + /* find() reaches the descendants of the elements in the set */ + $this->assertSame(['e'], $this->ids($contents->find('em'))); + $this->assertSame(['e'], $this->ids($contents->find('#e'))); + + /* filter() and is() ask about the elements in the set itself */ $this->assertSame(['s'], $this->ids($contents->filter('span'))); + $this->assertSame(['s'], $this->ids($contents->filter('.x'))); + $this->assertSame(['s'], $this->ids($contents->filter('#s'))); $this->assertTrue($contents->is('.x')); } } From 6f167ad8d326af75d2ac972ce31576eb6663d519 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Sat, 22 Aug 2026 04:56:16 +1000 Subject: [PATCH 5/6] Share the :text rule between the engines, and tidy the diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule — an input whose type is absent or case-insensitively "text" — was spelled out once per engine. The two are meant to agree, which is why this PR has a test asserting they do; sharing the definition is what actually keeps them agreeing. Util is the established home for this: 4.1.0 moved parseAnB() there for the same reason. Also in this commit, none of it behavioural: - Drop .phpunit.result.cache, which was committed despite being in .gitignore. - Restore the six blank lines the diff had stripped from released CHANGELOG sections. All five open PRs edit that file, so unrelated whitespace churn in it buys four conflicts for nothing. - Record the find('*') self-match change in the CHANGELOG. It was needed to make is(':text') work on an element under the current is(), but it is a behaviour change that was going in unmentioned, and #73 supersedes it. Co-Authored-By: Claude Opus 5 (1M context) --- .phpunit.result.cache | 1 - CHANGELOG.md | 3 +++ src/CSS/DOMTraverser/PseudoClass.php | 11 +---------- src/CSS/DOMTraverser/Util.php | 23 +++++++++++++++++++++++ src/CSS/QueryPathEventHandler.php | 9 ++------- 5 files changed, 29 insertions(+), 18 deletions(-) delete mode 100644 .phpunit.result.cache diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 851153e..0000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"QueryPathTests\\DOMQueryTest::testFilterLambda":1,"QueryPathTests\\DOMQueryTest::testEachLambda":1},"times":{"QueryPathTests\\Issue49Test::testCheckingForMatchingTextInputs":0.004,"QueryPathTests\\Issue49Test::testCheckingForEmptyTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorOnlyMatchesTextInputs":0,"QueryPathTests\\Issue49Test::testTextSelectorMatchesTheInputItself":0.001,"QueryPathTests\\Issue49Test::testTextSelectorInTheLegacyEngine":0.001,"QueryPathTests\\Issue49Test::testSelectorsAgainstATextNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstACommentNodeDoNotThrow":0,"QueryPathTests\\Issue49Test::testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow":0,"QueryPathTests\\Issue49Test::testMixedNodeMatchSetStillMatchesItsElements":0,"QueryPathTests\\CSS\\DOMTraverserTest::testConstructor":0,"QueryPathTests\\CSS\\DOMTraverserTest::testFind":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatches":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchElement":0.001,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchAttributes":0.002,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchId":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchClasses":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchPseudoClasses":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMatchPseudoElements":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineAdjacent":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineSibling":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineDirectDescendant":0,"QueryPathTests\\CSS\\DOMTraverserTest::testCombineAnyDescendant":0,"QueryPathTests\\CSS\\DOMTraverserTest::testMultipleSelectors":0,"QueryPathTests\\CSS\\ParserTest::testElementID":0.003,"QueryPathTests\\CSS\\ParserTest::testElement":0,"QueryPathTests\\CSS\\ParserTest::testElementNS":0,"QueryPathTests\\CSS\\ParserTest::testAnyElement":0,"QueryPathTests\\CSS\\ParserTest::testAnyElementInNS":0,"QueryPathTests\\CSS\\ParserTest::testElementClass":0,"QueryPathTests\\CSS\\ParserTest::testPseudoClass":0,"QueryPathTests\\CSS\\ParserTest::testPseudoElement":0,"QueryPathTests\\CSS\\ParserTest::testDirectDescendant":0,"QueryPathTests\\CSS\\ParserTest::testAnyDescendant":0,"QueryPathTests\\CSS\\ParserTest::testAdjacent":0,"QueryPathTests\\CSS\\ParserTest::testSibling":0,"QueryPathTests\\CSS\\ParserTest::testAnotherSelector":0,"QueryPathTests\\CSS\\ParserTest::testIllegalAttribute":0,"QueryPathTests\\CSS\\ParserTest::testAttribute":0.001,"QueryPathTests\\CSS\\ParserTest::testAttributeNS":0,"QueryPathTests\\CSS\\ParserTest::testIllegalCombinators1":0,"QueryPathTests\\CSS\\ParserTest::testIllegalCombinators2":0,"QueryPathTests\\CSS\\ParserTest::testIllegalID":0,"QueryPathTests\\CSS\\ParserTest::testElementNSClassAndAttribute":0,"QueryPathTests\\CSS\\ParserTest::testAllCombo":0,"QueryPathTests\\CSS\\PseudoClassTest::testUnknownPseudoClass":0,"QueryPathTests\\CSS\\PseudoClassTest::testLang":0,"QueryPathTests\\CSS\\PseudoClassTest::testLangNS":0,"QueryPathTests\\CSS\\PseudoClassTest::testFormType":0,"QueryPathTests\\CSS\\PseudoClassTest::testHasAttribute":0,"QueryPathTests\\CSS\\PseudoClassTest::testHeader":0,"QueryPathTests\\CSS\\PseudoClassTest::testContains":0,"QueryPathTests\\CSS\\PseudoClassTest::testContainsExactly":0,"QueryPathTests\\CSS\\PseudoClassTest::testHas":0,"QueryPathTests\\CSS\\PseudoClassTest::testParent":0,"QueryPathTests\\CSS\\PseudoClassTest::testFirst":0,"QueryPathTests\\CSS\\PseudoClassTest::testLast":0,"QueryPathTests\\CSS\\PseudoClassTest::testNot":0,"QueryPathTests\\CSS\\PseudoClassTest::testEmpty":0,"QueryPathTests\\CSS\\PseudoClassTest::testOnlyChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testLastOfType":0,"QueryPathTests\\CSS\\PseudoClassTest::testFirstOftype":0,"QueryPathTests\\CSS\\PseudoClassTest::testOnlyOfType":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthLastChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #0":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #1":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #2":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #3":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #4":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #5":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #6":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #7":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #8":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #9":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #10":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #11":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthChild with data set #12":0,"QueryPathTests\\CSS\\PseudoClassTest::testEven":0,"QueryPathTests\\CSS\\PseudoClassTest::testOdd":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthOfTypeChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testNthLastOfTypeChild":0,"QueryPathTests\\CSS\\PseudoClassTest::testLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testRoot":0,"QueryPathTests\\CSS\\PseudoClassTest::testLt":0,"QueryPathTests\\CSS\\PseudoClassTest::testGt":0,"QueryPathTests\\CSS\\PseudoClassTest::testEq":0,"QueryPathTests\\CSS\\PseudoClassTest::testAnyLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testLocalLink":0,"QueryPathTests\\CSS\\PseudoClassTest::testScope":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testGetMatches":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testEmptySelector":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testFailedElementNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementId":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyElementInNS":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testElementClass":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testDirectDescendant":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAttribute":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLang":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassEnabledDisabledChecked":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLink":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassXReset":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassRoot":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #0":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #1":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #2":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #3":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #4":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #5":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #6":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #7":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #8":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #9":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #10":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #11":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #12":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #13":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #14":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #15":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #16":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChild with data set #17":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthChildNested":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassOnlyChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassOnlyOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirstChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLastChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthLastChild":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirstOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthFirstOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLastOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoNthClassLastOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassEmpty":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFirst":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLast":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassGT":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassLT":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNTH":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNthOfType":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassFormElements":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassHeader":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassContains":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassContainsExactly":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassHas":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoClassNot":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testPseudoElement":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAdjacent":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnotherSelector":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testSibling":0,"QueryPathTests\\CSS\\QueryPathEventHandlerTest::testAnyDescendant":0,"QueryPathTests\\CSS\\SelectorTest::testElement":0,"QueryPathTests\\CSS\\SelectorTest::testElementNS":0,"QueryPathTests\\CSS\\SelectorTest::testId":0,"QueryPathTests\\CSS\\SelectorTest::testClasses":0,"QueryPathTests\\CSS\\SelectorTest::testAttributes":0,"QueryPathTests\\CSS\\SelectorTest::testAttributesNS":0,"QueryPathTests\\CSS\\SelectorTest::testPseudoClasses":0,"QueryPathTests\\CSS\\SelectorTest::testPseudoElements":0,"QueryPathTests\\CSS\\SelectorTest::testCombinators":0,"QueryPathTests\\CSS\\SelectorTest::testIterator":0,"QueryPathTests\\CSS\\TokenTest::testName":0,"QueryPathTests\\CSS\\UtilTest::testRemoveQuotes":0,"QueryPathTests\\CSS\\UtilTest::testParseAnB":0,"QueryPathTests\\DOMQueryTest::testDOMQueryConstructors":0,"QueryPathTests\\DOMQueryTest::testDOMQueryHtmlConstructors":0,"QueryPathTests\\DOMQueryTest::testHtml5":0.001,"QueryPathTests\\DOMQueryTest::testInnerHtml5":0,"QueryPathTests\\DOMQueryTest::testOptionXMLEncoding":0,"QueryPathTests\\DOMQueryTest::testQPAbstractFactory":0,"QueryPathTests\\DOMQueryTest::testQPAbstractFactoryIterating":0,"QueryPathTests\\DOMQueryTest::testFailedCall":0,"QueryPathTests\\DOMQueryTest::testFailedObjectConstruction":0,"QueryPathTests\\DOMQueryTest::testFailedHTTPLoad":0.001,"QueryPathTests\\DOMQueryTest::testFailedHTTPLoadWithContext":0,"QueryPathTests\\DOMQueryTest::testFailedParseHTMLElement":0,"QueryPathTests\\DOMQueryTest::testFailedParseXMLElement":0,"QueryPathTests\\DOMQueryTest::testIgnoreParserWarnings":0,"QueryPathTests\\DOMQueryTest::testFailedParseNonMarkup":0,"QueryPathTests\\DOMQueryTest::testFailedParseEntity":0,"QueryPathTests\\DOMQueryTest::testReplaceEntitiesOption":0,"QueryPathTests\\DOMQueryTest::testFind":0,"QueryPathTests\\DOMQueryTest::testFindInPlace":0,"QueryPathTests\\DOMQueryTest::testTop":0,"QueryPathTests\\DOMQueryTest::testAttr":0,"QueryPathTests\\DOMQueryTest::testHasAttr":0,"QueryPathTests\\DOMQueryTest::testVal":0,"QueryPathTests\\DOMQueryTest::testCss":0,"QueryPathTests\\DOMQueryTest::testRemoveAttr":0,"QueryPathTests\\DOMQueryTest::testEq":0,"QueryPathTests\\DOMQueryTest::testIs":0,"QueryPathTests\\DOMQueryTest::testIndex":0,"QueryPathTests\\DOMQueryTest::testFilter":0,"QueryPathTests\\DOMQueryTest::testFilterPreg":0,"QueryPathTests\\DOMQueryTest::testFilterLambda":0,"QueryPathTests\\DOMQueryTest::testFilterCallback":0,"QueryPathTests\\DOMQueryTest::testFailedFilterCallback":0,"QueryPathTests\\DOMQueryTest::testFailedMapCallback":0,"QueryPathTests\\DOMQueryTest::testNot":0,"QueryPathTests\\DOMQueryTest::testSlice":0,"QueryPathTests\\DOMQueryTest::testMap":0,"QueryPathTests\\DOMQueryTest::testEach":0,"QueryPathTests\\DOMQueryTest::testEachOnInvalidCallback":0,"QueryPathTests\\DOMQueryTest::testEachLambda":0,"QueryPathTests\\DOMQueryTest::testDeepest":0,"QueryPathTests\\DOMQueryTest::testTag":0,"QueryPathTests\\DOMQueryTest::testAppend":0.001,"QueryPathTests\\DOMQueryTest::testAppendBadMarkup":0,"QueryPathTests\\DOMQueryTest::testAppendBadObject":0,"QueryPathTests\\DOMQueryTest::testAppendTo":0,"QueryPathTests\\DOMQueryTest::testPrepend":0,"QueryPathTests\\DOMQueryTest::testPrependTo":0,"QueryPathTests\\DOMQueryTest::testBefore":0,"QueryPathTests\\DOMQueryTest::testAfter":0,"QueryPathTests\\DOMQueryTest::testInsertBefore":0,"QueryPathTests\\DOMQueryTest::testInsertAfter":0,"QueryPathTests\\DOMQueryTest::testReplaceWith":0,"QueryPathTests\\DOMQueryTest::testReplaceAll":0,"QueryPathTests\\DOMQueryTest::testUnwrap":0,"QueryPathTests\\DOMQueryTest::testFailedUnwrap":0,"QueryPathTests\\DOMQueryTest::testWrap":0.001,"QueryPathTests\\DOMQueryTest::testWrapAll":0.001,"QueryPathTests\\DOMQueryTest::testWrapInner":0,"QueryPathTests\\DOMQueryTest::testRemove":0,"QueryPathTests\\DOMQueryTest::testHasClass":0,"QueryPathTests\\DOMQueryTest::testAddClass":0,"QueryPathTests\\DOMQueryTest::testRemoveClass":0,"QueryPathTests\\DOMQueryTest::testAdd":0,"QueryPathTests\\DOMQueryTest::testEnd":0,"QueryPathTests\\DOMQueryTest::testAndSelf":0,"QueryPathTests\\DOMQueryTest::testChildren":0,"QueryPathTests\\DOMQueryTest::testRemoveChildren":0,"QueryPathTests\\DOMQueryTest::testContents":0,"QueryPathTests\\DOMQueryTest::testNS":0,"QueryPathTests\\DOMQueryTest::testSiblings":0,"QueryPathTests\\DOMQueryTest::testHTML":0.001,"QueryPathTests\\DOMQueryTest::testInnerHTML":0,"QueryPathTests\\DOMQueryTest::testInnerXML":0,"QueryPathTests\\DOMQueryTest::testInnerXHTML":0,"QueryPathTests\\DOMQueryTest::testXML":0,"QueryPathTests\\DOMQueryTest::testXHTML":0,"QueryPathTests\\DOMQueryTest::testWriteXML":0.001,"QueryPathTests\\DOMQueryTest::testWriteXHTML":0,"QueryPathTests\\DOMQueryTest::testFailWriteXML":0,"QueryPathTests\\DOMQueryTest::testFailWriteXHTML":0,"QueryPathTests\\DOMQueryTest::testFailWriteHTML":0,"QueryPathTests\\DOMQueryTest::testWriteHTML":0,"QueryPathTests\\DOMQueryTest::testText":0,"QueryPathTests\\DOMQueryTest::testTextAfter":0,"QueryPathTests\\DOMQueryTest::testTextBefore":0,"QueryPathTests\\DOMQueryTest::testTextImplode":0,"QueryPathTests\\DOMQueryTest::testChildrenText":0,"QueryPathTests\\DOMQueryTest::testNext":0,"QueryPathTests\\DOMQueryTest::testPrev":0,"QueryPathTests\\DOMQueryTest::testNextAll":0,"QueryPathTests\\DOMQueryTest::testPrevAll":0,"QueryPathTests\\DOMQueryTest::testParent":0,"QueryPathTests\\DOMQueryTest::testClosest":0,"QueryPathTests\\DOMQueryTest::testParents":0,"QueryPathTests\\DOMQueryTest::testCloneAll":0,"QueryPathTests\\DOMQueryTest::testBranch":0,"QueryPathTests\\DOMQueryTest::testXpath":0,"QueryPathTests\\DOMQueryTest::test__clone":0,"QueryPathTests\\DOMQueryTest::testStub":0,"QueryPathTests\\DOMQueryTest::testIterator":0,"QueryPathTests\\DOMQueryTest::testModeratelySizedDocument":0.002,"QueryPathTests\\DOMQueryTest::testSize":0,"QueryPathTests\\DOMQueryTest::testCount":0,"QueryPathTests\\DOMQueryTest::testLength":0,"QueryPathTests\\DOMQueryTest::testDocument":0,"QueryPathTests\\DOMQueryTest::testDetach":0,"QueryPathTests\\DOMQueryTest::testAttach":0,"QueryPathTests\\DOMQueryTest::testEmptyElement":0,"QueryPathTests\\DOMQueryTest::testHas":0,"QueryPathTests\\DOMQueryTest::testNextUntil":0,"QueryPathTests\\DOMQueryTest::testPrevUntil":0,"QueryPathTests\\DOMQueryTest::testEven":0,"QueryPathTests\\DOMQueryTest::testOdd":0,"QueryPathTests\\DOMQueryTest::testFirst":0,"QueryPathTests\\DOMQueryTest::testFirstChild":0,"QueryPathTests\\DOMQueryTest::testLast":0,"QueryPathTests\\DOMQueryTest::testLastChild":0,"QueryPathTests\\DOMQueryTest::testParentsUntil":0,"QueryPathTests\\DOMQueryTest::testSort":0,"QueryPathTests\\DOMQueryTest::testRegressionFindOptimizations":0,"QueryPathTests\\DOMQueryTest::testDataURL":0,"QueryPathTests\\DOMQueryTest::testEncodeDataURL":0,"QueryPathTests\\EntitiesTest::testReplaceEntity":0,"QueryPathTests\\EntitiesTest::testReplaceAllEntities":0,"QueryPathTests\\EntitiesTest::testReplaceHexEntities":0,"QueryPathTests\\EntitiesTest::testQPEntityReplacement":0,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-docx-parser\"":0.075,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-manipulation-filter-and-retrieval\"":0.046,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"basic-odt-parser\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-html-document\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-svg-document\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"create-xml-document\"":0.048,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"generating-rss-feed\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"hello-world\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"iterating-over-matches\"":0.05,"QueryPathTests\\ExamplesTest::testExampleRunsCleanly with data set \"parsing-php-source\"":0.05,"QueryPathTests\\ExamplesTest::testNetworkExampleListIsAccurate":0,"QueryPathTests\\ExamplesTest::testEveryExampleDirectoryIsRunnable":0,"QueryPathTests\\Extension\\FormatTest::it_formats_tag_text_node":0,"QueryPathTests\\Extension\\FormatTest::it_formats_attribute":0,"QueryPathTests\\Extension\\QPXMLTest::testCDATA":0,"QueryPathTests\\Extension\\QPXMLTest::testComment":0,"QueryPathTests\\Extension\\QPXMLTest::testProcessingInstruction":0,"QueryPathTests\\Extension\\QPXSLTest::testXSLT":0,"QueryPathTests\\ExtensionTest::testExtensions":0,"QueryPathTests\\ExtensionTest::testHasExtension":0,"QueryPathTests\\ExtensionTest::testStubToe":0,"QueryPathTests\\ExtensionTest::testStuble":0,"QueryPathTests\\ExtensionTest::testNoRegistry":0,"QueryPathTests\\ExtensionTest::testExtend":0,"QueryPathTests\\ExtensionTest::testAutoloadExtensions":0,"QueryPathTests\\ExtensionTest::testCallFailure":0,"QueryPathTests\\OptionsTest::testOptions":0,"QueryPathTests\\OptionsTest::testQPOverrideOrder":0,"QueryPathTests\\OptionsTest::testQPHas":0,"QueryPathTests\\OptionsTest::testQPMerge":0,"QueryPathTests\\QueryPathIteratorTest::testCurrent":0,"QueryPathTests\\QueryPathTest::testWith":0,"QueryPathTests\\QueryPathTest::testWithHTML":0,"QueryPathTests\\QueryPathTest::testWithHTML5":0,"QueryPathTests\\QueryPathTest::testWithXML":0,"QueryPathTests\\QueryPathTest::testEnable":0,"QueryPathTests\\XMLIshTest::testXMLishMock":0,"QueryPathTests\\XMLIshTest::testXMLishWithBrokenHTML":0}} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b8f0b..8e514ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ QueryPath Changelog or processing instruction). Those nodes now simply do not match, instead of calling element-only DOM methods on them. - Fix the `:text` pseudo-class so it matches jQuery: it selects `input` elements whose `type` attribute is absent or is `text` (case-insensitively). It never indicated, and still does not indicate, whether a node is a text node. +- `find('*')` now matches the nodes in the match set as well as their descendants, so that a selector can be tested + against an element already in hand. Note that #73 replaces this with jQuery's descendant-only `find()`; this entry + is provisional and should be dropped if that lands first. # 4.1.0 diff --git a/src/CSS/DOMTraverser/PseudoClass.php b/src/CSS/DOMTraverser/PseudoClass.php index 567d074..9083c7d 100644 --- a/src/CSS/DOMTraverser/PseudoClass.php +++ b/src/CSS/DOMTraverser/PseudoClass.php @@ -246,16 +246,7 @@ protected function lang($node, $value) */ protected function isTextInput($node): bool { - if (strtolower($node->localName) !== 'input') { - return false; - } - - // An input with no type attribute defaults to a text input. - if (! $node->hasAttribute('type')) { - return true; - } - - return strtolower($node->getAttribute('type')) === 'text'; + return Util::isTextInput($node); } /** diff --git a/src/CSS/DOMTraverser/Util.php b/src/CSS/DOMTraverser/Util.php index 9a89e99..c7e873b 100644 --- a/src/CSS/DOMTraverser/Util.php +++ b/src/CSS/DOMTraverser/Util.php @@ -516,4 +516,27 @@ public static function parseAnB($rule): array return [$aVal, $bVal]; } + + /** + * Does this node match jQuery's :text pseudo-class? + * + * jQuery's :text selects input elements whose type attribute is absent, or is "text" + * regardless of case. It says nothing about whether a node is a text node. + * + * Both selector engines ask this question, so they share one answer — they are meant to + * agree, and two copies of the rule would be free to drift apart. + * + * @param mixed $node + * + * @return bool + */ + public static function isTextInput($node): bool + { + if (! $node instanceof DOMElement || strtolower($node->localName) !== 'input') { + return false; + } + + // An input with no type attribute defaults to a text input. + return ! $node->hasAttribute('type') || strtolower($node->getAttribute('type')) === 'text'; + } } diff --git a/src/CSS/QueryPathEventHandler.php b/src/CSS/QueryPathEventHandler.php index 76a1802..bbb73c9 100644 --- a/src/CSS/QueryPathEventHandler.php +++ b/src/CSS/QueryPathEventHandler.php @@ -372,13 +372,8 @@ protected function textInput() $found = new SplObjectStorage(); $matches = $this->candidateList(); foreach ($matches as $item) { - if (strtolower($item->localName) !== 'input') { - continue; - } - - // An input with no type attribute defaults to a text input. - if (! $item->hasAttribute('type') || strtolower($item->getAttribute('type')) === 'text') { - $found->attach($item); + if (Util::isTextInput($item)) { + $found->offsetSet($item); } } From 745a4e2511800cbf7a0b95ecd9a113be2f339d77 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Mon, 24 Aug 2026 20:19:10 +1000 Subject: [PATCH 6/6] Fix the non-element crash at the boundary rather than at each use site The guards were spread over seven call sites, three of which no caller could reach: PseudoClass::elementMatches() and Util::matchesAttribute[NS]() are only ever entered through matchAttributes(DOMElement $node, ...) and matchPseudoClasses(DOMElement $node, ...), whose type hints already say what the guards were re-checking. Those three are gone. Every non-element that reaches the traverser arrives through the initial match set, so the three initialMatchOn* shortcuts are the boundary and the only place the check belongs. The reasoning is stated once, on initialMatch(), instead of six times in six different wordings. combineDirectDescendant() was reaching matchesSimpleSelector() with the document itself whenever the left side of "a > b" matched the root element, which is why the signatures had been widened from DOMElement to DOMNode. It now skips non-elements the way combineAdjacent(), combineSibling() and combineAnyDescendant() already did, so matchesSelector(), matchesSimpleSelector() and combine() keep their DOMElement hints. Those are public and cannot be re-narrowed later without a break, and a caller passing a text node is better served by a TypeError at the boundary than a silent false from four frames in. PseudoClass::isTextInput() was a protected method whose body was a single call to Util::isTextInput(), carrying a copy of that method's docblock. The neighbouring switch arms call Util::matchesAttribute() directly; ':text' now does the same. Fold the three near-duplicate "selectors against a node do not throw" tests into one loop over a fixture holding all four node kinds. The copies had drifted -- only the text-node one checked ':first-child' and a descendant selector, only the comment one checked ':text' -- so the shared battery is the union of what they each asserted, and every kind is now held to the same standard. Assertions go from 46 to 91. --- CHANGELOG.md | 12 +-- src/CSS/DOMTraverser.php | 40 ++++----- src/CSS/DOMTraverser/PseudoClass.php | 28 +----- src/CSS/DOMTraverser/Util.php | 11 --- tests/Issues/Issue49Test.php | 129 ++++++++++++--------------- 5 files changed, 77 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e514ea..c9c4dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,10 @@ QueryPath Changelog - Add `composer run test:examples` (and `test:examples:network`) to run the examples locally - Fix processing instructions gaining an extra `?` each time an HTML-parsed document was serialized, so `` came back out of `html()`, `innerHTML()`, `innerXML()`, `innerXHTML()`, `xml()`, `html5()`, `innerHTML5()`, and `writeXML()` as ``. libxml's HTML parser keeps the closing `?` as part of the node's data, unlike its XML parser and the Masterminds HTML5 parser, so QueryPath now strips it on load. `DOMProcessingInstruction::$data` consequently no longer has a stray `?` on the end for documents read with `htmlqp()` or `qp()` on an `.html` file. Note that this normalisation applies only to documents QueryPath parses itself, and that taking the underlying `DOMDocument` out of QueryPath and calling libxml's own `saveHTML()` on it will emit `` without the terminator, since libxml's HTML serializer never adds one - Add `QueryPath\Document`, a `DOMDocument` subclass QueryPath parses into. The type is how QueryPath records that a document's processing instruction data does not carry the closing `?`, which cannot be determined by inspecting the document afterwards. It travels with the document, so every route to a second `DOMQuery` over one document -- iteration, `add()`, `remove()`, `replaceAll()`, `branch()`, `QueryPath::with()`, and the bundled extensions -- serializes it correctly. A `DOMDocument` supplied by the caller is a plain `DOMDocument`, makes no such promise, and is still serialized exactly as it was handed over - -- Fix fatal error when running a CSS selector against a match set that contains non-element nodes (text, comment, CDATA - or processing instruction). Those nodes now simply do not match, instead of calling element-only DOM methods on them. -- Fix the `:text` pseudo-class so it matches jQuery: it selects `input` elements whose `type` attribute is absent or is - `text` (case-insensitively). It never indicated, and still does not indicate, whether a node is a text node. -- `find('*')` now matches the nodes in the match set as well as their descendants, so that a selector can be tested - against an element already in hand. Note that #73 replaces this with jQuery's descendant-only `find()`; this entry - is provisional and should be dropped if that lands first. +- Fix fatal error when running a CSS selector against a match set that contains non-element nodes (text, comment, CDATA or processing instruction), as `contents()` returns. Those nodes now simply do not match, instead of calling element-only DOM methods on them +- Fix `find('a > b')` throwing a `TypeError` when the left-hand side matched the root element, whose parent is the document rather than an element +- Fix the `:text` pseudo-class so it matches jQuery: it selects `input` elements whose `type` attribute is absent or is `text` (case-insensitively). It never indicated, and still does not indicate, whether a node is a text node +- `find('*')` now matches the nodes in the match set as well as their descendants, so that a selector can be tested against an element already in hand # 4.1.0 diff --git a/src/CSS/DOMTraverser.php b/src/CSS/DOMTraverser.php index f86afdc..c42662d 100644 --- a/src/CSS/DOMTraverser.php +++ b/src/CSS/DOMTraverser.php @@ -7,7 +7,6 @@ use DOMDocument; use DOMElement; -use DOMNode; use DOMNodeList; use DOMXPath; use QueryPath\CSS\DOMTraverser\Util; @@ -376,14 +375,14 @@ public function matches() * absolutely huge selectors or for versions of PHP tuned to * strictly limit recursion depth. * - * @param DOMNode $node + * @param DOMElement $node * The DOMNode to check. * @param $selector * * @return boolean * A boolean TRUE if the node matches, false otherwise. */ - public function matchesSelector(DOMNode $node, $selector) + public function matchesSelector(DOMElement $node, $selector) { return $this->matchesSimpleSelector($node, $selector, 0); } @@ -395,7 +394,7 @@ public function matchesSelector(DOMNode $node, $selector) * this checks only a simple selector (plus an optional * combinator). * - * @param DOMNode $node + * @param DOMElement $node * @param $selectors * @param $index * @@ -403,16 +402,8 @@ public function matchesSelector(DOMNode $node, $selector) * A boolean TRUE if the node matches, false otherwise. * @throws NotImplementedException */ - public function matchesSimpleSelector(DOMNode $node, $selectors, $index) + public function matchesSimpleSelector(DOMElement $node, $selectors, $index) { - // Selectors only ever match elements. A match set may legitimately - // contain text, comment, CDATA or processing instruction nodes (e.g. - // from contents()), and those simply do not match -- rather than - // blowing up on the element-only DOM API used below. - if (! $node instanceof DOMElement) { - return false; - } - $selector = $selectors[$index]; // A set-level pseudo-class on a non-subject simple selector has already @@ -474,7 +465,7 @@ public function matchesSimpleSelector(DOMNode $node, $selectors, $index) * @return boolean * TRUE if the next selector(s) match. */ - public function combine(DOMNode $node, $selectors, $index) + public function combine(DOMElement $node, $selectors, $index) { $selector = $selectors[$index]; //$this->debug(implode(' ', $selectors)); @@ -574,7 +565,10 @@ public function combineSibling($node, $selectors, $index) public function combineDirectDescendant($node, $selectors, $index) { $parent = $node->parentNode; - if (empty($parent)) { + // The parent of the root element is the document, which is not an element and can never + // match. combineAdjacent(), combineSibling() and combineAnyDescendant() all skip + // non-elements the same way. + if (empty($parent) || $parent->nodeType !== XML_ELEMENT_NODE) { return false; } @@ -622,6 +616,12 @@ public function combineAnyDescendant($node, $selectors, $index) * This should only be executed when not working with * an existing match set. * + * A match set handed in from outside may legitimately contain non-element nodes -- text, + * comment, CDATA or processing instruction -- because contents() returns them. Selectors only + * ever match elements, so the three shortcuts below skip anything that is not one rather than + * calling the element-only DOM API on it. Everything downstream of here is therefore elements, + * which is why matchesSimpleSelector() and friends can keep their DOMElement type hints. + * * @param \QueryPath\CSS\SimpleSelector $selector * @param SplObjectStorage $matches * @@ -691,9 +691,7 @@ protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $ $xpath = new DOMXPath($this->dom); // Now we try to find any matching IDs. - /** @var DOMElement $node */ foreach ($matches as $node) { - // Non-element nodes have neither attributes nor element children. if (! $node instanceof DOMElement) { continue; } @@ -740,9 +738,7 @@ protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStor $xpath = new DOMXPath($this->dom); // Now we try to find any matching IDs. - /** @var DOMElement $node */ foreach ($matches as $node) { - // Non-element nodes have neither attributes nor element children. if (! $node instanceof DOMElement) { continue; } @@ -812,11 +808,7 @@ protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStor $element = '*'; } $found = $this->newMatches(); - /** @var DOMDocument|DOMElement $node */ foreach ($matches as $node) { - // Only elements and documents can contain elements. Text, comment, - // CDATA and processing instruction nodes never match, and do not - // support the element-only API used below. if (! $node instanceof DOMElement && ! $node instanceof DOMDocument) { continue; } @@ -824,7 +816,7 @@ protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStor // Capture the case where the node itself matches the element. if ($node instanceof DOMElement && ($element === '*' || $node->tagName === $element)) { - $found->attach($node); + $found->offsetSet($node); } $nl = $node->getElementsByTagName($element); if (! empty($nl) && $nl instanceof DOMNodeList) { diff --git a/src/CSS/DOMTraverser/PseudoClass.php b/src/CSS/DOMTraverser/PseudoClass.php index 9083c7d..e8f8c25 100644 --- a/src/CSS/DOMTraverser/PseudoClass.php +++ b/src/CSS/DOMTraverser/PseudoClass.php @@ -47,13 +47,6 @@ class PseudoClass */ public function elementMatches($pseudoclass, $node, $scope, $value = null) { - // Pseudo-classes are only ever satisfied by elements. Text, comment, - // CDATA and processing instruction nodes have no tag name, attributes - // or element children, so they can never match. - if (! $node instanceof DOMElement) { - return false; - } - $name = strtolower($pseudoclass); // Need to handle known pseudoclasses. switch ($name) { @@ -162,7 +155,7 @@ public function elementMatches($pseudoclass, $node, $scope, $value = null) case 'checked': return Util::matchesAttribute($node, $name); case 'text': - return $this->isTextInput($node); + return Util::isTextInput($node); case 'radio': case 'checkbox': @@ -230,25 +223,6 @@ protected function lang($node, $value) return false; } - /** - * Provides jQuery pseudoclass ':text'. - * - * This mirrors jQuery, where `:text` selects `input` elements of type text - * -- that is, an `input` whose `type` attribute is either absent (`text` is - * the default type of an `input`) or is `text`, matched case-insensitively. - * - * It does NOT indicate whether the node is a text node. - * - * @param DOMElement $node - * - * @return bool - * @see https://api.jquery.com/text-selector/ - */ - protected function isTextInput($node): bool - { - return Util::isTextInput($node); - } - /** * Provides jQuery pseudoclass ':header'. * diff --git a/src/CSS/DOMTraverser/Util.php b/src/CSS/DOMTraverser/Util.php index c7e873b..cd2f542 100644 --- a/src/CSS/DOMTraverser/Util.php +++ b/src/CSS/DOMTraverser/Util.php @@ -366,12 +366,6 @@ private static function comparePaths(array $a, array $b): int */ public static function matchesAttribute($node, $name, $value = null, $operation = EventHandler::IS_EXACTLY): bool { - // Only elements have attributes. Text, comment, CDATA and processing - // instruction nodes can never match an attribute selector. - if (! $node instanceof DOMElement) { - return false; - } - if (! $node->hasAttribute($name)) { return false; } @@ -393,11 +387,6 @@ public static function matchesAttributeNS( $value = null, $operation = EventHandler::IS_EXACTLY ) { - // Only elements have attributes. - if (! $node instanceof DOMElement) { - return false; - } - if (! $node->hasAttributeNS($nsuri, $name)) { return false; } diff --git a/tests/Issues/Issue49Test.php b/tests/Issues/Issue49Test.php index fcc5750..ad9f8bc 100644 --- a/tests/Issues/Issue49Test.php +++ b/tests/Issues/Issue49Test.php @@ -12,7 +12,7 @@ class Issue49Test extends TestCase protected const INPUT_HTML = '
' . '' . '' - . '' + . '' . '' . '' . '' @@ -20,6 +20,18 @@ class Issue49Test extends TestCase . '' . '
'; + /** + * Every kind of non-element node the DOM can hand back from contents(), as siblings. + */ + protected const MIXED_XML = '' + . '' + . 'Sample' + . '' + . '' + . '' + . 'Child' + . ''; + /** * Get the ID of every element in the match set. * @@ -87,14 +99,17 @@ public function testTextSelectorOnlyMatchesTextInputs(): void public function testTextSelectorMatchesTheInputItself(): void { - $this->assertTrue(html5qp('
', 'input')->is(':text')); - $this->assertTrue(html5qp('
', 'input')->is(':text')); - $this->assertTrue(html5qp('
', 'input')->is(':text')); - - $this->assertFalse(html5qp('
', 'input')->is(':text')); - $this->assertFalse(html5qp('
', 'input')->is(':text')); - $this->assertFalse(html5qp('
', 'textarea')->is(':text')); - $this->assertFalse(html5qp('
', 'button')->is(':text')); + $q = html5qp(self::INPUT_HTML, 'div'); + + // Explicit type="text", no type at all, and mixed-case type. + foreach (['a', 'b', 'c'] as $id) { + $this->assertTrue($q->find('#' . $id)->is(':text'), $id); + } + + // password, checkbox, submit, textarea, button. + foreach (['d', 'e', 'f', 'g', 'h'] as $id) { + $this->assertFalse($q->find('#' . $id)->is(':text'), $id); + } } /** @@ -110,77 +125,45 @@ public function testTextSelectorInTheLegacyEngine(): void } /** - * Any selector run against a match set holding a text node must return a - * sane result rather than fataling on the element-only DOM API. + * Any selector run against a match set holding a non-element node must return a sane result + * rather than fataling on the element-only DOM API. */ - public function testSelectorsAgainstATextNodeDoNotThrow(): void + public function testSelectorsAgainstNonElementNodesDoNotThrow(): void { - $textNode = html5qp('
SampleChild
', 'div') - ->contents() - ->eq(0); - - $this->assertInstanceOf(DOMText::class, $textNode->get(0)); - - $this->assertFalse($textNode->is('*')); - $this->assertFalse($textNode->is('span')); - $this->assertFalse($textNode->is('.wrap')); - $this->assertFalse($textNode->is('#wrap')); - $this->assertFalse($textNode->is('[class]')); - $this->assertFalse($textNode->is('[class="wrap"]')); - $this->assertFalse($textNode->is(':first-child')); - $this->assertFalse($textNode->is('div span')); - - $this->assertCount(0, $textNode->find('*')); - $this->assertCount(0, $textNode->find('span')); - $this->assertCount(0, $textNode->find('.wrap')); - $this->assertCount(0, $textNode->find('#wrap')); - $this->assertCount(0, $textNode->find('[class]')); - $this->assertCount(0, $textNode->filter('*')); + $contents = qp(self::MIXED_XML, 'root')->contents(); + + $kinds = [ + DOMText::class, + DOMComment::class, + DOMCdataSection::class, + DOMProcessingInstruction::class, + ]; + + foreach ($kinds as $index => $class) { + $node = $contents->eq($index); + $this->assertInstanceOf($class, $node->get(0)); + $this->assertMatchesNothing($node, $class); + } } - public function testSelectorsAgainstACommentNodeDoNotThrow(): void + /** + * The full battery, so every node kind is held to the same standard. + * + * @param \QueryPath\DOMQuery $node + * @param string $kind + */ + private function assertMatchesNothing($node, $kind): void { - $comment = html5qp('
Child
', 'div') - ->contents() - ->eq(0); - - $this->assertInstanceOf(DOMComment::class, $comment->get(0)); - - $this->assertFalse($comment->is('*')); - $this->assertFalse($comment->is('span')); - $this->assertFalse($comment->is('.wrap')); - $this->assertFalse($comment->is('#wrap')); - $this->assertFalse($comment->is('[class]')); - $this->assertFalse($comment->is(':text')); - - $this->assertCount(0, $comment->find('*')); - $this->assertCount(0, $comment->find('span')); - $this->assertCount(0, $comment->find('[class]')); - } + $selectors = ['*', 'span', '.wrap', '#wrap', '[class]', '[class="wrap"]', ':first-child', ':text', 'root span']; + foreach ($selectors as $selector) { + $this->assertFalse($node->is($selector), sprintf('%s must not match is(%s)', $kind, $selector)); + } - public function testSelectorsAgainstCdataAndProcessingInstructionNodesDoNotThrow(): void - { - $contents = qp( - 'Text', - 'root' - )->contents(); - - $cdata = $contents->eq(0); - $pi = $contents->eq(1); - - $this->assertInstanceOf(DOMCdataSection::class, $cdata->get(0)); - $this->assertInstanceOf(DOMProcessingInstruction::class, $pi->get(0)); - - foreach ([$cdata, $pi] as $node) { - $this->assertFalse($node->is('*')); - $this->assertFalse($node->is('child')); - $this->assertFalse($node->is('.c')); - $this->assertFalse($node->is('#i')); - $this->assertFalse($node->is('[class]')); - - $this->assertCount(0, $node->find('*')); - $this->assertCount(0, $node->find('child')); + foreach (['*', 'span', '.wrap', '#wrap', '[class]'] as $selector) { + $this->assertCount(0, $node->find($selector), sprintf('%s must not match find(%s)', $kind, $selector)); } + + $this->assertCount(0, $node->filter('*'), sprintf('%s must not match filter(*)', $kind)); } /**