Repository navigation
Make find() search descendants only, as jQuery does - #73
Draft
jakejackson1 wants to merge 4 commits into
Draft
jakejackson1 wants to merge 4 commits into
jakejackson1 wants to merge 4 commits into
Conversation
is(), filter(), and the selector-filtered traversal methods all asked "does this
node contain a match?" where jQuery asks "is this node a match?". They ran a
descendant search against each candidate, so a selector that matched anything
below a candidate kept that candidate.
html5qp('<p><span>foo</span></p>', 'p')->is('span'); // true, expected false
qp($xml, 'Demographics > Age > Name')->parents('Demographics');
// <Demographics>, <AmplifyReturn>, <ns1:AmplifyResponse>
qp($file, 'inner')->filter('li')->count(); // 2, expected 0
has() does what these used to do, and is unchanged: it remains the migration path
for callers that want the containment behaviour.
The single-node test is extracted into QueryPath\Helpers\NodeMatcher so the three
call paths cannot drift apart. It builds the traverser in "initialized" mode,
which treats the supplied nodes as the candidates rather than seeding a
descendant search, and leaves the scope node at its default so :scope resolves
against the document element exactly as it does in find(). Passing a candidate as
the scope node made every candidate match :scope, which is why parents(':scope')
returned every ancestor and children(':scope') returned every child.
filter() and children() filter their candidates as one set rather than one node
at a time. A per-node pass cannot evaluate a selector describing a position
within the set, because each node is the only member of its own one-element set.
parents() and parentsUntil() now return reverse document order with duplicates
removed, as jQuery does. Previously a set built from more than one starting
element was grouped by starting element.
DOMQueryTest::testFilter asserted the containment result and is rebaselined. It
is the test the original author's "fails unit tests" comment on filter() referred
to; it was the only one.
Fixes #51
Fixes #62
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
find() treated the nodes already in the match set as candidates for their own
selector, so qp($xml, '#a1')->find('a') returned #a1 itself alongside any nested
<a>. jQuery's .find() searches descendants; filter() is what asks whether the
nodes in hand match.
The document element keeps its self-match. qp() seeds the match set with the
document element rather than with the document node, so it stands in for
$(document), and without the exception qp($xml)->find('root') could never match.
This depends on #72. Before it, the selector-filtered traversal methods reach
is(), which reached find(), so they relied on the self-match to test a node
against a selector; changing find() on its own breaks nine of them. Once those
go through NodeMatcher they no longer care.
testBefore searched for the node it was standing on. Rewritten to search from
top(), which is the shape the next two assertions in that test already use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## fix-selector-matching #73 +/- ##
========================================================
Coverage ? 89.69%
Complexity ? 1366
========================================================
Files ? 27
Lines ? 3066
Branches ? 0
========================================================
Hits ? 2750
Misses ? 316
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jakejackson1
added a commit
that referenced
this pull request
Aug 21, 2026
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 <em> 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) <noreply@anthropic.com>
Only the element matcher was changed, so find('a') stopped self-matching while
find('#a1') and find('.c') still did. Three initial matchers, three separate
self-tests, and the selector you happened to write decided the semantics.
All three now apply the same rule, with the same document-element exception.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule and its four-line explanation were pasted into all three initial
matchers. They have to agree — find('#a1') matching a node that find('a') does
not is the divergence this PR exists to remove — so they should not be three
independent copies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
added a commit
that referenced
this pull request
Aug 21, 2026
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) <noreply@anthropic.com>
jakejackson1
added a commit
that referenced
this pull request
Aug 24, 2026
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 <em> 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) <noreply@anthropic.com>
jakejackson1
added a commit
that referenced
this pull request
Aug 24, 2026
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) <noreply@anthropic.com>
This was referenced Aug 24, 2026
jakejackson1
added this pull request to stack #114
October 26, 2026 00:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
find()treated the nodes already in the match set as candidates for their own selector.jQuery's
.find()searches descendants;filter()is what asks whether the nodes in hand match.Based on #72 — merge that first. This is not stylistic sequencing; see below.
The document element stays matchable
qp()seeds the match set with the document element, not the document node, so it stands infor jQuery's
$(document). Without an exception for it,qp($xml)->find('root')could nevermatch — and several long-standing tests, correctly, expect it to. So the self-match is kept for
exactly that one node:
Arguably the deeper fix is to seed with the document itself, which would remove the exception
entirely. That is a much larger change and is not attempted here.
Why this needs #72
Before #72, the selector-filtered traversal methods (
next(),siblings(),prevAll(), …) testa candidate with
QueryPath::with($node)->is($selector), andis()ran afind(). They wererelying on the self-match to ask "does this node match?" — the very question
find()should notbe answering. Applying this change to
mainalone breaks nine of them:Once #72 routes them through
NodeMatcher, they stop caring. On top of #72 this change breaksexactly one test.
The one test changed
DOMQueryTest::testBeforesearched for the node it was standing on:Rewritten to search from
top(), which is the shape the next two assertions in that same testalready use. Its intent — that
before()inserted<test/>as a preceding sibling — is unchangedand still asserted.
Relationship to #50
#50 broadened the wildcard self-match from "the root element" to "any node", to make
is()workon an element itself under the old
is(). #72 replaces thatis(), and this PR removes theself-match generally, so that hunk becomes unnecessary. Expect a conflict in
DOMTraverser::initialMatchOnElement()between #50 and this branch; resolve toward this one.Two assertions in
Issue49Testreachfind()for a node in their own set and will need the sametreatment as
testBefore.Verification
vendor/bin/phpunit— 351 tests, 1175 assertions, 0 failures (2 pre-existingcreate_functionskips)composer run lintandcomposer run lint:min-php— cleantests/QueryPath/FindDescendantOnlyTest.php(5 tests) pinning the behaviour, the document-element exception, andfilter()as the replacement🤖 Generated with Claude Code