[ Index ]

PHP Cross Reference of Unnamed Project

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 = '(typeof M !== "undefined" && 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          // Throw exception, so dev knows it is not supported.
 106          if ($selector === 'named') {
 107              $exception = 'Using the "named" selector is deprecated as of 3.1. '
 108                  .' Use the "named_partial" or use the "named_exact" selector instead.';
 109              throw new ExpectationException($exception, $this->getSession());
 110          }
 111  
 112          // Returns the first match.
 113          $items = $this->find_all($selector, $locator, $exception, $node, $timeout);
 114          return count($items) ? reset($items) : null;
 115      }
 116  
 117      /**
 118       * Returns all matching elements.
 119       *
 120       * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method.
 121       *
 122       * @link http://mink.behat.org/#traverse-the-page-selectors
 123       * @param string $selector The selector type (css, xpath, named...)
 124       * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator...
 125       * @param Exception $exception Otherwise we throw expcetion with generic info
 126       * @param NodeElement $node Spins around certain DOM node instead of the whole page
 127       * @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied.
 128       * @return array NodeElements list
 129       */
 130      protected function find_all($selector, $locator, $exception = false, $node = false, $timeout = false) {
 131  
 132          // Throw exception, so dev knows it is not supported.
 133          if ($selector === 'named') {
 134              $exception = 'Using the "named" selector is deprecated as of 3.1. '
 135                  .' Use the "named_partial" or use the "named_exact" selector instead.';
 136              throw new ExpectationException($exception, $this->getSession());
 137          }
 138  
 139          // Generic info.
 140          if (!$exception) {
 141  
 142              // With named selectors we can be more specific.
 143              if (($selector == 'named_exact') || ($selector == 'named_partial')) {
 144                  $exceptiontype = $locator[0];
 145                  $exceptionlocator = $locator[1];
 146  
 147                  // If we are in a @javascript session all contents would be displayed as HTML characters.
 148                  if ($this->running_javascript()) {
 149                      $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES);
 150                  }
 151  
 152              } else {
 153                  $exceptiontype = $selector;
 154                  $exceptionlocator = $locator;
 155              }
 156  
 157              $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator);
 158          }
 159  
 160          $params = array('selector' => $selector, 'locator' => $locator);
 161          // Pushing $node if required.
 162          if ($node) {
 163              $params['node'] = $node;
 164          }
 165  
 166          // How much we will be waiting for the element to appear.
 167          if (!$timeout) {
 168              $timeout = self::TIMEOUT;
 169              $microsleep = false;
 170          } else {
 171              // Spinning each 0.1 seconds if the timeout was forced as we understand
 172              // that is a special case and is good to refine the performance as much
 173              // as possible.
 174              $microsleep = true;
 175          }
 176  
 177          // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception.
 178          return $this->spin(
 179              function($context, $args) {
 180  
 181                  // If no DOM node provided look in all the page.
 182                  if (empty($args['node'])) {
 183                      return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
 184                  }
 185  
 186                  // For nodes contained in other nodes we can not use the basic named selectors
 187                  // as they include unions and they would look for matches in the DOM root.
 188                  $elementxpath = $context->getSession()->getSelectorsHandler()->selectorToXpath($args['selector'], $args['locator']);
 189  
 190                  // Split the xpath in unions and prefix them with the container xpath.
 191                  $unions = explode('|', $elementxpath);
 192                  foreach ($unions as $key => $union) {
 193                      $union = trim($union);
 194  
 195                      // We are in the container node.
 196                      if (strpos($union, '.') === 0) {
 197                          $union = substr($union, 1);
 198                      } else if (strpos($union, '/') !== 0) {
 199                          // Adding the path separator in case it is not there.
 200                          $union = '/' . $union;
 201                      }
 202                      $unions[$key] = $args['node']->getXpath() . $union;
 203                  }
 204  
 205                  // We can not use usual Element::find() as it prefixes with DOM root.
 206                  return $context->getSession()->getDriver()->find(implode('|', $unions));
 207              },
 208              $params,
 209              $timeout,
 210              $exception,
 211              $microsleep
 212          );
 213      }
 214  
 215      /**
 216       * Finds DOM nodes in the page using named selectors.
 217       *
 218       * The point of using this method instead of Mink ones is the spin
 219       * method of behat_base::find() that looks for the element until it
 220       * is available or it timeouts, this avoids the false failures received
 221       * when selenium tries to execute commands on elements that are not
 222       * ready to be used.
 223       *
 224       * All steps that requires elements to be available before interact with
 225       * them should use one of the find* methods.
 226       *
 227       * The methods calls requires a {'find_' . $elementtype}($locator)
 228       * format, like find_link($locator), find_select($locator),
 229       * find_button($locator)...
 230       *
 231       * @link http://mink.behat.org/#named-selectors
 232       * @throws coding_exception
 233       * @param string $name The name of the called method
 234       * @param mixed $arguments
 235       * @return NodeElement
 236       */
 237      public function __call($name, $arguments) {
 238  
 239          if (substr($name, 0, 5) !== 'find_') {
 240              throw new coding_exception('The "' . $name . '" method does not exist');
 241          }
 242  
 243          // Only the named selector identifier.
 244          $cleanname = substr($name, 5);
 245  
 246          // All named selectors shares the interface.
 247          if (count($arguments) !== 1) {
 248              throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument');
 249          }
 250  
 251          // Redirecting execution to the find method with the specified selector.
 252          // It will detect if it's pointing to an unexisting named selector.
 253          return $this->find('named_partial',
 254              array(
 255                  $cleanname,
 256                  behat_context_helper::escape($arguments[0])
 257              )
 258          );
 259      }
 260  
 261      /**
 262       * Escapes the double quote character.
 263       *
 264       * Double quote is the argument delimiter, it can be escaped
 265       * with a backslash, but we auto-remove this backslashes
 266       * before the step execution, this method is useful when using
 267       * arguments as arguments for other steps.
 268       *
 269       * @param string $string
 270       * @return string
 271       */
 272      public function escape($string) {
 273          return str_replace('"', '\"', $string);
 274      }
 275  
 276      /**
 277       * Executes the passed closure until returns true or time outs.
 278       *
 279       * In most cases the document.readyState === 'complete' will be enough, but sometimes JS
 280       * requires more time to be completely loaded or an element to be visible or whatever is required to
 281       * perform some action on an element; this method receives a closure which should contain the
 282       * required statements to ensure the step definition actions and assertions have all their needs
 283       * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the
 284       * closure to the caller.
 285       *
 286       * The closures requirements to work well with this spin method are:
 287       * - Must return false, null or '' if something goes wrong
 288       * - Must return something != false if finishes as expected, this will be the (mixed) value
 289       * returned by spin()
 290       *
 291       * The arguments of the closure are mixed, use $args depending on your needs.
 292       *
 293       * You can provide an exception to give more accurate feedback to tests writers, otherwise the
 294       * closure exception will be used, but you must provide an exception if the closure does not throw
 295       * an exception.
 296       *
 297       * @throws Exception If it timeouts without receiving something != false from the closure
 298       * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method)
 299       * @param mixed $args Arguments to pass to the closure
 300       * @param int $timeout Timeout in seconds
 301       * @param Exception $exception The exception to throw in case it time outs.
 302       * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds.
 303       * @return mixed The value returned by the closure
 304       */
 305      protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) {
 306  
 307          // Using default timeout which is pretty high.
 308          if (!$timeout) {
 309              $timeout = self::TIMEOUT;
 310          }
 311          if ($microsleep) {
 312              // Will sleep 1/10th of a second by default for self::TIMEOUT seconds.
 313              $loops = $timeout * 10;
 314          } else {
 315              // Will sleep for self::TIMEOUT seconds.
 316              $loops = $timeout;
 317          }
 318  
 319          // DOM will never change on non-javascript case; do not wait or try again.
 320          if (!$this->running_javascript()) {
 321              $loops = 1;
 322          }
 323  
 324          for ($i = 0; $i < $loops; $i++) {
 325              // We catch the exception thrown by the step definition to execute it again.
 326              try {
 327                  // We don't check with !== because most of the time closures will return
 328                  // direct Behat methods returns and we are not sure it will be always (bool)false
 329                  // if it just runs the behat method without returning anything $return == null.
 330                  if ($return = call_user_func($lambda, $this, $args)) {
 331                      return $return;
 332                  }
 333              } catch (Exception $e) {
 334                  // We would use the first closure exception if no exception has been provided.
 335                  if (!$exception) {
 336                      $exception = $e;
 337                  }
 338                  // We wait until no exception is thrown or timeout expires.
 339                  continue;
 340              }
 341  
 342              if ($this->running_javascript()) {
 343                  if ($microsleep) {
 344                      usleep(100000);
 345                  } else {
 346                      sleep(1);
 347                  }
 348              }
 349          }
 350  
 351          // Using coding_exception as is a development issue if no exception has been provided.
 352          if (!$exception) {
 353              $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception');
 354          }
 355  
 356          // Throwing exception to the user.
 357          throw $exception;
 358      }
 359  
 360      /**
 361       * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
 362       *
 363       * Use behat_base::get_text_selector_node() for text-based selectors.
 364       *
 365       * @throws ElementNotFoundException Thrown by behat_base::find
 366       * @param string $selectortype
 367       * @param string $element
 368       * @return NodeElement
 369       */
 370      protected function get_selected_node($selectortype, $element) {
 371  
 372          // Getting Mink selector and locator.
 373          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 374  
 375          // Returns the NodeElement.
 376          return $this->find($selector, $locator);
 377      }
 378  
 379      /**
 380       * Gets a NodeElement based on the locator and selector type received as argument from steps definitions.
 381       *
 382       * @throws ElementNotFoundException Thrown by behat_base::find
 383       * @param string $selectortype
 384       * @param string $element
 385       * @return NodeElement
 386       */
 387      protected function get_text_selector_node($selectortype, $element) {
 388  
 389          // Getting Mink selector and locator.
 390          list($selector, $locator) = $this->transform_text_selector($selectortype, $element);
 391  
 392          // Returns the NodeElement.
 393          return $this->find($selector, $locator);
 394      }
 395  
 396      /**
 397       * Gets the requested element inside the specified container.
 398       *
 399       * @throws ElementNotFoundException Thrown by behat_base::find
 400       * @param mixed $selectortype The element selector type.
 401       * @param mixed $element The element locator.
 402       * @param mixed $containerselectortype The container selector type.
 403       * @param mixed $containerelement The container locator.
 404       * @return NodeElement
 405       */
 406      protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) {
 407  
 408          // Gets the container, it will always be text based.
 409          $containernode = $this->get_text_selector_node($containerselectortype, $containerelement);
 410  
 411          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 412  
 413          // Specific exception giving info about where can't we find the element.
 414          $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
 415          $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
 416  
 417          // Looks for the requested node inside the container node.
 418          return $this->find($selector, $locator, $exception, $containernode);
 419      }
 420  
 421      /**
 422       * Transforms from step definition's argument style to Mink format.
 423       *
 424       * Mink has 3 different selectors css, xpath and named, where named
 425       * selectors includes link, button, field... to simplify and group multiple
 426       * steps in one we use the same interface, considering all link, buttons...
 427       * at the same level as css selectors and xpath; this method makes the
 428       * conversion from the arguments received by the steps to the selectors and locators
 429       * required to interact with Mink.
 430       *
 431       * @throws ExpectationException
 432       * @param string $selectortype It can be css, xpath or any of the named selectors.
 433       * @param string $element The locator (or string) we are looking for.
 434       * @return array Contains the selector and the locator expected by Mink.
 435       */
 436      protected function transform_selector($selectortype, $element) {
 437  
 438          // Here we don't know if an allowed text selector is being used.
 439          $selectors = behat_selectors::get_allowed_selectors();
 440          if (!isset($selectors[$selectortype])) {
 441              throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
 442          }
 443  
 444          return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession());
 445      }
 446  
 447      /**
 448       * Transforms from step definition's argument style to Mink format.
 449       *
 450       * Delegates all the process to behat_base::transform_selector() checking
 451       * the provided $selectortype.
 452       *
 453       * @throws ExpectationException
 454       * @param string $selectortype It can be css, xpath or any of the named selectors.
 455       * @param string $element The locator (or string) we are looking for.
 456       * @return array Contains the selector and the locator expected by Mink.
 457       */
 458      protected function transform_text_selector($selectortype, $element) {
 459  
 460          $selectors = behat_selectors::get_allowed_text_selectors();
 461          if (empty($selectors[$selectortype])) {
 462              throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession());
 463          }
 464  
 465          return $this->transform_selector($selectortype, $element);
 466      }
 467  
 468      /**
 469       * Returns whether the scenario is running in a browser that can run Javascript or not.
 470       *
 471       * @return boolean
 472       */
 473      protected function running_javascript() {
 474          return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
 475      }
 476  
 477      /**
 478       * Spins around an element until it exists
 479       *
 480       * @throws ExpectationException
 481       * @param string $element
 482       * @param string $selectortype
 483       * @return void
 484       */
 485      protected function ensure_element_exists($element, $selectortype) {
 486  
 487          // Getting the behat selector & locator.
 488          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 489  
 490          // Exception if it timesout and the element is still there.
 491          $msg = 'The "' . $element . '" element does not exist and should exist';
 492          $exception = new ExpectationException($msg, $this->getSession());
 493  
 494          // It will stop spinning once the find() method returns true.
 495          $this->spin(
 496              function($context, $args) {
 497                  // We don't use behat_base::find as it is already spinning.
 498                  if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
 499                      return true;
 500                  }
 501                  return false;
 502              },
 503              array('selector' => $selector, 'locator' => $locator),
 504              self::EXTENDED_TIMEOUT,
 505              $exception,
 506              true
 507          );
 508  
 509      }
 510  
 511      /**
 512       * Spins until the element does not exist
 513       *
 514       * @throws ExpectationException
 515       * @param string $element
 516       * @param string $selectortype
 517       * @return void
 518       */
 519      protected function ensure_element_does_not_exist($element, $selectortype) {
 520  
 521          // Getting the behat selector & locator.
 522          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 523  
 524          // Exception if it timesout and the element is still there.
 525          $msg = 'The "' . $element . '" element exists and should not exist';
 526          $exception = new ExpectationException($msg, $this->getSession());
 527  
 528          // It will stop spinning once the find() method returns false.
 529          $this->spin(
 530              function($context, $args) {
 531                  // We don't use behat_base::find() as we are already spinning.
 532                  if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
 533                      return true;
 534                  }
 535                  return false;
 536              },
 537              array('selector' => $selector, 'locator' => $locator),
 538              self::EXTENDED_TIMEOUT,
 539              $exception,
 540              true
 541          );
 542      }
 543  
 544      /**
 545       * Ensures that the provided node is visible and we can interact with it.
 546       *
 547       * @throws ExpectationException
 548       * @param NodeElement $node
 549       * @return void Throws an exception if it times out without the element being visible
 550       */
 551      protected function ensure_node_is_visible($node) {
 552  
 553          if (!$this->running_javascript()) {
 554              return;
 555          }
 556  
 557          // Exception if it timesout and the element is still there.
 558          $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible';
 559          $exception = new ExpectationException($msg, $this->getSession());
 560  
 561          // It will stop spinning once the isVisible() method returns true.
 562          $this->spin(
 563              function($context, $args) {
 564                  if ($args->isVisible()) {
 565                      return true;
 566                  }
 567                  return false;
 568              },
 569              $node,
 570              self::EXTENDED_TIMEOUT,
 571              $exception,
 572              true
 573          );
 574      }
 575  
 576      /**
 577       * Ensures that the provided element is visible and we can interact with it.
 578       *
 579       * Returns the node in case other actions are interested in using it.
 580       *
 581       * @throws ExpectationException
 582       * @param string $element
 583       * @param string $selectortype
 584       * @return NodeElement Throws an exception if it times out without being visible
 585       */
 586      protected function ensure_element_is_visible($element, $selectortype) {
 587  
 588          if (!$this->running_javascript()) {
 589              return;
 590          }
 591  
 592          $node = $this->get_selected_node($selectortype, $element);
 593          $this->ensure_node_is_visible($node);
 594  
 595          return $node;
 596      }
 597  
 598      /**
 599       * Ensures that all the page's editors are loaded.
 600       *
 601       * @deprecated since Moodle 2.7 MDL-44084 - please do not use this function any more.
 602       * @throws ElementNotFoundException
 603       * @throws ExpectationException
 604       * @return void
 605       */
 606      protected function ensure_editors_are_loaded() {
 607          global $CFG;
 608  
 609          if (empty($CFG->behat_usedeprecated)) {
 610              debugging('Function behat_base::ensure_editors_are_loaded() is deprecated. It is no longer required.');
 611          }
 612          return;
 613      }
 614  
 615      /**
 616       * Change browser window size.
 617       *   - small: 640x480
 618       *   - medium: 1024x768
 619       *   - large: 2560x1600
 620       *
 621       * @param string $windowsize size of window.
 622       * @param bool $viewport If true, changes viewport rather than window size
 623       * @throws ExpectationException
 624       */
 625      protected function resize_window($windowsize, $viewport = false) {
 626          // Non JS don't support resize window.
 627          if (!$this->running_javascript()) {
 628              return;
 629          }
 630  
 631          switch ($windowsize) {
 632              case "small":
 633                  $width = 640;
 634                  $height = 480;
 635                  break;
 636              case "medium":
 637                  $width = 1024;
 638                  $height = 768;
 639                  break;
 640              case "large":
 641                  $width = 2560;
 642                  $height = 1600;
 643                  break;
 644              default:
 645                  preg_match('/^(\d+x\d+)$/', $windowsize, $matches);
 646                  if (empty($matches) || (count($matches) != 2)) {
 647                      throw new ExpectationException("Invalid screen size, can't resize", $this->getSession());
 648                  }
 649                  $size = explode('x', $windowsize);
 650                  $width = (int) $size[0];
 651                  $height = (int) $size[1];
 652          }
 653          if ($viewport) {
 654              // When setting viewport size, we set it so that the document width will be exactly
 655              // as specified, assuming that there is a vertical scrollbar. (In cases where there is
 656              // no scrollbar it will be slightly wider. We presume this is rare and predictable.)
 657              // The window inner height will be as specified, which means the available viewport will
 658              // actually be smaller if there is a horizontal scrollbar. We assume that horizontal
 659              // scrollbars are rare so this doesn't matter.
 660              $offset = $this->getSession()->getDriver()->evaluateScript(
 661                      'return (function() { var before = document.body.style.overflowY;' .
 662                      'document.body.style.overflowY = "scroll";' .
 663                      'var result = {};' .
 664                      'result.x = window.outerWidth - document.body.offsetWidth;' .
 665                      'result.y = window.outerHeight - window.innerHeight;' .
 666                      'document.body.style.overflowY = before;' .
 667                      'return result; })();');
 668              $width += $offset['x'];
 669              $height += $offset['y'];
 670          }
 671  
 672          $this->getSession()->getDriver()->resizeWindow($width, $height);
 673      }
 674  
 675      /**
 676       * Waits for all the JS to be loaded.
 677       *
 678       * @throws \Exception
 679       * @throws NoSuchWindow
 680       * @throws UnknownError
 681       * @return bool True or false depending whether all the JS is loaded or not.
 682       */
 683      public function wait_for_pending_js() {
 684          // Waiting for JS is only valid for JS scenarios.
 685          if (!$this->running_javascript()) {
 686              return;
 687          }
 688  
 689          // We don't use behat_base::spin() here as we don't want to end up with an exception
 690          // if the page & JSs don't finish loading properly.
 691          for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
 692              $pending = '';
 693              try {
 694                  $jscode = '
 695                      return function() {
 696                          if (typeof M === "undefined") {
 697                              if (document.readyState === "complete") {
 698                                  return "";
 699                              } else {
 700                                  return "incomplete";
 701                              }
 702                          } else if (' . self::PAGE_READY_JS . ') {
 703                              return "";
 704                          } else if (typeof M.util !== "undefined") {
 705                              return M.util.pending_js.join(":");
 706                          } else {
 707                              return "incomplete"
 708                          }
 709                      }();';
 710                  $pending = $this->getSession()->evaluateScript($jscode);
 711              } catch (NoSuchWindow $nsw) {
 712                  // We catch an exception here, in case we just closed the window we were interacting with.
 713                  // No javascript is running if there is no window right?
 714                  $pending = '';
 715              } catch (UnknownError $e) {
 716                  // M is not defined when the window or the frame don't exist anymore.
 717                  if (strstr($e->getMessage(), 'M is not defined') != false) {
 718                      $pending = '';
 719                  }
 720              }
 721  
 722              // If there are no pending JS we stop waiting.
 723              if ($pending === '') {
 724                  return true;
 725              }
 726  
 727              // 0.1 seconds.
 728              usleep(100000);
 729          }
 730  
 731          // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions().
 732          // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds
 733          // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the
 734          // number of JS pending code and JS completed code will not match and we will reach this point.
 735          throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT .
 736              ' seconds. There is a Javascript error or the code is extremely slow.');
 737      }
 738  
 739      /**
 740       * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
 741       *
 742       * Part of behat_hooks class as is part of the testing framework, is auto-executed
 743       * after each step so no features will splicitly use it.
 744       *
 745       * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception.
 746       * @see Moodle\BehatExtension\Tester\MoodleStepTester
 747       */
 748      public function look_for_exceptions() {
 749          // Wrap in try in case we were interacting with a closed window.
 750          try {
 751  
 752              // Exceptions.
 753              $exceptionsxpath = "//div[@data-rel='fatalerror']";
 754              // Debugging messages.
 755              $debuggingxpath = "//div[@data-rel='debugging']";
 756              // PHP debug messages.
 757              $phperrorxpath = "//div[@data-rel='phpdebugmessage']";
 758              // Any other backtrace.
 759              $othersxpath = "(//*[contains(., ': call to ')])[1]";
 760  
 761              $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath);
 762              $joinedxpath = implode(' | ', $xpaths);
 763  
 764              // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check
 765              // is faster than to send the 4 xpath queries for each step.
 766              if (!$this->getSession()->getDriver()->find($joinedxpath)) {
 767                  return;
 768              }
 769  
 770              // Exceptions.
 771              if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) {
 772  
 773                  // Getting the debugging info and the backtrace.
 774                  $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error');
 775                  // If errorinfoboxes is empty, try find notifytiny (original) class.
 776                  if (empty($errorinfoboxes)) {
 777                      $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
 778                  }
 779                  $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
 780                      $this->get_debug_text($errorinfoboxes[1]->getHtml());
 781  
 782                  $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
 783                  throw new \Exception(html_entity_decode($msg));
 784              }
 785  
 786              // Debugging messages.
 787              if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) {
 788                  $msgs = array();
 789                  foreach ($debuggingmessages as $debuggingmessage) {
 790                      $msgs[] = $this->get_debug_text($debuggingmessage->getHtml());
 791                  }
 792                  $msg = "debugging() message/s found:\n" . implode("\n", $msgs);
 793                  throw new \Exception(html_entity_decode($msg));
 794              }
 795  
 796              // PHP debug messages.
 797              if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) {
 798  
 799                  $msgs = array();
 800                  foreach ($phpmessages as $phpmessage) {
 801                      $msgs[] = $this->get_debug_text($phpmessage->getHtml());
 802                  }
 803                  $msg = "PHP debug message/s found:\n" . implode("\n", $msgs);
 804                  throw new \Exception(html_entity_decode($msg));
 805              }
 806  
 807              // Any other backtrace.
 808              // First looking through xpath as it is faster than get and parse the whole page contents,
 809              // we get the contents and look for matches once we found something to suspect that there is a backtrace.
 810              if ($this->getSession()->getDriver()->find($othersxpath)) {
 811                  $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/';
 812                  if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) {
 813                      $msgs = array();
 814                      foreach ($backtraces[0] as $backtrace) {
 815                          $msgs[] = $backtrace . '()';
 816                      }
 817                      $msg = "Other backtraces found:\n" . implode("\n", $msgs);
 818                      throw new \Exception(htmlentities($msg));
 819                  }
 820              }
 821  
 822          } catch (NoSuchWindow $e) {
 823              // If we were interacting with a popup window it will not exists after closing it.
 824          }
 825      }
 826  
 827      /**
 828       * Converts HTML tags to line breaks to display the info in CLI
 829       *
 830       * @param string $html
 831       * @return string
 832       */
 833      protected function get_debug_text($html) {
 834  
 835          // Replacing HTML tags for new lines and keeping only the text.
 836          $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html);
 837          return preg_replace("/(\n)+/s", "\n", $notags);
 838      }
 839  
 840      /**
 841       * Helper function to execute api in a given context.
 842       *
 843       * @param string $contextapi context in which api is defined.
 844       * @param array $params list of params to pass.
 845       * @throws Exception
 846       */
 847      protected function execute($contextapi, $params = array()) {
 848          if (!is_array($params)) {
 849              $params = array($params);
 850          }
 851  
 852          // Get required context and execute the api.
 853          $contextapi = explode("::", $contextapi);
 854          $context = behat_context_helper::get($contextapi[0]);
 855          call_user_func_array(array($context, $contextapi[1]), $params);
 856  
 857          // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results.
 858          // Don't make them optional for performance reasons.
 859  
 860          // Wait for pending js.
 861          $this->wait_for_pending_js();
 862  
 863          // Look for exceptions.
 864          $this->look_for_exceptions();
 865      }
 866  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1