[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/behat/ -> behat_base.php (source)

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Base class of all steps definitions.
  19   *
  20   * This script is only called from Behat as part of it's integration
  21   * in Moodle.
  22   *
  23   * @package   core
  24   * @category  test
  25   * @copyright 2012 David Monllaó
  26   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  30  
  31  use Behat\Mink\Exception\ExpectationException as ExpectationException,
  32      Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
  33      Behat\Mink\Element\NodeElement as NodeElement;
  34  
  35  /**
  36   * Steps definitions base class.
  37   *
  38   * To extend by the steps definitions of the different Moodle components.
  39   *
  40   * It can not contain steps definitions to avoid duplicates, only utility
  41   * methods shared between steps.
  42   *
  43   * @method NodeElement find_field(string $locator) Finds a form element
  44   * @method NodeElement find_button(string $locator) Finds a form input submit element or a button
  45   * @method NodeElement find_link(string $locator) Finds a link on a page
  46   * @method NodeElement find_file(string $locator) Finds a forum input file element
  47   *
  48   * @package   core
  49   * @category  test
  50   * @copyright 2012 David Monllaó
  51   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  52   */
  53  class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
  54  
  55      /**
  56       * Small timeout.
  57       *
  58       * A reduced timeout for cases where self::TIMEOUT is too much
  59       * and a simple $this->getSession()->getPage()->find() could not
  60       * be enough.
  61       */
  62      const REDUCED_TIMEOUT = 2;
  63  
  64      /**
  65       * The timeout for each Behat step (load page, wait for an element to load...).
  66       */
  67      const TIMEOUT = 6;
  68  
  69      /**
  70       * And extended timeout for specific cases.
  71       */
  72      const EXTENDED_TIMEOUT = 10;
  73  
  74      /**
  75       * The JS code to check that the page is ready.
  76       */
  77      const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
  78  
  79      /**
  80       * Locates url, based on provided path.
  81       * Override to provide custom routing mechanism.
  82       *
  83       * @see Behat\MinkExtension\Context\MinkContext
  84       * @param string $path
  85       * @return string
  86       */
  87      protected function locate_path($path) {
  88          $starturl = rtrim($this->getMinkParameter('base_url'), '/') . '/';
  89          return 0 !== strpos($path, 'http') ? $starturl . ltrim($path, '/') : $path;
  90      }
  91  
  92      /**
  93       * Returns the first matching element.
  94       *
  95       * @link http://mink.behat.org/#traverse-the-page-selectors
  96       * @param string $selector The selector type (css, xpath, named...)
  97       * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
  98       * @param Exception $exception Otherwise we throw exception with generic info
  99       * @param NodeElement $node Spins around certain DOM node instead of the whole page
 100       * @param int $timeout Forces a specific time out (in seconds).
 101       * @return NodeElement
 102       */
 103      protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) {
 104  
 105          // Returns the first match.
 106          $items = $this->find_all($selector, $locator, $exception, $node, $timeout);
 107          return count($items) ? reset($items) : null;
 108      }
 109  
 110      /**
 111       * Returns all matching elements.
 112       *
 113       * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method.
 114       *
 115       * @link http://mink.behat.org/#traverse-the-page-selectors
 116       * @param string $selector The selector type (css, xpath, named...)
 117       * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
 118       * @param Exception $exception Otherwise we throw expcetion with generic info
 119       * @param NodeElement $node Spins around certain DOM node instead of the whole page
 120       * @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied.
 121       * @return array NodeElements list
 122       */
 123      protected function find_all($selector, $locator, $exception = false, $node = false, $timeout = false) {
 124  
 125          // Generic info.
 126          if (!$exception) {
 127  
 128              // With named selectors we can be more specific.
 129              if ($selector == 'named') {
 130                  $exceptiontype = $locator[0];
 131                  $exceptionlocator = $locator[1];
 132  
 133                  // If we are in a @javascript session all contents would be displayed as HTML characters.
 134                  if ($this->running_javascript()) {
 135                      $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES);
 136                  }
 137  
 138              } else {
 139                  $exceptiontype = $selector;
 140                  $exceptionlocator = $locator;
 141              }
 142  
 143              $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
 144          }
 145  
 146          $params = array('selector' => $selector, 'locator' => $locator);
 147          // Pushing $node if required.
 148          if ($node) {
 149              $params['node'] = $node;
 150          }
 151  
 152          // How much we will be waiting for the element to appear.
 153          if (!$timeout) {
 154              $timeout = self::TIMEOUT;
 155              $microsleep = false;
 156          } else {
 157              // Spinning each 0.1 seconds if the timeout was forced as we understand
 158              // that is a special case and is good to refine the performance as much
 159              // as possible.
 160              $microsleep = true;
 161          }
 162  
 163          // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
 164          return $this->spin(
 165              function($context, $args) {
 166  
 167                  // If no DOM node provided look in all the page.
 168                  if (empty($args['node'])) {
 169                      return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
 170                  }
 171  
 172                  // For nodes contained in other nodes we can not use the basic named selectors
 173                  // as they include unions and they would look for matches in the DOM root.
 174                  $elementxpath = $context->getSession()->getSelectorsHandler()->selectorToXpath($args['selector'], $args['locator']);
 175  
 176                  // Split the xpath in unions and prefix them with the container xpath.
 177                  $unions = explode('|', $elementxpath);
 178                  foreach ($unions as $key => $union) {
 179                      $union = trim($union);
 180  
 181                      // We are in the container node.
 182                      if (strpos($union, '.') === 0) {
 183                          $union = substr($union, 1);
 184                      } else if (strpos($union, '/') !== 0) {
 185                          // Adding the path separator in case it is not there.
 186                          $union = '/' . $union;
 187                      }
 188                      $unions[$key] = $args['node']->getXpath() . $union;
 189                  }
 190  
 191                  // We can not use usual Element::find() as it prefixes with DOM root.
 192                  return $context->getSession()->getDriver()->find(implode('|', $unions));
 193              },
 194              $params,
 195              $timeout,
 196              $exception,
 197              $microsleep
 198          );
 199      }
 200  
 201      /**
 202       * Finds DOM nodes in the page using named selectors.
 203       *
 204       * The point of using this method instead of Mink ones is the spin
 205       * method of behat_base::find() that looks for the element until it
 206       * is available or it timeouts, this avoids the false failures received
 207       * when selenium tries to execute commands on elements that are not
 208       * ready to be used.
 209       *
 210       * All steps that requires elements to be available before interact with
 211       * them should use one of the find* methods.
 212       *
 213       * The methods calls requires a {'find_' . $elementtype}($locator)
 214       * format, like find_link($locator), find_select($locator),
 215       * find_button($locator)...
 216       *
 217       * @link http://mink.behat.org/#named-selectors
 218       * @throws coding_exception
 219       * @param string $name The name of the called method
 220       * @param mixed $arguments
 221       * @return NodeElement
 222       */
 223      public function __call($name, $arguments) {
 224  
 225          if (substr($name, 0, 5) !== 'find_') {
 226              throw new coding_exception('The "' . $name . '" method does not exist');
 227          }
 228  
 229          // Only the named selector identifier.
 230          $cleanname = substr($name, 5);
 231  
 232          // All named selectors shares the interface.
 233          if (count($arguments) !== 1) {
 234              throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument');
 235          }
 236  
 237          // Redirecting execution to the find method with the specified selector.
 238          // It will detect if it's pointing to an unexisting named selector.
 239          return $this->find('named',
 240              array(
 241                  $cleanname,
 242                  $this->getSession()->getSelectorsHandler()->xpathLiteral($arguments[0])
 243              )
 244          );
 245      }
 246  
 247      /**
 248       * Escapes the double quote character.
 249       *
 250       * Double quote is the argument delimiter, it can be escaped
 251       * with a backslash, but we auto-remove this backslashes
 252       * before the step execution, this method is useful when using
 253       * arguments as arguments for other steps.
 254       *
 255       * @param string $string
 256       * @return string
 257       */
 258      public function escape($string) {
 259          return str_replace('"', '\"', $string);
 260      }
 261  
 262      /**
 263       * Executes the passed closure until returns true or time outs.
 264       *
 265       * In most cases the document.readyState === 'complete' will be enough, but sometimes JS
 266       * requires more time to be completely loaded or an element to be visible or whatever is required to
 267       * perform some action on an element; this method receives a closure which should contain the
 268       * required statements to ensure the step definition actions and assertions have all their needs
 269       * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the
 270       * closure to the caller.
 271       *
 272       * The closures requirements to work well with this spin method are:
 273       * - Must return false, null or '' if something goes wrong
 274       * - Must return something != false if finishes as expected, this will be the (mixed) value
 275       * returned by spin()
 276       *
 277       * The arguments of the closure are mixed, use $args depending on your needs.
 278       *
 279       * You can provide an exception to give more accurate feedback to tests writers, otherwise the
 280       * closure exception will be used, but you must provide an exception if the closure does not throw
 281       * an exception.
 282       *
 283       * @throws Exception If it timeouts without receiving something != false from the closure
 284       * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method)
 285       * @param mixed $args Arguments to pass to the closure
 286       * @param int $timeout Timeout in seconds
 287       * @param Exception $exception The exception to throw in case it time outs.
 288       * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds.
 289       * @return mixed The value returned by the closure
 290       */
 291      protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) {
 292  
 293          // Using default timeout which is pretty high.
 294          if (!$timeout) {
 295              $timeout = self::TIMEOUT;
 296          }
 297          if ($microsleep) {
 298              // Will sleep 1/10th of a second by default for self::TIMEOUT seconds.
 299              $loops = $timeout * 10;
 300          } else {
 301              // Will sleep for self::TIMEOUT seconds.
 302              $loops = $timeout;
 303          }
 304  
 305          for ($i = 0; $i < $loops; $i++) {
 306              // We catch the exception thrown by the step definition to execute it again.
 307              try {
 308                  // We don't check with !== because most of the time closures will return
 309                  // direct Behat methods returns and we are not sure it will be always (bool)false
 310                  // if it just runs the behat method without returning anything $return == null.
 311                  if ($return = call_user_func($lambda, $this, $args)) {
 312                      return $return;
 313                  }
 314              } catch (Exception $e) {
 315                  // We would use the first closure exception if no exception has been provided.
 316                  if (!$exception) {
 317                      $exception = $e;
 318                  }
 319                  // We wait until no exception is thrown or timeout expires.
 320                  continue;
 321              }
 322  
 323              if ($microsleep) {
 324                  usleep(100000);
 325              } else {
 326                  sleep(1);
 327              }
 328          }
 329  
 330          // Using coding_exception as is a development issue if no exception has been provided.
 331          if (!$exception) {
 332              $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception');
 333          }
 334  
 335          // Throwing exception to the user.
 336          throw $exception;
 337      }
 338  
 339      /**
 340       * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
 341       *
 342       * Use behat_base::get_text_selector_node() for text-based selectors.
 343       *
 344       * @throws ElementNotFoundException Thrown by behat_base::find
 345       * @param string $selectortype
 346       * @param string $element
 347       * @return NodeElement
 348       */
 349      protected function get_selected_node($selectortype, $element) {
 350  
 351          // Getting Mink selector and locator.
 352          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 353  
 354          // Returns the NodeElement.
 355          return $this->find($selector, $locator);
 356      }
 357  
 358      /**
 359       * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
 360       *
 361       * @throws ElementNotFoundException Thrown by behat_base::find
 362       * @param string $selectortype
 363       * @param string $element
 364       * @return NodeElement
 365       */
 366      protected function get_text_selector_node($selectortype, $element) {
 367  
 368          // Getting Mink selector and locator.
 369          list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
 370  
 371          // Returns the NodeElement.
 372          return $this->find($selector, $locator);
 373      }
 374  
 375      /**
 376       * Gets the requested element inside the specified container.
 377       *
 378       * @throws ElementNotFoundException Thrown by behat_base::find
 379       * @param mixed $selectortype The element selector type.
 380       * @param mixed $element The element locator.
 381       * @param mixed $containerselectortype The container selector type.
 382       * @param mixed $containerelement The container locator.
 383       * @return NodeElement
 384       */
 385      protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) {
 386  
 387          // Gets the container, it will always be text based.
 388          $containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
 389  
 390          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 391  
 392          // Specific exception giving info about where can't we find the element.
 393          $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
 394          $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
 395  
 396          // Looks for the requested node inside the container node.
 397          return $this->find($selector, $locator, $exception, $containernode);
 398      }
 399  
 400      /**
 401       * Transforms from step definition's argument style to Mink format.
 402       *
 403       * Mink has 3 different selectors css, xpath and named, where named
 404       * selectors includes link, button, field... to simplify and group multiple
 405       * steps in one we use the same interface, considering all link, buttons...
 406       * at the same level as css selectors and xpath; this method makes the
 407       * conversion from the arguments received by the steps to the selectors and locators
 408       * required to interact with Mink.
 409       *
 410       * @throws ExpectationException
 411       * @param string $selectortype It can be css, xpath or any of the named selectors.
 412       * @param string $element The locator (or string) we are looking for.
 413       * @return array Contains the selector and the locator expected by Mink.
 414       */
 415      protected function transform_selector($selectortype, $element) {
 416  
 417          // Here we don't know if an allowed text selector is being used.
 418          $selectors = behat_selectors::get_allowed_selectors();
 419          if (!isset($selectors[$selectortype])) {
 420              throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
 421          }
 422  
 423          return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession());
 424      }
 425  
 426      /**
 427       * Transforms from step definition's argument style to Mink format.
 428       *
 429       * Delegates all the process to behat_base::transform_selector() checking
 430       * the provided $selectortype.
 431       *
 432       * @throws ExpectationException
 433       * @param string $selectortype It can be css, xpath or any of the named selectors.
 434       * @param string $element The locator (or string) we are looking for.
 435       * @return array Contains the selector and the locator expected by Mink.
 436       */
 437      protected function transform_text_selector($selectortype, $element) {
 438  
 439          $selectors = behat_selectors::get_allowed_text_selectors();
 440          if (empty($selectors[$selectortype])) {
 441              throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession());
 442          }
 443  
 444          return $this->transform_selector($selectortype, $element);
 445      }
 446  
 447      /**
 448       * Returns whether the scenario is running in a browser that can run Javascript or not.
 449       *
 450       * @return boolean
 451       */
 452      protected function running_javascript() {
 453          return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
 454      }
 455  
 456      /**
 457       * Spins around an element until it exists
 458       *
 459       * @throws ExpectationException
 460       * @param string $element
 461       * @param string $selectortype
 462       * @return void
 463       */
 464      protected function ensure_element_exists($element, $selectortype) {
 465  
 466          // Getting the behat selector & locator.
 467          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 468  
 469          // Exception if it timesout and the element is still there.
 470          $msg = 'The "' . $element . '" element does not exist and should exist';
 471          $exception = new ExpectationException($msg, $this->getSession());
 472  
 473          // It will stop spinning once the find() method returns true.
 474          $this->spin(
 475              function($context, $args) {
 476                  // We don't use behat_base::find as it is already spinning.
 477                  if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
 478                      return true;
 479                  }
 480                  return false;
 481              },
 482              array('selector' => $selector, 'locator' => $locator),
 483              self::EXTENDED_TIMEOUT,
 484              $exception,
 485              true
 486          );
 487  
 488      }
 489  
 490      /**
 491       * Spins until the element does not exist
 492       *
 493       * @throws ExpectationException
 494       * @param string $element
 495       * @param string $selectortype
 496       * @return void
 497       */
 498      protected function ensure_element_does_not_exist($element, $selectortype) {
 499  
 500          // Getting the behat selector & locator.
 501          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 502  
 503          // Exception if it timesout and the element is still there.
 504          $msg = 'The "' . $element . '" element exists and should not exist';
 505          $exception = new ExpectationException($msg, $this->getSession());
 506  
 507          // It will stop spinning once the find() method returns false.
 508          $this->spin(
 509              function($context, $args) {
 510                  // We don't use behat_base::find() as we are already spinning.
 511                  if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
 512                      return true;
 513                  }
 514                  return false;
 515              },
 516              array('selector' => $selector, 'locator' => $locator),
 517              self::EXTENDED_TIMEOUT,
 518              $exception,
 519              true
 520          );
 521      }
 522  
 523      /**
 524       * Ensures that the provided node is visible and we can interact with it.
 525       *
 526       * @throws ExpectationException
 527       * @param NodeElement $node
 528       * @return void Throws an exception if it times out without the element being visible
 529       */
 530      protected function ensure_node_is_visible($node) {
 531  
 532          if (!$this->running_javascript()) {
 533              return;
 534          }
 535  
 536          // Exception if it timesout and the element is still there.
 537          $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible';
 538          $exception = new ExpectationException($msg, $this->getSession());
 539  
 540          // It will stop spinning once the isVisible() method returns true.
 541          $this->spin(
 542              function($context, $args) {
 543                  if ($args->isVisible()) {
 544                      return true;
 545                  }
 546                  return false;
 547              },
 548              $node,
 549              self::EXTENDED_TIMEOUT,
 550              $exception,
 551              true
 552          );
 553      }
 554  
 555      /**
 556       * Ensures that the provided element is visible and we can interact with it.
 557       *
 558       * Returns the node in case other actions are interested in using it.
 559       *
 560       * @throws ExpectationException
 561       * @param string $element
 562       * @param string $selectortype
 563       * @return NodeElement Throws an exception if it times out without being visible
 564       */
 565      protected function ensure_element_is_visible($element, $selectortype) {
 566  
 567          if (!$this->running_javascript()) {
 568              return;
 569          }
 570  
 571          $node = $this->get_selected_node($selectortype, $element);
 572          $this->ensure_node_is_visible($node);
 573  
 574          return $node;
 575      }
 576  
 577      /**
 578       * Ensures that all the page's editors are loaded.
 579       *
 580       * @deprecated since Moodle 2.7 MDL-44084 - please do not use this function any more.
 581       * @throws ElementNotFoundException
 582       * @throws ExpectationException
 583       * @return void
 584       */
 585      protected function ensure_editors_are_loaded() {
 586          global $CFG;
 587  
 588          if (empty($CFG->behat_usedeprecated)) {
 589              debugging('Function behat_base::ensure_editors_are_loaded() is deprecated. It is no longer required.');
 590          }
 591          return;
 592      }
 593  
 594      /**
 595       * Change browser window size.
 596       *   - small: 640x480
 597       *   - medium: 1024x768
 598       *   - large: 2560x1600
 599       *
 600       * @param string $windowsize size of window.
 601       * @throws ExpectationException
 602       */
 603      protected function resize_window($windowsize) {
 604          // Non JS don't support resize window.
 605          if (!$this->running_javascript()) {
 606              return;
 607          }
 608  
 609          switch ($windowsize) {
 610              case "small":
 611                  $width = 640;
 612                  $height = 480;
 613                  break;
 614              case "medium":
 615                  $width = 1024;
 616                  $height = 768;
 617                  break;
 618              case "large":
 619                  $width = 2560;
 620                  $height = 1600;
 621                  break;
 622              default:
 623                  preg_match('/^(\d+x\d+)$/', $windowsize, $matches);
 624                  if (empty($matches) || (count($matches) != 2)) {
 625                      throw new ExpectationException("Invalid screen size, can't resize", $this->getSession());
 626                  }
 627                  $size = explode('x', $windowsize);
 628                  $width = (int) $size[0];
 629                  $height = (int) $size[1];
 630          }
 631          $this->getSession()->getDriver()->resizeWindow($width, $height);
 632      }
 633  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1