123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- <?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/>.
- *
- */
- /**
- * 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.
- *
- * NoSuchElement exceptions are sometimes thrown instead of
- * StaleElementReference exceptions. This can happen when the Selenium2 driver
- * for Mink performs an action on an element through the WebDriver session
- * instead of directly through the WebDriver element. In that case, if the
- * element with the given ID does not exist, a NoSuchElement exception would be
- * thrown instead of a StaleElementReference exception, so those cases are
- * handled like StaleElementReference exceptions.
- *
- * 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.
- *
- * MoveTargetOutOfBounds exceptions are sometimes thrown instead of
- * ElementNotVisible exceptions. This can happen when the Selenium2 driver for
- * Mink moves the cursor on an element using the "moveto" method of the
- * WebDriver session, for example, before clicking on an element. In that case,
- * if the element is not visible, "moveto" would throw a MoveTargetOutOfBounds
- * exception instead of an ElementNotVisible exception, so those cases are
- * handled like ElementNotVisible exceptions.
- *
- * ElementNotInteractable exceptions are thrown in Selenium 3 when the command
- * needs to interact with an element but that is not possible. This could be a
- * transitive situation (for example, due to an animation), so the command is
- * executed again after a small timeout.
- *
- * 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). In a
- * similar way, MoveTargetOutOfBounds exceptions would be thrown again if
- * originally they were thrown because the element was visible but "out of
- * reach". ElementNotInteractable exceptions would be thrown again if it is not
- * possible to interact yet with the element after the wait (which could mean
- * that the test has to, indeed, fail, although it could mean too that the
- * automatic handling needs to be improved).
- *
- * 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 $this->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 bool 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 whether the wrapped element is checked or not.
- *
- * @return bool true if the wrapped element is checked, false otherwise.
- */
- public function isChecked() {
- $commandCallback = function () {
- return $this->element->isChecked();
- };
- return $this->executeCommand($commandCallback, "check state 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");
- }
- /**
- * Check the wrapped element.
- *
- * If automatically waits for the wrapped element to be visible (up to the
- * timeout set when finding it).
- */
- public function check() {
- $commandCallback = function () {
- $this->element->check();
- };
- $this->executeCommand($commandCallback, "could not be checked");
- }
- /**
- * uncheck the wrapped element.
- *
- * If automatically waits for the wrapped element to be visible (up to the
- * timeout set when finding it).
- */
- public function uncheck() {
- $commandCallback = function () {
- $this->element->uncheck();
- };
- $this->executeCommand($commandCallback, "could not be unchecked");
- }
- /**
- * Executes the given command.
- *
- * If a StaleElementReference or a NoSuchElement 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);
- } catch (\WebDriver\Exception\NoSuchElement $exception) {
- $this->printFailedCommandMessage($exception, $errorMessage);
- }
- $this->element = $this->elementFinder->find();
- return $commandCallback();
- }
- /**
- * Executes the given command on a visible element.
- *
- * If a StaleElementReference or a NoSuchElement exception is thrown the
- * wrapped element is found again and, then, the command is executed again.
- * If an ElementNotVisible or a MoveTargetOutOfBounds exception is thrown it
- * is waited for the wrapped element to be visible and, then, the command is
- * executed again.
- * If an ElementNotInteractable exception is thrown it is also waited for
- * the wrapped element to be visible. It is very likely that the element was
- * visible already, but it is not possible to easily check if the element
- * can be interacted with, retrying will be only useful if it was a
- * transitive situation that resolves itself with a wait (for example, due
- * to an animation) and waiting for the element to be visible will always
- * start with a wait.
- *
- * @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);
- } catch (\WebDriver\Exception\MoveTargetOutOfBounds $exception) {
- $this->printFailedCommandMessage($exception, $errorMessage);
- } catch (\Exception $exception) {
- // The "ElementNotInteractable" exception is not available yet in
- // the current "instaclick/php-webdriver" version, so it is thrown
- // as a generic exception with a specific message.
- if (stripos($exception->getMessage(), "element not interactable") === false) {
- throw $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);
- }
- }
|