server/build/acceptance/features/core/Actor.php
Daniel Calviño Sánchez b22997796b Add wrappers to adapt the element finding system of Mink
Mink elements (including the document element) provide a
"find(selector, locator)" method to look for child elements in their web
browser session. The Locator class is added to be able to store the
selector and locator in a single object; it also provides a fluent API
to ease the definition of Mink locators, specially those using the
"named" selector.

The method "find(locator, timeout, timeoutStep)" is added to Actor
objects; it is simply a wrapper over Mink's "find(selector, locator)"
method, although it throws an exception if the element can not be found
instead of returning null, and it also makes possible to automatically
retry to find the element for certain amount of time.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2017-04-19 08:26:03 +02:00

191 lines
7.1 KiB
PHP

<?php
/**
*
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* An actor in a test scenario.
*
* Every Actor object is intended to be used only in a single test scenario.
* An Actor can control its web browser thanks to the Mink Session received when
* it was created, so in each scenario each Actor must have its own Mink
* Session; the same Mink Session can be used by different Actors in different
* scenarios, but never by different Actors in the same scenario.
*
* The test servers used in an scenario can change between different test runs,
* so an Actor stores the base URL for the current test server being used; in
* most cases the tests are specified using relative paths that can be converted
* to the appropriate absolute URL using locatePath() in the step
* implementation.
*
* An Actor can find elements in its Mink Session using its find() method; it is
* a wrapper over the find() method provided by Mink that extends it with
* several features: the element can be looked for based on a Locator object, an
* exception is thrown if the element is not found, and, optionally, it is
* possible to try again to find the element several times before giving up.
*/
class Actor {
/**
* @var \Behat\Mink\Session
*/
private $session;
/**
* @var string
*/
private $baseUrl;
/**
* Creates a new Actor.
*
* @param \Behat\Mink\Session $session the Mink Session used to control its
* web browser.
* @param string $baseUrl the base URL used when solving relative URLs.
*/
public function __construct(\Behat\Mink\Session $session, $baseUrl) {
$this->session = $session;
$this->baseUrl = $baseUrl;
}
/**
* Sets the base URL.
*
* @param string $baseUrl the base URL used when solving relative URLs.
*/
public function setBaseUrl($baseUrl) {
$this->baseUrl = $baseUrl;
}
/**
* Returns the Mink Session used to control its web browser.
*
* @return \Behat\Mink\Session the Mink Session used to control its web
* browser.
*/
public function getSession() {
return $this->session;
}
/**
* Returns the full path for the given relative path based on the base URL.
*
* @param string relativePath the relative path.
* @return string the full path.
*/
public function locatePath($relativePath) {
return $this->baseUrl . $relativePath;
}
/**
* Finds an element in the Mink Session of this Actor.
*
* The given element locator is relative to its ancestor (either another
* locator or an actual element); if it has no ancestor then the base
* document element is used.
*
* Sometimes an element may not be found simply because it has not appeared
* yet; for those cases this method supports trying again to find the
* element several times before giving up. The timeout parameter controls
* how much time to wait, at most, to find the element; the timeoutStep
* parameter controls how much time to wait before trying again to find the
* element. If ancestor locators need to be found the timeout is applied
* individually to each one, that is, if the timeout is 10 seconds the
* method will wait up to 10 seconds to find the ancestor of the ancestor
* and, then, up to 10 seconds to find the ancestor and, then, up to 10
* seconds to find the element. By default the timeout is 0, so the element
* and its ancestor will be looked for just once; the default time to wait
* before retrying is half a second.
*
* In any case, if the element, or its ancestors, can not be found a
* NoSuchElementException is thrown.
*
* @param Locator $elementLocator the locator for the element.
* @param float $timeout the number of seconds (decimals allowed) to wait at
* most for the element to appear.
* @param float $timeoutStep the number of seconds (decimals allowed) to
* wait before trying to find the element again.
* @return \Behat\Mink\Element\Element the element found.
* @throws NoSuchElementException if the element, or its ancestor, can not
* be found.
*/
public function find($elementLocator, $timeout = 0, $timeoutStep = 0.5) {
$element = null;
$selector = $elementLocator->getSelector();
$locator = $elementLocator->getLocator();
$ancestorElement = $this->findAncestorElement($elementLocator, $timeout, $timeoutStep);
$findCallback = function() use (&$element, $selector, $locator, $ancestorElement) {
$element = $ancestorElement->find($selector, $locator);
return $element !== null;
};
if (!Utils::waitFor($findCallback, $timeout, $timeoutStep)) {
throw new NoSuchElementException($elementLocator->getDescription() . " could not be found");
}
return $element;
}
/**
* Returns the ancestor element from which the given locator will be looked
* for.
*
* If the ancestor of the given locator is another locator the element for
* the ancestor locator is found and returned. If the ancestor of the given
* locator is already an element that element is the one returned. If the
* given locator has no ancestor then the base document element is returned.
*
* The timeout is used only when finding the element for the ancestor
* locator; if the timeout expires a NoSuchElementException is thrown.
*
* @param Locator $elementLocator the locator for the element to get its
* ancestor.
* @param float $timeout the number of seconds (decimals allowed) to wait at
* most for the ancestor element to appear.
* @param float $timeoutStep the number of seconds (decimals allowed) to
* wait before trying to find the ancestor element again.
* @return \Behat\Mink\Element\Element the ancestor element found.
* @throws NoSuchElementException if the ancestor element can not be found.
*/
private function findAncestorElement($elementLocator, $timeout, $timeoutStep) {
$ancestorElement = $elementLocator->getAncestor();
if ($ancestorElement instanceof Locator) {
try {
$ancestorElement = $this->find($ancestorElement, $timeout, $timeoutStep);
} catch (NoSuchElementException $exception) {
// Little hack to show the stack of ancestor elements that could
// not be found, as Behat only shows the message of the last
// exception in the chain.
$message = $exception->getMessage() . "\n" .
$elementLocator->getDescription() . " could not be found";
throw new NoSuchElementException($message, $exception);
}
}
if ($ancestorElement === null) {
$ancestorElement = $this->getSession()->getPage();
}
return $ancestorElement;
}
}