From 16e3e816352d8afc46db82fa85c2bf776a4d7fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 4 May 2017 12:10:02 +0200 Subject: [PATCH 1/4] Add missing type hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- tests/acceptance/features/core/Actor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php index 3a57b7e605..fdb15aa946 100644 --- a/tests/acceptance/features/core/Actor.php +++ b/tests/acceptance/features/core/Actor.php @@ -162,7 +162,7 @@ class Actor { * @throws NoSuchElementException if the element, or its ancestor, can not * be found. */ - public function find($elementLocator, $timeout = 0, $timeoutStep = 0.5) { + public function find(Locator $elementLocator, $timeout = 0, $timeoutStep = 0.5) { $timeout = $timeout * $this->findTimeoutMultiplier; return $this->findInternal($elementLocator, $timeout, $timeoutStep); @@ -176,7 +176,7 @@ class Actor { * * @see find($elementLocator, $timeout, $timeoutStep) */ - private function findInternal($elementLocator, $timeout, $timeoutStep) { + private function findInternal(Locator $elementLocator, $timeout, $timeoutStep) { $element = null; $selector = $elementLocator->getSelector(); $locator = $elementLocator->getLocator(); @@ -219,7 +219,7 @@ class Actor { * @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) { + private function findAncestorElement(Locator $elementLocator, $timeout, $timeoutStep) { $ancestorElement = $elementLocator->getAncestor(); if ($ancestorElement instanceof Locator) { try { From 7642a4b72746838f50ff0fc1eba2320fb63d1660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 4 May 2017 12:35:01 +0200 Subject: [PATCH 2/4] Make internal find methods static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- tests/acceptance/features/core/Actor.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php index fdb15aa946..d5987f4e84 100644 --- a/tests/acceptance/features/core/Actor.php +++ b/tests/acceptance/features/core/Actor.php @@ -165,22 +165,22 @@ class Actor { public function find(Locator $elementLocator, $timeout = 0, $timeoutStep = 0.5) { $timeout = $timeout * $this->findTimeoutMultiplier; - return $this->findInternal($elementLocator, $timeout, $timeoutStep); + return self::findInternal($this->session, $elementLocator, $timeout, $timeoutStep); } /** - * Finds an element in the Mink Session of this Actor. + * Finds an element in the given Mink Session. * * The timeout is not affected by the multiplier set using * setFindTimeoutMultiplier(). * - * @see find($elementLocator, $timeout, $timeoutStep) + * @see find($session, $elementLocator, $timeout, $timeoutStep) */ - private function findInternal(Locator $elementLocator, $timeout, $timeoutStep) { + private static function findInternal(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { $element = null; $selector = $elementLocator->getSelector(); $locator = $elementLocator->getLocator(); - $ancestorElement = $this->findAncestorElement($elementLocator, $timeout, $timeoutStep); + $ancestorElement = self::findAncestorElement($session, $elementLocator, $timeout, $timeoutStep); $findCallback = function() use (&$element, $selector, $locator, $ancestorElement) { $element = $ancestorElement->find($selector, $locator); @@ -210,6 +210,8 @@ class Actor { * The timeout is used only when finding the element for the ancestor * locator; if the timeout expires a NoSuchElementException is thrown. * + * @param \Behat\Mink\Session $session the Mink Session to get the ancestor + * element from. * @param Locator $elementLocator the locator for the element to get its * ancestor. * @param float $timeout the number of seconds (decimals allowed) to wait at @@ -219,11 +221,11 @@ class Actor { * @return \Behat\Mink\Element\Element the ancestor element found. * @throws NoSuchElementException if the ancestor element can not be found. */ - private function findAncestorElement(Locator $elementLocator, $timeout, $timeoutStep) { + private static function findAncestorElement(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { $ancestorElement = $elementLocator->getAncestor(); if ($ancestorElement instanceof Locator) { try { - $ancestorElement = $this->findInternal($ancestorElement, $timeout, $timeoutStep); + $ancestorElement = self::findInternal($session, $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 @@ -238,7 +240,7 @@ class Actor { } if ($ancestorElement === null) { - $ancestorElement = $this->getSession()->getPage(); + $ancestorElement = $session->getPage(); } return $ancestorElement; From 64f9c56224fca9859c79bb1e2c3ae373b76c2864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 4 May 2017 13:03:14 +0200 Subject: [PATCH 3/4] Extract element finding to a command object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- tests/acceptance/features/core/Actor.php | 80 +------- .../features/core/ElementFinder.php | 176 ++++++++++++++++++ 2 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 tests/acceptance/features/core/ElementFinder.php diff --git a/tests/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php index d5987f4e84..0e45aea335 100644 --- a/tests/acceptance/features/core/Actor.php +++ b/tests/acceptance/features/core/Actor.php @@ -165,85 +165,9 @@ class Actor { public function find(Locator $elementLocator, $timeout = 0, $timeoutStep = 0.5) { $timeout = $timeout * $this->findTimeoutMultiplier; - return self::findInternal($this->session, $elementLocator, $timeout, $timeoutStep); - } + $elementFinder = new ElementFinder($this->session, $elementLocator, $timeout, $timeoutStep); - /** - * Finds an element in the given Mink Session. - * - * The timeout is not affected by the multiplier set using - * setFindTimeoutMultiplier(). - * - * @see find($session, $elementLocator, $timeout, $timeoutStep) - */ - private static function findInternal(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { - $element = null; - $selector = $elementLocator->getSelector(); - $locator = $elementLocator->getLocator(); - $ancestorElement = self::findAncestorElement($session, $elementLocator, $timeout, $timeoutStep); - - $findCallback = function() use (&$element, $selector, $locator, $ancestorElement) { - $element = $ancestorElement->find($selector, $locator); - - return $element !== null; - }; - if (!Utils::waitFor($findCallback, $timeout, $timeoutStep)) { - $message = $elementLocator->getDescription() . " could not be found"; - if ($timeout > 0) { - $message = $message . " after $timeout seconds"; - } - throw new NoSuchElementException($message); - } - - 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 \Behat\Mink\Session $session the Mink Session to get the ancestor - * element from. - * @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 static function findAncestorElement(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { - $ancestorElement = $elementLocator->getAncestor(); - if ($ancestorElement instanceof Locator) { - try { - $ancestorElement = self::findInternal($session, $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"; - if ($timeout > 0) { - $message = $message . " after $timeout seconds"; - } - throw new NoSuchElementException($message, $exception); - } - } - - if ($ancestorElement === null) { - $ancestorElement = $session->getPage(); - } - - return $ancestorElement; + return $elementFinder->find(); } /** diff --git a/tests/acceptance/features/core/ElementFinder.php b/tests/acceptance/features/core/ElementFinder.php new file mode 100644 index 0000000000..9e1457a686 --- /dev/null +++ b/tests/acceptance/features/core/ElementFinder.php @@ -0,0 +1,176 @@ +. + * + */ + +/** + * Command object to find Mink elements. + * + * The 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 ElementFinder 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. + */ +class ElementFinder { + + /** + * Finds an element in the given Mink Session. + * + * @see ElementFinder + */ + private static function findInternal(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { + $element = null; + $selector = $elementLocator->getSelector(); + $locator = $elementLocator->getLocator(); + $ancestorElement = self::findAncestorElement($session, $elementLocator, $timeout, $timeoutStep); + + $findCallback = function() use (&$element, $selector, $locator, $ancestorElement) { + $element = $ancestorElement->find($selector, $locator); + + return $element !== null; + }; + if (!Utils::waitFor($findCallback, $timeout, $timeoutStep)) { + $message = $elementLocator->getDescription() . " could not be found"; + if ($timeout > 0) { + $message = $message . " after $timeout seconds"; + } + throw new NoSuchElementException($message); + } + + 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 \Behat\Mink\Session $session the Mink Session to get the ancestor + * element from. + * @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 static function findAncestorElement(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { + $ancestorElement = $elementLocator->getAncestor(); + if ($ancestorElement instanceof Locator) { + try { + $ancestorElement = self::findInternal($session, $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"; + if ($timeout > 0) { + $message = $message . " after $timeout seconds"; + } + throw new NoSuchElementException($message, $exception); + } + } + + if ($ancestorElement === null) { + $ancestorElement = $session->getPage(); + } + + return $ancestorElement; + } + + /** + * @var \Behat\Mink\Session + */ + private $session; + + /** + * @param Locator + */ + private $elementLocator; + + /** + * @var float + */ + private $timeout; + + /** + * @var float + */ + private $timeoutStep; + + /** + * Creates a new ElementFinder. + * + * @param \Behat\Mink\Session $session the Mink Session to get the element + * from. + * @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. + */ + public function __construct(\Behat\Mink\Session $session, Locator $elementLocator, $timeout, $timeoutStep) { + $this->session = $session; + $this->elementLocator = $elementLocator; + $this->timeout = $timeout; + $this->timeoutStep = $timeoutStep; + } + + /** + * Finds an element using the parameters set in the constructor of this + * ElementFinder. + * + * If the element, or its ancestors, can not be found a + * NoSuchElementException is thrown. + * + * @return \Behat\Mink\Element\Element the element found. + * @throws NoSuchElementException if the element, or its ancestor, can not + * be found. + */ + public function find() { + return self::findInternal($this->session, $this->elementLocator, $this->timeout, $this->timeoutStep); + } + +} From 9313c9797fe1ef9349526175f72aedf0483dea39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 5 May 2017 22:54:20 +0200 Subject: [PATCH 4/4] Add automatic handling of common command failures of Mink elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commands executed on Mink elements may fail for several reasons. ElementWrapper is introduced to automatically handle some of those situations, like StaleElementReference exceptions and ElementNotVisible exceptions. StaleElementReference exceptions are thrown when the command is executed on an element that is no longer attached to the DOM. When that happens the ElementWrapper finds again the element and executes the command again on the new element. ElementNotVisible exceptions are thrown when the command requires the element to be visible but the element is not. When that happens the ElementWrapper waits for the element to be visible before executing the command again. These changes are totally compatible with the current acceptance tests. They just make the tests more robust, but they do not change their behaviour. In fact, this should minimize some of the sporadic failures in the acceptance tests caused by their concurrent nature with respect to the web browser executing the commands. However, the ElementWrapper is not a silver bullet; it handles the most common situations, but it does not handle every possible scenario. For example, the acceptance tests would still fail sporadically if an element can become staled several times in a row (uncommon) or if it does not become visible before the timeout expires (which could still happen in a loaded system even if the components under test work right, but obviously it is not possible to wait indefinitely for them). Signed-off-by: Daniel Calviño Sánchez --- tests/acceptance/features/core/Actor.php | 13 +- .../features/core/ElementFinder.php | 29 ++ .../features/core/ElementWrapper.php | 275 ++++++++++++++++++ 3 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 tests/acceptance/features/core/ElementWrapper.php diff --git a/tests/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php index 0e45aea335..a87ccfb773 100644 --- a/tests/acceptance/features/core/Actor.php +++ b/tests/acceptance/features/core/Actor.php @@ -42,6 +42,11 @@ * 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. * + * The returned object is also a wrapper over the element itself that + * automatically handles common causes of failed commands, like clicking on a + * hidden element; in this case, the wrapper would wait for the element to be + * visible up to the timeout set to find the element. + * * The amount of time to wait before giving up is specified in each call to * find(). However, a general multiplier to be applied to every timeout can be * set using setFindTimeoutMultiplier(); this makes possible to retry longer @@ -150,6 +155,10 @@ class Actor { * before retrying is half a second. If the timeout is not 0 it will be * affected by the multiplier set using setFindTimeoutMultiplier(), if any. * + * When found, the element is returned wrapped in an ElementWrapper; the + * ElementWrapper handles common causes of failures when executing commands + * in an element, like clicking on a hidden element. + * * In any case, if the element, or its ancestors, can not be found a * NoSuchElementException is thrown. * @@ -158,7 +167,7 @@ class Actor { * 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. + * @return ElementWrapper an ElementWrapper object for the element. * @throws NoSuchElementException if the element, or its ancestor, can not * be found. */ @@ -167,7 +176,7 @@ class Actor { $elementFinder = new ElementFinder($this->session, $elementLocator, $timeout, $timeoutStep); - return $elementFinder->find(); + return new ElementWrapper($elementFinder); } /** diff --git a/tests/acceptance/features/core/ElementFinder.php b/tests/acceptance/features/core/ElementFinder.php index 9e1457a686..d075e9fe66 100644 --- a/tests/acceptance/features/core/ElementFinder.php +++ b/tests/acceptance/features/core/ElementFinder.php @@ -158,6 +158,35 @@ class ElementFinder { $this->timeoutStep = $timeoutStep; } + /** + * Returns the description of the element to find. + * + * @return string the description of the element to find. + */ + public function getDescription() { + return $this->elementLocator->getDescription(); + } + + /** + * Returns the timeout. + * + * @return float the number of seconds (decimals allowed) to wait at most + * for the element to appear. + */ + public function getTimeout() { + return $this->timeout; + } + + /** + * Returns the timeout step. + * + * @return float the number of seconds (decimals allowed) to wait before + * trying to find the element again. + */ + public function getTimeoutStep() { + return $this->timeoutStep; + } + /** * Finds an element using the parameters set in the constructor of this * ElementFinder. diff --git a/tests/acceptance/features/core/ElementWrapper.php b/tests/acceptance/features/core/ElementWrapper.php new file mode 100644 index 0000000000..6b730903f6 --- /dev/null +++ b/tests/acceptance/features/core/ElementWrapper.php @@ -0,0 +1,275 @@ +. + * + */ + +/** + * Wrapper to automatically handle failed commands on Mink elements. + * + * Commands executed on Mink elements may fail for several reasons. The + * ElementWrapper frees the caller of the commands from handling the most common + * reasons of failure. + * + * StaleElementReference exceptions are thrown when the command is executed on + * an element that is no longer attached to the DOM. This can happen even in + * a chained call like "$actor->find($locator)->click()"; in the milliseconds + * between finding the element and clicking it the element could have been + * removed from the page (for example, if a previous interaction with the page + * started an asynchronous update of the DOM). Every command executed through + * the ElementWrapper is guarded against StaleElementReference exceptions; if + * the element is stale it is found again using the same parameters to find it + * in the first place. + * + * ElementNotVisible exceptions are thrown when the command requires the element + * to be visible but the element is not. Finding an element only guarantees that + * (at that time) the element is attached to the DOM, but it does not provide + * any guarantee regarding its visibility. Due to that, a call like + * "$actor->find($locator)->click()" can fail if the element was hidden and + * meant to be made visible by a previous interaction with the page, but that + * interaction triggered an asynchronous update that was not finished when the + * click command is executed. All commands executed through the ElementWrapper + * that require the element to be visible are guarded against ElementNotVisible + * exceptions; if the element is not visible it is waited for it to be visible + * up to the timeout set to find it. + * + * Despite the automatic handling it is possible for the commands to throw those + * exceptions when they are executed again; this class does not handle cases + * like an element becoming stale several times in a row (uncommon) or an + * element not becoming visible before the timeout expires (which would mean + * that the timeout is too short or that the test has to, indeed, fail). + * + * If needed, automatically handling failed commands can be disabled calling + * "doNotHandleFailedCommands()"; as it returns the ElementWrapper it can be + * chained with the command to execute (but note that automatically handling + * failed commands will still be disabled if further commands are executed on + * the ElementWrapper). + */ +class ElementWrapper { + + /** + * @var ElementFinder + */ + private $elementFinder; + + /** + * @var \Behat\Mink\Element\Element + */ + private $element; + + /** + * @param boolean + */ + private $handleFailedCommands; + + /** + * Creates a new ElementWrapper. + * + * The wrapped element is found in the constructor itself using the + * ElementFinder. + * + * @param ElementFinder $elementFinder the command object to find the + * wrapped element. + * @throws NoSuchElementException if the element, or its ancestor, can not + * be found. + */ + public function __construct(ElementFinder $elementFinder) { + $this->elementFinder = $elementFinder; + $this->element = $elementFinder->find(); + $this->handleFailedCommands = true; + } + + /** + * Returns the raw Mink element. + * + * @return \Behat\Mink\Element\Element the wrapped element. + */ + public function getWrappedElement() { + return $element; + } + + /** + * Prevents the automatic handling of failed commands. + * + * @return ElementWrapper this ElementWrapper. + */ + public function doNotHandleFailedCommands() { + $this->handleFailedCommands = false; + + return $this; + } + + /** + * Returns whether the wrapped element is visible or not. + * + * @return boolbean true if the wrapped element is visible, false otherwise. + */ + public function isVisible() { + $commandCallback = function() { + return $this->element->isVisible(); + }; + return $this->executeCommand($commandCallback, "visibility could not be got"); + } + + /** + * Returns the text of the wrapped element. + * + * If the wrapped element is not visible the returned text is an empty + * string. + * + * @return string the text of the wrapped element, or an empty string if it + * is not visible. + */ + public function getText() { + $commandCallback = function() { + return $this->element->getText(); + }; + return $this->executeCommand($commandCallback, "text could not be got"); + } + + /** + * Returns the value of the wrapped element. + * + * The value can be got even if the wrapped element is not visible. + * + * @return string the value of the wrapped element. + */ + public function getValue() { + $commandCallback = function() { + return $this->element->getValue(); + }; + return $this->executeCommand($commandCallback, "value could not be got"); + } + + /** + * Sets the given value on the wrapped element. + * + * If automatically waits for the wrapped element to be visible (up to the + * timeout set when finding it). + * + * @param string $value the value to set. + */ + public function setValue($value) { + $commandCallback = function() use ($value) { + $this->element->setValue($value); + }; + $this->executeCommandOnVisibleElement($commandCallback, "value could not be set"); + } + + /** + * Clicks on the wrapped element. + * + * If automatically waits for the wrapped element to be visible (up to the + * timeout set when finding it). + */ + public function click() { + $commandCallback = function() { + $this->element->click(); + }; + $this->executeCommandOnVisibleElement($commandCallback, "could not be clicked"); + } + + /** + * Executes the given command. + * + * If a StaleElementReference exception is thrown the wrapped element is + * found again and, then, the command is executed again. + * + * @param \Closure $commandCallback the command to execute. + * @param string $errorMessage an error message that describes the failed + * command (appended to the description of the element). + */ + private function executeCommand(\Closure $commandCallback, $errorMessage) { + if (!$this->handleFailedCommands) { + return $commandCallback(); + } + + try { + return $commandCallback(); + } catch (\WebDriver\Exception\StaleElementReference $exception) { + $this->printFailedCommandMessage($exception, $errorMessage); + } + + $this->element = $this->elementFinder->find(); + + return $commandCallback(); + } + + /** + * Executes the given command on a visible element. + * + * If a StaleElementReference exception is thrown the wrapped element is + * found again and, then, the command is executed again. If an + * ElementNotVisible exception is thrown it is waited for the wrapped + * element to be visible and, then, the command is executed again. + * + * @param \Closure $commandCallback the command to execute. + * @param string $errorMessage an error message that describes the failed + * command (appended to the description of the element). + */ + private function executeCommandOnVisibleElement(\Closure $commandCallback, $errorMessage) { + if (!$this->handleFailedCommands) { + return $commandCallback(); + } + + try { + return $this->executeCommand($commandCallback, $errorMessage); + } catch (\WebDriver\Exception\ElementNotVisible $exception) { + $this->printFailedCommandMessage($exception, $errorMessage); + } + + $this->waitForElementToBeVisible(); + + return $commandCallback(); + } + + /** + * Prints information about the failed command. + * + * @param \Exception exception the exception thrown by the command. + * @param string $errorMessage an error message that describes the failed + * command (appended to the description of the locator of the element). + */ + private function printFailedCommandMessage(\Exception $exception, $errorMessage) { + echo $this->elementFinder->getDescription() . " " . $errorMessage . "\n"; + echo "Exception message: " . $exception->getMessage() . "\n"; + echo "Trying again\n"; + } + + /** + * Waits for the wrapped element to be visible. + * + * This method waits up to the timeout used when finding the wrapped + * element; therefore, it may return when the element is still not visible. + * + * @return boolean true if the element is visible after the wait, false + * otherwise. + */ + private function waitForElementToBeVisible() { + $isVisibleCallback = function() { + return $this->isVisible(); + }; + $timeout = $this->elementFinder->getTimeout(); + $timeoutStep = $this->elementFinder->getTimeoutStep(); + + return Utils::waitFor($isVisibleCallback, $timeout, $timeoutStep); + } + +}