From 0ace93972a69b487d2f3eab085699983b943a0dd Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 13 Jun 2023 11:36:26 +0200 Subject: [PATCH] Moved the search of elements to an ElementFinder This extracts the complex logic out of Element::findAll. The logic is better encapsulated and this keeps the collaborators of the element to a sane number. --- phpstan.dist.neon | 1 + src/Element/Element.php | 29 +- src/Element/ElementFinder.php | 71 ++++ src/Session.php | 38 ++- tests/Element/DocumentElementTest.php | 463 ++++++++++++++------------ tests/Element/ElementFinderTest.php | 162 +++++++++ tests/Element/ElementTest.php | 43 +-- tests/Element/NodeElementTest.php | 130 ++------ 8 files changed, 546 insertions(+), 391 deletions(-) create mode 100644 src/Element/ElementFinder.php create mode 100644 tests/Element/ElementFinderTest.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 6317062da..ef5dfa28e 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -4,6 +4,7 @@ parameters: - src - tests checkMissingIterableValueType: false + treatPhpDocTypesAsCertain: false ignoreErrors: - '#^Method Behat\\Mink\\Tests\\[^:]+Test(Case)?\:\:test\w*\(\) has no return type specified\.$#' - '#^Method Behat\\Mink\\Tests\\[^:]+Test(Case)?\:\:provide\w*\(\) has no return type specified\.$#' diff --git a/src/Element/Element.php b/src/Element/Element.php index d6bacb082..a6f249e71 100644 --- a/src/Element/Element.php +++ b/src/Element/Element.php @@ -13,7 +13,6 @@ use Behat\Mink\Driver\DriverInterface; use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Selector\SelectorsHandler; -use Behat\Mink\Selector\Xpath\Manipulator; use Behat\Mink\Session; /** @@ -36,14 +35,9 @@ abstract class Element implements ElementInterface private $driver; /** - * @var SelectorsHandler + * @var ElementFinder */ - private $selectorsHandler; - - /** - * @var Manipulator - */ - private $xpathManipulator; + private $elementFinder; /** * Initialize element. @@ -52,11 +46,10 @@ abstract class Element implements ElementInterface */ public function __construct(Session $session) { - $this->xpathManipulator = new Manipulator(); $this->session = $session; $this->driver = $session->getDriver(); - $this->selectorsHandler = $session->getSelectorsHandler(); + $this->elementFinder = $session->getElementFinder(); } /** @@ -94,7 +87,7 @@ protected function getSelectorsHandler() { @trigger_error(sprintf('The method %s is deprecated as of 1.7 and will be removed in 2.0', __METHOD__), E_USER_DEPRECATED); - return $this->selectorsHandler; + return $this->session->getSelectorsHandler(); } /** @@ -153,19 +146,7 @@ public function find($selector, $locator) */ public function findAll($selector, $locator) { - if ('named' === $selector) { - $items = $this->findAll('named_exact', $locator); - if (empty($items)) { - $items = $this->findAll('named_partial', $locator); - } - - return $items; - } - - $xpath = $this->selectorsHandler->selectorToXpath($selector, $locator); - $xpath = $this->xpathManipulator->prepend($xpath, $this->getXpath()); - - return $this->getDriver()->find($xpath); + return $this->elementFinder->findAll($selector, $locator, $this->getXpath()); } /** diff --git a/src/Element/ElementFinder.php b/src/Element/ElementFinder.php new file mode 100644 index 000000000..01e1c3255 --- /dev/null +++ b/src/Element/ElementFinder.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Behat\Mink\Element; + +use Behat\Mink\Driver\DriverInterface; +use Behat\Mink\Selector\SelectorsHandler; +use Behat\Mink\Selector\Xpath\Manipulator; + +/** + * @final + */ +class ElementFinder +{ + /** + * @var DriverInterface + */ + private $driver; + /** + * @var SelectorsHandler + */ + private $selectorsHandler; + /** + * @var Manipulator + */ + private $xpathManipulator; + + public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler = null, Manipulator $xpathManipulator = null) + { + $this->driver = $driver; + $this->selectorsHandler = $selectorsHandler ?? new SelectorsHandler(); + $this->xpathManipulator = $xpathManipulator ?? new Manipulator(); + } + + /** + * @param string|array $locator + * + * @return NodeElement[] + */ + public function findAll(string $selector, $locator, string $parentXpath) + { + if ('named' === $selector) { + $items = $this->findAll('named_exact', $locator, $parentXpath); + if (empty($items)) { + $items = $this->findAll('named_partial', $locator, $parentXpath); + } + + return $items; + } + + $xpath = $this->selectorsHandler->selectorToXpath($selector, $locator); + $xpath = $this->xpathManipulator->prepend($xpath, $parentXpath); + + return $this->driver->find($xpath); + } + + /** + * @internal + */ + public function getSelectorsHandler(): SelectorsHandler + { + return $this->selectorsHandler; + } +} diff --git a/src/Session.php b/src/Session.php index 442652d28..703ecc04e 100644 --- a/src/Session.php +++ b/src/Session.php @@ -11,6 +11,7 @@ namespace Behat\Mink; use Behat\Mink\Driver\DriverInterface; +use Behat\Mink\Element\ElementFinder; use Behat\Mink\Selector\SelectorsHandler; use Behat\Mink\Element\DocumentElement; @@ -30,27 +31,28 @@ class Session */ private $page; /** - * @var SelectorsHandler + * @var ElementFinder */ - private $selectorsHandler; + private $elementFinder; /** - * Initializes session. - * - * @param DriverInterface $driver - * @param SelectorsHandler|null $selectorsHandler + * @param ElementFinder|SelectorsHandler|null $elementFinder */ - public function __construct(DriverInterface $driver, SelectorsHandler $selectorsHandler = null) + public function __construct(DriverInterface $driver, $elementFinder = null) { - $driver->setSession($this); - - if (null === $selectorsHandler) { - $selectorsHandler = new SelectorsHandler(); + if ($elementFinder instanceof SelectorsHandler) { + $elementFinder = new ElementFinder($driver, $elementFinder); + } elseif ($elementFinder === null) { + $elementFinder = new ElementFinder($driver); + } elseif (!$elementFinder instanceof ElementFinder) { + throw new \TypeError(sprintf('Argument #2 of %s expects %s|%s|null, %s given.', __METHOD__, ElementFinder::class, SelectorsHandler::class, \is_object($elementFinder) ? get_class($elementFinder) : gettype($elementFinder))); } $this->driver = $driver; - $this->selectorsHandler = $selectorsHandler; + $this->elementFinder = $elementFinder; $this->page = new DocumentElement($this); + + $driver->setSession($this); } /** @@ -140,14 +142,24 @@ public function getPage() return $this->page; } + /** + * @internal + */ + public function getElementFinder(): ElementFinder + { + return $this->elementFinder; + } + /** * Returns selectors handler. * * @return SelectorsHandler + * + * @internal since 1.11 */ public function getSelectorsHandler() { - return $this->selectorsHandler; + return $this->elementFinder->getSelectorsHandler(); } /** diff --git a/tests/Element/DocumentElementTest.php b/tests/Element/DocumentElementTest.php index 0e307a105..18b245022 100644 --- a/tests/Element/DocumentElementTest.php +++ b/tests/Element/DocumentElementTest.php @@ -3,6 +3,7 @@ namespace Behat\Mink\Tests\Element; use Behat\Mink\Element\DocumentElement; +use Behat\Mink\Element\NodeElement; class DocumentElementTest extends ElementTest { @@ -32,389 +33,413 @@ public function testGetSession() public function testFindAll() { + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + $xpath = 'h3[a]'; $css = 'h3 > a'; - $this->driver + $this->elementFinder ->expects($this->exactly(2)) - ->method('find') - ->will($this->returnValueMap(array( - array('//html/'.$xpath, array(2, 3, 4)), - array('//html/'.$css, array(1, 2)), - ))); - - $this->selectors - ->expects($this->exactly(2)) - ->method('selectorToXpath') - ->will($this->returnValueMap(array( - array('xpath', $xpath, $xpath), - array('css', $css, $css), - ))); - - $this->assertEquals(3, count($this->document->findAll('xpath', $xpath))); - $this->assertEquals(2, count($this->document->findAll('css', $css))); + ->method('findAll') + ->willReturnMap(array( + array('xpath', $xpath, '//html', array($node1, $node2)), + array('css', $css, '//html', array()), + )); + + $this->assertSame(array($node1, $node2), $this->document->findAll('xpath', $xpath)); + $this->assertCount(0, $this->document->findAll('css', $css)); } public function testFind() { - $this->driver - ->expects($this->exactly(3)) - ->method('find') - ->with('//html/h3[a]') - ->will($this->onConsecutiveCalls(array(2, 3, 4), array(1, 2), array())); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + $node3 = $this->createStub(NodeElement::class); + $node4 = $this->createStub(NodeElement::class); $xpath = 'h3[a]'; + $xpath2 = 'h3[b]'; $css = 'h3 > a'; - $this->selectors + $this->elementFinder ->expects($this->exactly(3)) - ->method('selectorToXpath') - ->will($this->returnValueMap(array( - array('xpath', $xpath, $xpath), - array('xpath', $xpath, $xpath), - array('css', $css, $xpath), - ))); - - $this->assertEquals(2, $this->document->find('xpath', $xpath)); - $this->assertEquals(1, $this->document->find('css', $css)); - $this->assertNull($this->document->find('xpath', $xpath)); + ->method('findAll') + ->willReturnMap(array( + array('xpath', $xpath, '//html', array($node2, $node3, $node4)), + array('css', $css, '//html', array($node1, $node2)), + array('xpath', $xpath2, '//html', array()), + )); + + $this->assertSame($node2, $this->document->find('xpath', $xpath)); + $this->assertSame($node1, $this->document->find('css', $css)); + $this->assertNull($this->document->find('xpath', $xpath2)); } public function testFindField() { - $this->mockNamedFinder( - '//field', - array('field1', 'field2', 'field3'), - array('field', 'some field') - ); - - $this->assertEquals('field1', $this->document->findField('some field')); - $this->assertEquals(null, $this->document->findField('some field')); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); + + $this->assertSame($node1, $this->document->findField('some field')); + $this->assertNull($this->document->findField('some other field')); } public function testFindLink() { - $this->mockNamedFinder( - '//link', - array('link1', 'link2', 'link3'), - array('link', 'some link') - ); - - $this->assertEquals('link1', $this->document->findLink('some link')); - $this->assertEquals(null, $this->document->findLink('some link')); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('link', 'some link'), '//html', array($node1, $node2)), + array('named', array('link', 'some other link'), '//html', array()), + )); + + $this->assertSame($node1, $this->document->findLink('some link')); + $this->assertNull($this->document->findLink('some other link')); } public function testFindButton() { - $this->mockNamedFinder( - '//button', - array('button1', 'button2', 'button3'), - array('button', 'some button') - ); - - $this->assertEquals('button1', $this->document->findButton('some button')); - $this->assertEquals(null, $this->document->findButton('some button')); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('button', 'some button'), '//html', array($node1, $node2)), + array('named', array('button', 'some other button'), '//html', array()), + )); + + $this->assertSame($node1, $this->document->findButton('some button')); + $this->assertNull($this->document->findButton('some other button')); } public function testFindById() { - $xpath = '//*[@id=some-item-2]'; - - $this->mockNamedFinder($xpath, array(array('id2', 'id3'), array()), array('id', 'some-item-2')); - - $this->assertEquals('id2', $this->document->findById('some-item-2')); - $this->assertEquals(null, $this->document->findById('some-item-2')); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('id', 'some-item-1'), '//html', array($node1, $node2)), + array('named', array('id', 'some-item-2'), '//html', array()), + )); + + $this->assertSame($node1, $this->document->findById('some-item-1')); + $this->assertNull($this->document->findById('some-item-2')); } public function testHasSelector() { - $this->driver - ->expects($this->exactly(2)) - ->method('find') - ->with('//html/some xpath') - ->will($this->onConsecutiveCalls(array('id2', 'id3'), array())); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); - $this->selectors - ->expects($this->exactly(2)) - ->method('selectorToXpath') - ->with('xpath', 'some xpath') - ->will($this->returnValue('some xpath')); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('xpath', 'some xpath', '//html', array($node1, $node2)), + array('xpath', 'some other xpath', '//html', array()), + )); $this->assertTrue($this->document->has('xpath', 'some xpath')); - $this->assertFalse($this->document->has('xpath', 'some xpath')); + $this->assertFalse($this->document->has('xpath', 'some other xpath')); } public function testHasContent() { - $this->mockNamedFinder( - '//some content', - array('item1', 'item2'), - array('content', 'some content') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('content', 'some content'), '//html', array($node1, $node2)), + array('named', array('content', 'some other content'), '//html', array()), + )); $this->assertTrue($this->document->hasContent('some content')); - $this->assertFalse($this->document->hasContent('some content')); + $this->assertFalse($this->document->hasContent('some other content')); } public function testHasLink() { - $this->mockNamedFinder( - '//link', - array('link1', 'link2', 'link3'), - array('link', 'some link') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('link', 'some link'), '//html', array($node1, $node2)), + array('named', array('link', 'some other link'), '//html', array()), + )); $this->assertTrue($this->document->hasLink('some link')); - $this->assertFalse($this->document->hasLink('some link')); + $this->assertFalse($this->document->hasLink('some other link')); } public function testHasButton() { - $this->mockNamedFinder( - '//button', - array('button1', 'button2', 'button3'), - array('button', 'some button') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('button', 'some button'), '//html', array($node1, $node2)), + array('named', array('button', 'some other button'), '//html', array()), + )); $this->assertTrue($this->document->hasButton('some button')); - $this->assertFalse($this->document->hasButton('some button')); + $this->assertFalse($this->document->hasButton('some other button')); } public function testHasField() { - $this->mockNamedFinder( - '//field', - array('field1', 'field2', 'field3'), - array('field', 'some field') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->assertTrue($this->document->hasField('some field')); - $this->assertFalse($this->document->hasField('some field')); + $this->assertFalse($this->document->hasField('some other field')); } public function testHasCheckedField() { - $checkbox = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $checkbox - ->expects($this->exactly(2)) - ->method('isChecked') - ->will($this->onConsecutiveCalls(true, false)); - - $this->mockNamedFinder( - '//field', - array(array($checkbox), array(), array($checkbox)), - array('field', 'some checkbox'), - 3 - ); + $node1 = $this->createStub(NodeElement::class); + $node1->method('isChecked')->willReturn(true); + $node2 = $this->createStub(NodeElement::class); + $node2->method('isChecked')->willReturn(false); + + $this->elementFinder->expects($this->exactly(3)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some checkbox'), '//html', array($node1, $node2)), + array('named', array('field', 'some unchecked checkbox'), '//html', array($node2)), + array('named', array('field', 'some other checkbox'), '//html', array()), + )); $this->assertTrue($this->document->hasCheckedField('some checkbox')); - $this->assertFalse($this->document->hasCheckedField('some checkbox')); - $this->assertFalse($this->document->hasCheckedField('some checkbox')); + $this->assertFalse($this->document->hasCheckedField('some other checkbox')); + $this->assertFalse($this->document->hasCheckedField('some unchecked checkbox')); } public function testHasUncheckedField() { - $checkbox = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $checkbox - ->expects($this->exactly(2)) - ->method('isChecked') - ->will($this->onConsecutiveCalls(true, false)); + $node1 = $this->createStub(NodeElement::class); + $node1->method('isChecked')->willReturn(true); + $node2 = $this->createStub(NodeElement::class); + $node2->method('isChecked')->willReturn(false); + + $this->elementFinder->expects($this->exactly(3)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some checkbox'), '//html', array($node1, $node2)), + array('named', array('field', 'some unchecked checkbox'), '//html', array($node2)), + array('named', array('field', 'some other checkbox'), '//html', array()), + )); - $this->mockNamedFinder( - '//field', - array(array($checkbox), array(), array($checkbox)), - array('field', 'some checkbox'), - 3 - ); - - $this->assertFalse($this->document->hasUncheckedField('some checkbox')); $this->assertFalse($this->document->hasUncheckedField('some checkbox')); - $this->assertTrue($this->document->hasUncheckedField('some checkbox')); + $this->assertFalse($this->document->hasUncheckedField('some other checkbox')); + $this->assertTrue($this->document->hasUncheckedField('some unchecked checkbox')); } public function testHasSelect() { - $this->mockNamedFinder( - '//select', - array('select'), - array('select', 'some select field') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('select', 'some select field'), '//html', array($node1, $node2)), + array('named', array('select', 'some other select field'), '//html', array()), + )); $this->assertTrue($this->document->hasSelect('some select field')); - $this->assertFalse($this->document->hasSelect('some select field')); + $this->assertFalse($this->document->hasSelect('some other select field')); } public function testHasTable() { - $this->mockNamedFinder( - '//table', - array('table'), - array('table', 'some table') - ); + $node1 = $this->createStub(NodeElement::class); + $node2 = $this->createStub(NodeElement::class); + + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('table', 'some table'), '//html', array($node1, $node2)), + array('named', array('table', 'some other table'), '//html', array()), + )); $this->assertTrue($this->document->hasTable('some table')); - $this->assertFalse($this->document->hasTable('some table')); + $this->assertFalse($this->document->hasTable('some other table')); } public function testClickLink() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('click'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//link', - array($node), - array('link', 'some link') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('link', 'some link'), '//html', array($node1, $node2)), + array('named', array('link', 'some other link'), '//html', array()), + )); $this->document->clickLink('some link'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->clickLink('some link'); + $this->document->clickLink('some other link'); } public function testClickButton() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('press'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//button', - array($node), - array('button', 'some button') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('button', 'some button'), '//html', array($node1, $node2)), + array('named', array('button', 'some other button'), '//html', array()), + )); $this->document->pressButton('some button'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->pressButton('some button'); + $this->document->pressButton('some other button'); } public function testFillField() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('setValue') ->with('some val'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//field', - array($node), - array('field', 'some field') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->document->fillField('some field', 'some val'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->fillField('some field', 'some val'); + $this->document->fillField('some other field', 'some val'); } public function testCheckField() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('check'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//field', - array($node), - array('field', 'some field') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->document->checkField('some field'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->checkField('some field'); + $this->document->checkField('some other field'); } public function testUncheckField() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('uncheck'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//field', - array($node), - array('field', 'some field') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->document->uncheckField('some field'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->uncheckField('some field'); + $this->document->uncheckField('some other field'); } public function testSelectField() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('selectOption') ->with('option2'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//field', - array($node), - array('field', 'some field') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->document->selectFieldOption('some field', 'option2'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->selectFieldOption('some field', 'option2'); + $this->document->selectFieldOption('some other field', 'option2'); } public function testAttachFileToField() { - $node = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); - $node - ->expects($this->once()) + $node1 = $this->createMock(NodeElement::class); + $node1->expects($this->once()) ->method('attachFile') ->with('/path/to/file'); + $node2 = $this->createStub(NodeElement::class); - $this->mockNamedFinder( - '//field', - array($node), - array('field', 'some field') - ); + $this->elementFinder->expects($this->exactly(2)) + ->method('findAll') + ->willReturnMap(array( + array('named', array('field', 'some field'), '//html', array($node1, $node2)), + array('named', array('field', 'some other field'), '//html', array()), + )); $this->document->attachFileToField('some field', '/path/to/file'); $this->expectException('Behat\Mink\Exception\ElementNotFoundException'); - $this->document->attachFileToField('some field', '/path/to/file'); + $this->document->attachFileToField('some other field', '/path/to/file'); } public function testGetContent() diff --git a/tests/Element/ElementFinderTest.php b/tests/Element/ElementFinderTest.php new file mode 100644 index 000000000..97c254403 --- /dev/null +++ b/tests/Element/ElementFinderTest.php @@ -0,0 +1,162 @@ +driver = $this->createMock(DriverInterface::class); + $this->selectorsHandler = $this->createMock(SelectorsHandler::class); + $this->manipulator = $this->createMock(Manipulator::class); + + $this->finder = new ElementFinder($this->driver, $this->selectorsHandler, $this->manipulator); + } + + public function testNotFound() + { + $this->selectorsHandler->expects($this->once()) + ->method('selectorToXpath') + ->with('css', 'h3 > a') + ->will($this->returnValue('css_xpath')); + + $this->manipulator->expects($this->once()) + ->method('prepend') + ->with('css_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(), $this->finder->findAll('css', 'h3 > a', 'parent_xpath')); + } + + public function testFound() + { + $element1 = $this->createStub(NodeElement::class); + $element1->method('getXpath')->willReturn('element1'); + + $element2 = $this->createStub(NodeElement::class); + $element2->method('getXpath')->willReturn('element2'); + + $this->selectorsHandler->expects($this->once()) + ->method('selectorToXpath') + ->with('css', 'h3 > a') + ->will($this->returnValue('css_xpath')); + + $this->manipulator->expects($this->once()) + ->method('prepend') + ->with('css_xpath', 'parent_xpath') + ->will($this->returnValue('full_xpath')); + + $this->driver->expects($this->once()) + ->method('find') + ->with('full_xpath') + ->will($this->returnValue(array($element1, $element2))); + + $results = $this->finder->findAll('css', 'h3 > a', 'parent_xpath'); + + $this->assertCount(2, $results); + $this->assertContainsOnlyInstancesOf(NodeElement::class, $results); + $this->assertEquals('element1', $results[0]->getXpath()); + $this->assertEquals('element2', $results[1]->getXpath()); + } + + public function testNamedFound() + { + $element1 = $this->createStub(NodeElement::class); + $element1->method('getXpath')->willReturn('element1'); + + $element2 = $this->createStub(NodeElement::class); + $element2->method('getXpath')->willReturn('element2'); + + $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($element1, $element2))); + + $results = $this->finder->findAll('named', 'test', 'parent_xpath'); + + $this->assertCount(2, $results); + $this->assertContainsOnlyInstancesOf(NodeElement::class, $results); + $this->assertEquals('element1', $results[0]->getXpath()); + $this->assertEquals('element2', $results[1]->getXpath()); + } + + public function testNamedPartialFallback() + { + $element1 = $this->createStub(NodeElement::class); + $element1->method('getXpath')->willReturn('element1'); + + $element2 = $this->createStub(NodeElement::class); + $element2->method('getXpath')->willReturn('element2'); + + $this->selectorsHandler->expects($this->exactly(2)) + ->method('selectorToXpath') + ->will($this->returnValueMap(array( + array('named_exact', 'test', 'named_xpath'), + array('named_partial', 'test', 'partial_xpath'), + ))); + + $this->manipulator->expects($this->exactly(2)) + ->method('prepend') + ->willReturnMap(array( + array('named_xpath', 'parent_xpath', 'full_xpath'), + array('partial_xpath', 'parent_xpath', 'full_partial_xpath'), + )); + + $this->driver->expects($this->exactly(2)) + ->method('find') + ->willReturnMap(array( + array('full_xpath', array()), + array('full_partial_xpath', array($element1, $element2)), + )); + + $results = $this->finder->findAll('named', 'test', 'parent_xpath'); + + $this->assertCount(2, $results); + $this->assertContainsOnlyInstancesOf(NodeElement::class, $results); + $this->assertEquals('element1', $results[0]->getXpath()); + $this->assertEquals('element2', $results[1]->getXpath()); + } +} diff --git a/tests/Element/ElementTest.php b/tests/Element/ElementTest.php index f892e95c9..1b73b9d85 100644 --- a/tests/Element/ElementTest.php +++ b/tests/Element/ElementTest.php @@ -3,8 +3,8 @@ namespace Behat\Mink\Tests\Element; use Behat\Mink\Driver\DriverInterface; +use Behat\Mink\Element\ElementFinder; use Behat\Mink\Session; -use Behat\Mink\Selector\SelectorsHandler; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -23,11 +23,9 @@ abstract class ElementTest extends TestCase protected $driver; /** - * Selectors. - * - * @var SelectorsHandler&MockObject + * @var ElementFinder&MockObject */ - protected $selectors; + protected $elementFinder; /** * @before @@ -39,38 +37,7 @@ protected function prepareSession(): void ->expects($this->once()) ->method('setSession'); - $this->selectors = $this->getMockBuilder('Behat\Mink\Selector\SelectorsHandler')->getMock(); - $this->session = new Session($this->driver, $this->selectors); - } - - protected function mockNamedFinder(string $xpath, array $results, array $locator, int $times = 2): void - { - if (!is_array($results[0])) { - $results = array($results, array()); - } - - // In case of empty results, a second call will be done using the partial selector - $processedResults = array(); - foreach ($results as $result) { - $processedResults[] = $result; - if (empty($result)) { - $processedResults[] = $result; - ++$times; - } - } - - $returnValue = call_user_func_array(array($this, 'onConsecutiveCalls'), $processedResults); - - $this->driver - ->expects($this->exactly($times)) - ->method('find') - ->with('//html'.$xpath) - ->will($returnValue); - - $this->selectors - ->expects($this->exactly($times)) - ->method('selectorToXpath') - ->with($this->logicalOr('named_exact', 'named_partial'), $locator) - ->will($this->returnValue($xpath)); + $this->elementFinder = $this->createMock(ElementFinder::class); + $this->session = new Session($this->driver, $this->elementFinder); } } diff --git a/tests/Element/NodeElementTest.php b/tests/Element/NodeElementTest.php index 2114a9f14..75a542f87 100644 --- a/tests/Element/NodeElementTest.php +++ b/tests/Element/NodeElementTest.php @@ -52,7 +52,7 @@ public function testElementIsValid() ->expects($this->once()) ->method('find') ->with($elementXpath) - ->will($this->returnValue(array($elementXpath))); + ->willReturn(array($this->createStub(NodeElement::class))); $this->assertTrue($node->isValid()); } @@ -62,12 +62,24 @@ public function testElementIsNotValid() $node = new NodeElement('some xpath', $this->session); $this->driver - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('find') ->with('some xpath') - ->will($this->onConsecutiveCalls(array(), array('xpath1', 'xpath2'))); + ->willReturn(array()); $this->assertFalse($node->isValid(), 'no elements found is invalid element'); + } + + public function testElementIsNotValidWithMultipleFound() + { + $node = new NodeElement('some xpath', $this->session); + + $this->driver + ->expects($this->once()) + ->method('find') + ->with('some xpath') + ->willReturn(array($this->createStub(NodeElement::class), $this->createStub(NodeElement::class))); + $this->assertFalse($node->isValid(), 'more then 1 element found is invalid element'); } @@ -281,9 +293,7 @@ public function testUncheck() public function testSelectOption() { $node = new NodeElement('select', $this->session); - $option = $this->getMockBuilder('Behat\Mink\Element\NodeElement') - ->disableOriginalConstructor() - ->getMock(); + $option = $this->createMock(NodeElement::class); $option ->expects($this->once()) ->method('getValue') @@ -295,17 +305,10 @@ public function testSelectOption() ->with('select') ->will($this->returnValue('select')); - $this->driver - ->expects($this->once()) - ->method('find') - ->with('select/option') - ->will($this->returnValue(array($option))); - - $this->selectors - ->expects($this->once()) - ->method('selectorToXpath') - ->with('named_exact', array('option', 'item1')) - ->will($this->returnValue('option')); + $this->elementFinder->expects($this->once()) + ->method('findAll') + ->with('named', array('option', 'item1'), 'select') + ->willReturn(array($option)); $this->driver ->expects($this->once()) @@ -317,7 +320,6 @@ public function testSelectOption() public function testSelectOptionNotFound() { - $this->expectException('\Behat\Mink\Exception\ElementNotFoundException'); $node = new NodeElement('select', $this->session); $this->driver @@ -326,18 +328,12 @@ public function testSelectOptionNotFound() ->with('select') ->will($this->returnValue('select')); - $this->driver - ->expects($this->exactly(2)) - ->method('find') - ->with('select/option') - ->will($this->returnValue(array())); - - $this->selectors - ->expects($this->exactly(2)) - ->method('selectorToXpath') - ->with($this->logicalOr('named_exact', 'named_partial'), array('option', 'item1')) - ->will($this->returnValue('option')); + $this->elementFinder->expects($this->once()) + ->method('findAll') + ->with('named', array('option', 'item1'), 'select') + ->willReturn(array()); + $this->expectException('\Behat\Mink\Exception\ElementNotFoundException'); $node->selectOption('item1'); } @@ -379,17 +375,10 @@ public function testGetParent() ->disableOriginalConstructor() ->getMock(); - $this->driver - ->expects($this->once()) - ->method('find') - ->with('elem/..') - ->will($this->returnValue(array($parent))); - - $this->selectors - ->expects($this->once()) - ->method('selectorToXpath') - ->with('xpath', '..') - ->will($this->returnValue('..')); + $this->elementFinder->expects($this->once()) + ->method('findAll') + ->with('xpath', '..', 'elem') + ->willReturn(array($parent)); $this->assertSame($parent, $node->getParent()); } @@ -398,17 +387,10 @@ public function testGetParentNotFound() { $node = new NodeElement('elem', $this->session); - $this->driver - ->expects($this->once()) - ->method('find') - ->with('elem/..') - ->will($this->returnValue(array())); - - $this->selectors - ->expects($this->once()) - ->method('selectorToXpath') - ->with('xpath', '..') - ->will($this->returnValue('..')); + $this->elementFinder->expects($this->once()) + ->method('findAll') + ->with('xpath', '..', 'elem') + ->willReturn(array()); $this->expectException(DriverException::class); $this->expectExceptionMessage('Could not find the element parent. Maybe the element has been removed from the page.'); @@ -569,50 +551,4 @@ public function testSubmitForm() $node->submit(); } - - public function testFindAllUnion() - { - $node = new NodeElement('some_xpath', $this->session); - $xpath = "some_tag1 | some_tag2[@foo =\n 'bar|']\n | some_tag3[foo | bar]"; - $expected = "some_xpath/some_tag1 | some_xpath/some_tag2[@foo =\n 'bar|'] | some_xpath/some_tag3[foo | bar]"; - - $this->driver - ->expects($this->exactly(1)) - ->method('find') - ->will($this->returnValueMap(array( - array($expected, array(2, 3, 4)), - ))); - - $this->selectors - ->expects($this->exactly(1)) - ->method('selectorToXpath') - ->will($this->returnValueMap(array( - array('xpath', $xpath, $xpath), - ))); - - $this->assertEquals(3, count($node->findAll('xpath', $xpath))); - } - - public function testFindAllParentUnion() - { - $node = new NodeElement('some_xpath | another_xpath', $this->session); - $xpath = 'some_tag1 | some_tag2'; - $expectedPrefixed = '(some_xpath | another_xpath)/some_tag1 | (some_xpath | another_xpath)/some_tag2'; - - $this->driver - ->expects($this->exactly(1)) - ->method('find') - ->will($this->returnValueMap(array( - array($expectedPrefixed, array(2, 3, 4)), - ))); - - $this->selectors - ->expects($this->exactly(1)) - ->method('selectorToXpath') - ->will($this->returnValueMap(array( - array('xpath', $xpath, $xpath), - ))); - - $this->assertEquals(3, count($node->findAll('xpath', $xpath))); - } }