Allow the named selector to skip the partial fallback - #882
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #882 +/- ##
=========================================
Coverage 98.56% 98.57%
- Complexity 389 392 +3
=========================================
Files 24 24
Lines 909 912 +3
=========================================
+ Hits 896 899 +3
Misses 13 13 ☔ View full report in Codecov by Harness. |
|
Hi @Amoifr, thanks for working on this, I'm super keen for it to get in! I had a couple of points of feedback:
|
|
I think we should rather put those constants on a dedicated class than on |
| $this->assertEquals(array(), $finder->findAll('named', 'test', 'parent_xpath')); | ||
| } | ||
|
|
||
| public function testNamedExactModeStillReturnsExactMatches() |
There was a problem hiding this comment.
Please:
- rename this test into
testNamedExactModeDoesNotFallBackToPartial - drop the
testNamedExactModeDoesNotFallBackToPartial(declared above this one)
Most of test in this class assert search-behavior and return-behavior in the same method. No need to overcomplicate this by checking both things in separate test methods.
There was a problem hiding this comment.
Done — the two tests are now a single testNamedExactModeDoesNotFallBackToPartial asserting the search behaviour and the return value together, like the rest of the class.
| public function __construct(DriverInterface $driver, ?SelectorsHandler $selectorsHandler = null) | ||
| /** | ||
| * @param ElementFinder::NAMED_* $namedMode How the "named" selector is resolved: an exact match | ||
| * falling back to a partial one, or an exact match only. |
There was a problem hiding this comment.
Maybe indentation is wrong (missing space before falling word).
There was a problem hiding this comment.
Fixed: the annotation is a single line now, so there is nothing left to misalign.
|
|
||
| public function testNamedModeIsForwardedToTheElementFinder() | ||
| { | ||
| $selectorsHandler = $this->getMockBuilder('Behat\\Mink\\Selector\\SelectorsHandler')->getMock(); |
There was a problem hiding this comment.
Please use ::class with corresponding class import instead of specifying it's FQCN as string.
There was a problem hiding this comment.
Done, SelectorsHandler::class with the import — and createMock() rather than the builder while I was at it.
|
Agree with moving new class constants into a |
a20a286 to
6a1cd24
Compare
|
Thanks all three of you. Pushed everything that is not the open question. @andriokha you are right, and it undercuts my own argument on the issue: I said
Say which and I will move them in the next push. If it is a dedicated class it also needs a name, which is @stof's point about naming the enum we cannot write. The validation is back, @andriokha, with one consequence worth stating rather than hiding. The narrow @aik099, the three review points:
Suite green at 526 tests, PHPStan clean. |
Actually, we can have the precise type in phpdoc and the runtime guard by setting |
|
I suggest going for the separate class to hold the constants. Based on the packagist installation stats, I'm considering dropping support for PHP <8.1, which would then allow turning this separate class into an actual enum before releasing it. |
6a1cd24 to
890036b
Compare
|
Done, @stof. The constants now live in I put it in the use Behat\Mink\Selector\NamedSelectorMode;
$session = new Session($driver, $selectorsHandler, NamedSelectorMode::EXACT);That also answers @andriokha's objection properly: nothing users have to name is marked Suite green at 526 tests, PHPStan clean. |
| * | ||
| * This is the historical behaviour, and stays the default. |
There was a problem hiding this comment.
| * | |
| * This is the historical behaviour, and stays the default. |
I'd suggest avoiding describing historical behavior - code comments should describe how things work now imho. Source control can give historical information.
There was a problem hiding this comment.
Applied — the note about the historical behaviour is gone, the constants only describe what they do now. Git remembers the rest.
|
|
||
| public function __construct(DriverInterface $driver, ?SelectorsHandler $selectorsHandler = null) | ||
| /** | ||
| * @param string $namedMode How the "named" selector is resolved: one of the |
There was a problem hiding this comment.
I think the advantage of switching off treatPhpDocTypesAsCertain is we can be more specific with the type here without upsetting phpstan.
| * @param string $namedMode How the "named" selector is resolved: one of the | |
| * @param NamedSelectorMode::* $namedMode How the "named" selector is resolved: one of the |
There was a problem hiding this comment.
Applied, and it corrected something I had got wrong earlier in this PR: the narrow annotation and the runtime guard do coexist fine. Both Session and ElementFinder are typed NamedSelectorMode::*.
890036b to
bb330a7
Compare
|
Both applied, thanks @andriokha. The second one corrects something I got wrong earlier in this pull request. I claimed the narrow The historical note is gone from the constant, you are right that git carries that. One thing I have left as is, so you can tell me if you would rather have it otherwise: Suite green at 526 tests, PHPStan clean. |
Just for transparency, I'm a nobody :)
Dunno if the maintainers think it's worth it, but you can always do something like the following prior to attempting to instantiate the class: $this->expectException(\Error::class); |
|
You can exclude that enum-candidate class from coverage checks by adding corresponding PHPUnit annotation/attribute in its declaration. |
bb330a7 to
e8f8bb2
Compare
|
Done, thanks @aik099. I put it on the constructor rather than on the class declaration. It is the same thing today, the constants have no executable line, but it keeps the exclusion to the one member that can genuinely never run if the class ever grows. Say the word if you would rather have it on the class. That also settles @andriokha's Suite green at 526 tests, PHPStan clean. |
|
|
||
| public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler, ?Manipulator $xpathManipulator = null) | ||
| /** | ||
| * @param string $namedMode How the "named" selector is resolved: one of the |
There was a problem hiding this comment.
| * @param string $namedMode How the "named" selector is resolved: one of the | |
| * @param NamedSelectorMode::* $namedSelectorMode |
There was a problem hiding this comment.
Applied, with one small change: the parameter is called $namedMode, not $namedSelectorMode, so I kept the existing name to avoid documenting a parameter that does not exist.
Worth knowing that the narrowed type made PHPStan fail on ElementFinderTest, where testUnknownNamedModeIsRejected deliberately passes invalid values to exercise the runtime guard. I put a targeted @phpstan-ignore argument.type on that single line with a reason. This project usually keeps its exclusions in phpstan.dist.neon, so tell me if you would rather have it there and I will move it.
There was a problem hiding this comment.
I would actually rename the parameter. What is named is the selector, not the mode.
I put a targeted
@phpstan-ignore argument.typeon that single line with a reason.
A targeted comment is fine for this case, as this is a case where we intentionally pass invalid data.
There was a problem hiding this comment.
Renamed, you're right — the mode is a property of the selector, not something named. $namedSelectorMode now, for the constructor parameter and the property on both ElementFinder and Session, with the test names following suit (testUnknownNamedSelectorModeIsRejected, testNamedSelectorModeIsForwardedToTheElementFinder, testTheDefaultNamedSelectorModeStillFallsBackToPartial).
And thanks for settling the ignore, I'll leave the targeted comment where it is.
| */ | ||
| private $xpathManipulator; | ||
| /** | ||
| * @var string one of the NamedSelectorMode constants |
There was a problem hiding this comment.
| * @var string one of the NamedSelectorMode constants | |
| * @var NamedSelectorMode::* |
There was a problem hiding this comment.
Applied. Session::__construct() was already documented as NamedSelectorMode::*, so ElementFinder was simply the odd one out. I also dropped the "one of the NamedSelectorMode constants" prose from both, since the type now says it better than the sentence did.
What is named is the selector, not the mode, so the constructor parameter and the property become $namedSelectorMode on both ElementFinder and Session, and the tests follow the same naming.
Fixes #880.
Implements the shape @aik099 settled on: an optional mode on
ElementFinderdeciding how thenamedselector resolves, and an optional mode onSessionthat forwards it. Nothing outside those two constructors changes, and both default to today's behaviour.ElementFinderstays untouched from the outside, which is the point of passing a mode rather than a finder instance: users never name a class that is marked@internal.On the tests you asked for
The mode reaching
ElementFinderis checked through behaviour rather than through a getter, since a fallback costs an extra driver query and that is observable.SessionTestcovers both directions:testNamedModeIsForwardedToTheElementFinderbuilds aSessionwithNAMED_EXACTand asserts the driver is queried once, so no partial lookup happened.testTheDefaultNamedModeStillFallsBackToPartialbuilds one without the argument and asserts the driver is queried twice.ElementFinderTestcovers each mode symmetrically, two tests per mode:testNamedFoundandtestNamedPartialFallbackfor the default,testNamedExactModeStillReturnsExactMatchesandtestNamedExactModeDoesNotFallBackToPartialfor the strict one. I checked they fail against an unmodifiedsrc/.One design note
The condition reads
self::NAMED_EXACT !== $this->namedModerather than testing for the default. It is the safer way round: a mode that is neither constant falls back to the historical behaviour instead of silently switching to strict.I first added a runtime guard rejecting unknown modes, then dropped it. PHPStan flagged the test for it, because the
@param self::NAMED_*annotation makes an invalid argument impossible statically, and this repository keeps level 8 clean without a single inline@phpstan-ignore. Inverting the condition made the guard unnecessary rather than merely unenforced.Full suite green, PHPStan clean. Point 3 of your plan shipped separately in #881.