Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/Element/ElementFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
namespace Behat\Mink\Element;

use Behat\Mink\Driver\DriverInterface;
use Behat\Mink\Selector\NamedSelectorMode;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Selector\Xpath\Manipulator;

Expand All @@ -32,12 +33,24 @@ class ElementFinder
* @var Manipulator
*/
private $xpathManipulator;
/**
* @var NamedSelectorMode::*
*/
private $namedSelectorMode;

public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler, ?Manipulator $xpathManipulator = null)
/**
* @param NamedSelectorMode::* $namedSelectorMode How the "named" selector is resolved.
*/
public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler, ?Manipulator $xpathManipulator = null, string $namedSelectorMode = NamedSelectorMode::PARTIAL_FALLBACK)
{
if (NamedSelectorMode::PARTIAL_FALLBACK !== $namedSelectorMode && NamedSelectorMode::EXACT !== $namedSelectorMode) {
throw new \InvalidArgumentException(sprintf('Unknown named selector mode "%s".', $namedSelectorMode));
}

$this->driver = $driver;
$this->selectorsHandler = $selectorsHandler;
$this->xpathManipulator = $xpathManipulator ?? new Manipulator();
$this->namedSelectorMode = $namedSelectorMode;
}

/**
Expand All @@ -49,7 +62,7 @@ public function findAll(string $selector, $locator, string $parentXpath)
{
if ('named' === $selector) {
$items = $this->findAll('named_exact', $locator, $parentXpath);
if (empty($items)) {
if (empty($items) && NamedSelectorMode::EXACT !== $this->namedSelectorMode) {
$items = $this->findAll('named_partial', $locator, $parentXpath);
}

Expand Down
36 changes: 36 additions & 0 deletions src/Selector/NamedSelectorMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Mink package.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Behat\Mink\Selector;

/**
* How the "named" selector resolves a locator.
*
* This is a placeholder for an enum, which the minimum PHP version does not allow yet.
*/
final class NamedSelectorMode
{
/**
* Try an exact match first, and fall back to a partial one when it finds nothing.
*/
public const PARTIAL_FALLBACK = 'partial_fallback';

/**
* Only match exactly, the way the "named_exact" selector does.
*/
public const EXACT = 'exact';

/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
}
8 changes: 6 additions & 2 deletions src/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

use Behat\Mink\Driver\DriverInterface;
use Behat\Mink\Element\ElementFinder;
use Behat\Mink\Selector\NamedSelectorMode;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Element\DocumentElement;

Expand Down Expand Up @@ -39,11 +40,14 @@ class Session
*/
private $selectorsHandler;

public function __construct(DriverInterface $driver, ?SelectorsHandler $selectorsHandler = null)
/**
* @param NamedSelectorMode::* $namedSelectorMode How the "named" selector is resolved.
*/
public function __construct(DriverInterface $driver, ?SelectorsHandler $selectorsHandler = null, string $namedSelectorMode = NamedSelectorMode::PARTIAL_FALLBACK)
{
$this->driver = $driver;
$this->selectorsHandler = $selectorsHandler ?? new SelectorsHandler();
$this->elementFinder = new ElementFinder($driver, $this->selectorsHandler);
$this->elementFinder = new ElementFinder($driver, $this->selectorsHandler, null, $namedSelectorMode);
$this->page = new DocumentElement($this);

$driver->setSession($this);
Expand Down
47 changes: 47 additions & 0 deletions tests/Element/ElementFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Behat\Mink\Driver\DriverInterface;
use Behat\Mink\Element\ElementFinder;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Selector\NamedSelectorMode;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Selector\Xpath\Manipulator;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -159,4 +160,50 @@ public function testNamedPartialFallback()
$this->assertEquals('element1', $results[0]->getXpath());
$this->assertEquals('element2', $results[1]->getXpath());
}

public function testNamedExactModeDoesNotFallBackToPartial()
{
$finder = new ElementFinder($this->driver, $this->selectorsHandler, $this->manipulator, NamedSelectorMode::EXACT);

$this->selectorsHandler->expects($this->once())
->method('selectorToXpath')
->with('named_exact', 'test')
->will($this->returnValue('named_xpath'));

$this->manipulator->expects($this->once())
->method('prepend')
->with('named_xpath', 'parent_xpath')
->will($this->returnValue('full_xpath'));

$this->driver->expects($this->once())
->method('find')
->with('full_xpath')
->will($this->returnValue(array()));

$this->assertEquals(array(), $finder->findAll('named', 'test', 'parent_xpath'));
}

/**
* @dataProvider provideInvalidNamedSelectorModes
*/
public function testUnknownNamedSelectorModeIsRejected(string $namedSelectorMode)
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Unknown named selector mode "%s".', $namedSelectorMode));

// @phpstan-ignore argument.type (this test exercises the runtime guard, on values the narrowed type forbids)
new ElementFinder($this->driver, $this->selectorsHandler, $this->manipulator, $namedSelectorMode);
}

/**
* @return array<string, array{string}>
*/
public static function provideInvalidNamedSelectorModes()
{
return array(
'unknown value' => array('nope'),
'empty string' => array(''),
'the selector name itself' => array('named_exact'),
);
}
}
40 changes: 40 additions & 0 deletions tests/SessionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Behat\Mink\Tests;

use Behat\Mink\Driver\DriverInterface;
use Behat\Mink\Element\ElementFinder;
use Behat\Mink\Selector\NamedSelectorMode;
use Behat\Mink\Selector\SelectorsHandler;
use Behat\Mink\Session;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -355,4 +357,42 @@ public function testMaximizeWindow()

$this->session->maximizeWindow('test');
}

public function testNamedSelectorModeIsForwardedToTheElementFinder()
{
$selectorsHandler = $this->createMock(SelectorsHandler::class);
$selectorsHandler->expects($this->once())
->method('selectorToXpath')
->with('named_exact', 'test')
->willReturn('named_xpath');

// a partial fallback would query the driver a second time
$this->driver->expects($this->once())
->method('find')
->willReturn(array());

$session = new Session($this->driver, $selectorsHandler, NamedSelectorMode::EXACT);

$this->assertSame(array(), $session->getPage()->findAll('named', 'test'));
}

public function testTheDefaultNamedSelectorModeStillFallsBackToPartial()
{
$selectorsHandler = $this->createMock(SelectorsHandler::class);
$selectorsHandler->expects($this->exactly(2))
->method('selectorToXpath')
->willReturnMap(array(
array('named_exact', 'test', 'named_xpath'),
array('named_partial', 'test', 'partial_xpath'),
));

// the exact lookup finds nothing, so the partial one is queried as well
$this->driver->expects($this->exactly(2))
->method('find')
->willReturn(array());

$session = new Session($this->driver, $selectorsHandler);

$this->assertSame(array(), $session->getPage()->findAll('named', 'test'));
}
}
Loading