[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/tests/behat/ -> behat_general.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   * General use steps definitions.
  19   *
  20   * @package   core
  21   * @category  test
  22   * @copyright 2012 David Monllaó
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  27  
  28  require_once (__DIR__ . '/../../behat/behat_base.php');
  29  
  30  use Behat\Mink\Exception\ExpectationException as ExpectationException,
  31      Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
  32      Behat\Mink\Exception\DriverException as DriverException,
  33      WebDriver\Exception\NoSuchElement as NoSuchElement,
  34      WebDriver\Exception\StaleElementReference as StaleElementReference,
  35      Behat\Gherkin\Node\TableNode as TableNode;
  36  
  37  /**
  38   * Cross component steps definitions.
  39   *
  40   * Basic web application definitions from MinkExtension and
  41   * BehatchExtension. Definitions modified according to our needs
  42   * when necessary and including only the ones we need to avoid
  43   * overlapping and confusion.
  44   *
  45   * @package   core
  46   * @category  test
  47   * @copyright 2012 David Monllaó
  48   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class behat_general extends behat_base {
  51  
  52      /**
  53       * @var string used by {@link switch_to_window()} and
  54       * {@link switch_to_the_main_window()} to work-around a Chrome browser issue.
  55       */
  56      const MAIN_WINDOW_NAME = '__moodle_behat_main_window_name';
  57  
  58      /**
  59       * @var string when we want to check whether or not a new page has loaded,
  60       * we first write this unique string into the page. Then later, by checking
  61       * whether it is still there, we can tell if a new page has been loaded.
  62       */
  63      const PAGE_LOAD_DETECTION_STRING = 'new_page_not_loaded_since_behat_started_watching';
  64  
  65      /**
  66       * @var $pageloaddetectionrunning boolean Used to ensure that page load detection was started before a page reload
  67       * was checked for.
  68       */
  69      private $pageloaddetectionrunning = false;
  70  
  71      /**
  72       * Opens Moodle homepage.
  73       *
  74       * @Given /^I am on homepage$/
  75       */
  76      public function i_am_on_homepage() {
  77          $this->getSession()->visit($this->locate_path('/'));
  78      }
  79  
  80      /**
  81       * Opens Moodle site homepage.
  82       *
  83       * @Given /^I am on site homepage$/
  84       */
  85      public function i_am_on_site_homepage() {
  86          $this->getSession()->visit($this->locate_path('/?redirect=0'));
  87      }
  88  
  89      /**
  90       * Reloads the current page.
  91       *
  92       * @Given /^I reload the page$/
  93       */
  94      public function reload() {
  95          $this->getSession()->reload();
  96      }
  97  
  98      /**
  99       * Follows the page redirection. Use this step after any action that shows a message and waits for a redirection
 100       *
 101       * @Given /^I wait to be redirected$/
 102       */
 103      public function i_wait_to_be_redirected() {
 104  
 105          // Xpath and processes based on core_renderer::redirect_message(), core_renderer::$metarefreshtag and
 106          // moodle_page::$periodicrefreshdelay possible values.
 107          if (!$metarefresh = $this->getSession()->getPage()->find('xpath', "//head/descendant::meta[@http-equiv='refresh']")) {
 108              // We don't fail the scenario if no redirection with message is found to avoid race condition false failures.
 109              return true;
 110          }
 111  
 112          // Wrapped in try & catch in case the redirection has already been executed.
 113          try {
 114              $content = $metarefresh->getAttribute('content');
 115          } catch (NoSuchElement $e) {
 116              return true;
 117          } catch (StaleElementReference $e) {
 118              return true;
 119          }
 120  
 121          // Getting the refresh time and the url if present.
 122          if (strstr($content, 'url') != false) {
 123  
 124              list($waittime, $url) = explode(';', $content);
 125  
 126              // Cleaning the URL value.
 127              $url = trim(substr($url, strpos($url, 'http')));
 128  
 129          } else {
 130              // Just wait then.
 131              $waittime = $content;
 132          }
 133  
 134  
 135          // Wait until the URL change is executed.
 136          if ($this->running_javascript()) {
 137              $this->getSession()->wait($waittime * 1000, false);
 138  
 139          } else if (!empty($url)) {
 140              // We redirect directly as we can not wait for an automatic redirection.
 141              $this->getSession()->getDriver()->getClient()->request('get', $url);
 142  
 143          } else {
 144              // Reload the page if no URL was provided.
 145              $this->getSession()->getDriver()->reload();
 146          }
 147      }
 148  
 149      /**
 150       * Switches to the specified iframe.
 151       *
 152       * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe$/
 153       * @param string $iframename
 154       */
 155      public function switch_to_iframe($iframename) {
 156  
 157          // We spin to give time to the iframe to be loaded.
 158          // Using extended timeout as we don't know about which
 159          // kind of iframe will be loaded.
 160          $this->spin(
 161              function($context, $iframename) {
 162                  $context->getSession()->switchToIFrame($iframename);
 163  
 164                  // If no exception we are done.
 165                  return true;
 166              },
 167              $iframename,
 168              self::EXTENDED_TIMEOUT
 169          );
 170      }
 171  
 172      /**
 173       * Switches to the main Moodle frame.
 174       *
 175       * @Given /^I switch to the main frame$/
 176       */
 177      public function switch_to_the_main_frame() {
 178          $this->getSession()->switchToIFrame();
 179      }
 180  
 181      /**
 182       * Switches to the specified window. Useful when interacting with popup windows.
 183       *
 184       * @Given /^I switch to "(?P<window_name_string>(?:[^"]|\\")*)" window$/
 185       * @param string $windowname
 186       */
 187      public function switch_to_window($windowname) {
 188          // In Behat, some browsers (e.g. Chrome) are unable to switch to a
 189          // window without a name, and by default the main browser window does
 190          // not have a name. To work-around this, when we switch away from an
 191          // unnamed window (presumably the main window) to some other named
 192          // window, then we first set the main window name to a conventional
 193          // value that we can later use this name to switch back.
 194          $this->getSession()->executeScript(
 195                  'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
 196  
 197          $this->getSession()->switchToWindow($windowname);
 198      }
 199  
 200      /**
 201       * Switches to the main Moodle window. Useful when you finish interacting with popup windows.
 202       *
 203       * @Given /^I switch to the main window$/
 204       */
 205      public function switch_to_the_main_window() {
 206          $this->getSession()->switchToWindow(self::MAIN_WINDOW_NAME);
 207      }
 208  
 209      /**
 210       * Accepts the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
 211       * @Given /^I accept the currently displayed dialog$/
 212       */
 213      public function accept_currently_displayed_alert_dialog() {
 214          $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
 215      }
 216  
 217      /**
 218       * Dismisses the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
 219       * @Given /^I dismiss the currently displayed dialog$/
 220       */
 221      public function dismiss_currently_displayed_alert_dialog() {
 222          $this->getSession()->getDriver()->getWebDriverSession()->dismiss_alert();
 223      }
 224  
 225      /**
 226       * Clicks link with specified id|title|alt|text.
 227       *
 228       * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
 229       * @throws ElementNotFoundException Thrown by behat_base::find
 230       * @param string $link
 231       */
 232      public function click_link($link) {
 233  
 234          $linknode = $this->find_link($link);
 235          $this->ensure_node_is_visible($linknode);
 236          $linknode->click();
 237      }
 238  
 239      /**
 240       * Waits X seconds. Required after an action that requires data from an AJAX request.
 241       *
 242       * @Then /^I wait "(?P<seconds_number>\d+)" seconds$/
 243       * @param int $seconds
 244       */
 245      public function i_wait_seconds($seconds) {
 246          if ($this->running_javascript()) {
 247              $this->getSession()->wait($seconds * 1000, false);
 248          } else {
 249              sleep($seconds);
 250          }
 251      }
 252  
 253      /**
 254       * Waits until the page is completely loaded. This step is auto-executed after every step.
 255       *
 256       * @Given /^I wait until the page is ready$/
 257       */
 258      public function wait_until_the_page_is_ready() {
 259  
 260          // No need to wait if not running JS.
 261          if (!$this->running_javascript()) {
 262              return;
 263          }
 264  
 265          $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
 266      }
 267  
 268      /**
 269       * Waits until the provided element selector exists in the DOM
 270       *
 271       * Using the protected method as this method will be usually
 272       * called by other methods which are not returning a set of
 273       * steps and performs the actions directly, so it would not
 274       * be executed if it returns another step.
 275  
 276       * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
 277       * @param string $element
 278       * @param string $selector
 279       * @return void
 280       */
 281      public function wait_until_exists($element, $selectortype) {
 282          $this->ensure_element_exists($element, $selectortype);
 283      }
 284  
 285      /**
 286       * Waits until the provided element does not exist in the DOM
 287       *
 288       * Using the protected method as this method will be usually
 289       * called by other methods which are not returning a set of
 290       * steps and performs the actions directly, so it would not
 291       * be executed if it returns another step.
 292  
 293       * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
 294       * @param string $element
 295       * @param string $selector
 296       * @return void
 297       */
 298      public function wait_until_does_not_exists($element, $selectortype) {
 299          $this->ensure_element_does_not_exist($element, $selectortype);
 300      }
 301  
 302      /**
 303       * Generic mouse over action. Mouse over a element of the specified type.
 304       *
 305       * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
 306       * @param string $element Element we look for
 307       * @param string $selectortype The type of what we look for
 308       */
 309      public function i_hover($element, $selectortype) {
 310  
 311          // Gets the node based on the requested selector type and locator.
 312          $node = $this->get_selected_node($selectortype, $element);
 313          $node->mouseOver();
 314      }
 315  
 316      /**
 317       * Generic click action. Click on the element of the specified type.
 318       *
 319       * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
 320       * @param string $element Element we look for
 321       * @param string $selectortype The type of what we look for
 322       */
 323      public function i_click_on($element, $selectortype) {
 324  
 325          // Gets the node based on the requested selector type and locator.
 326          $node = $this->get_selected_node($selectortype, $element);
 327          $this->ensure_node_is_visible($node);
 328          $node->click();
 329      }
 330  
 331      /**
 332       * Sets the focus and takes away the focus from an element, generating blur JS event.
 333       *
 334       * @When /^I take focus off "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
 335       * @param string $element Element we look for
 336       * @param string $selectortype The type of what we look for
 337       */
 338      public function i_take_focus_off_field($element, $selectortype) {
 339          if (!$this->running_javascript()) {
 340              throw new ExpectationException('Can\'t take focus off from "' . $element . '" in non-js mode', $this->getSession());
 341          }
 342          // Gets the node based on the requested selector type and locator.
 343          $node = $this->get_selected_node($selectortype, $element);
 344          $this->ensure_node_is_visible($node);
 345  
 346          // Ensure element is focused before taking it off.
 347          $node->focus();
 348          $node->blur();
 349      }
 350  
 351      /**
 352       * Clicks the specified element and confirms the expected dialogue.
 353       *
 354       * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" confirming the dialogue$/
 355       * @throws ElementNotFoundException Thrown by behat_base::find
 356       * @param string $element Element we look for
 357       * @param string $selectortype The type of what we look for
 358       */
 359      public function i_click_on_confirming_the_dialogue($element, $selectortype) {
 360          $this->i_click_on($element, $selectortype);
 361          $this->accept_currently_displayed_alert_dialog();
 362      }
 363  
 364      /**
 365       * Clicks the specified element and dismissing the expected dialogue.
 366       *
 367       * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" dismissing the dialogue$/
 368       * @throws ElementNotFoundException Thrown by behat_base::find
 369       * @param string $element Element we look for
 370       * @param string $selectortype The type of what we look for
 371       */
 372      public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
 373          $this->i_click_on($element, $selectortype);
 374          $this->dismiss_currently_displayed_alert_dialog();
 375      }
 376  
 377      /**
 378       * Click on the element of the specified type which is located inside the second element.
 379       *
 380       * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
 381       * @param string $element Element we look for
 382       * @param string $selectortype The type of what we look for
 383       * @param string $nodeelement Element we look in
 384       * @param string $nodeselectortype The type of selector where we look in
 385       */
 386      public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {
 387  
 388          $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
 389          $this->ensure_node_is_visible($node);
 390          $node->click();
 391      }
 392  
 393      /**
 394       * Drags and drops the specified element to the specified container. This step does not work in all the browsers, consider it experimental.
 395       *
 396       * The steps definitions calling this step as part of them should
 397       * manage the wait times by themselves as the times and when the
 398       * waits should be done depends on what is being dragged & dropper.
 399       *
 400       * @Given /^I drag "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" and I drop it in "(?P<container_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
 401       * @param string $element
 402       * @param string $selectortype
 403       * @param string $containerelement
 404       * @param string $containerselectortype
 405       */
 406      public function i_drag_and_i_drop_it_in($element, $selectortype, $containerelement, $containerselectortype) {
 407  
 408          list($sourceselector, $sourcelocator) = $this->transform_selector($selectortype, $element);
 409          $sourcexpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($sourceselector, $sourcelocator);
 410  
 411          list($containerselector, $containerlocator) = $this->transform_selector($containerselectortype, $containerelement);
 412          $destinationxpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($containerselector, $containerlocator);
 413  
 414          $node = $this->get_selected_node("xpath_element", $sourcexpath);
 415          if (!$node->isVisible()) {
 416              throw new ExpectationException('"' . $sourcexpath . '" "xpath_element" is not visible', $this->getSession());
 417          }
 418          $node = $this->get_selected_node("xpath_element", $destinationxpath);
 419          if (!$node->isVisible()) {
 420              throw new ExpectationException('"' . $destinationxpath . '" "xpath_element" is not visible', $this->getSession());
 421          }
 422  
 423          $this->getSession()->getDriver()->dragTo($sourcexpath, $destinationxpath);
 424      }
 425  
 426      /**
 427       * Checks, that the specified element is visible. Only available in tests using Javascript.
 428       *
 429       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should be visible$/
 430       * @throws ElementNotFoundException
 431       * @throws ExpectationException
 432       * @throws DriverException
 433       * @param string $element
 434       * @param string $selectortype
 435       * @return void
 436       */
 437      public function should_be_visible($element, $selectortype) {
 438  
 439          if (!$this->running_javascript()) {
 440              throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
 441          }
 442  
 443          $node = $this->get_selected_node($selectortype, $element);
 444          if (!$node->isVisible()) {
 445              throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is not visible', $this->getSession());
 446          }
 447      }
 448  
 449      /**
 450       * Checks, that the existing element is not visible. Only available in tests using Javascript.
 451       *
 452       * As a "not" method, it's performance could not be good, but in this
 453       * case the performance is good because the element must exist,
 454       * otherwise there would be a ElementNotFoundException, also here we are
 455       * not spinning until the element is visible.
 456       *
 457       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should not be visible$/
 458       * @throws ElementNotFoundException
 459       * @throws ExpectationException
 460       * @param string $element
 461       * @param string $selectortype
 462       * @return void
 463       */
 464      public function should_not_be_visible($element, $selectortype) {
 465  
 466          try {
 467              $this->should_be_visible($element, $selectortype);
 468          } catch (ExpectationException $e) {
 469              // All as expected.
 470              return;
 471          }
 472          throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is visible', $this->getSession());
 473      }
 474  
 475      /**
 476       * Checks, that the specified element is visible inside the specified container. Only available in tests using Javascript.
 477       *
 478       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should be visible$/
 479       * @throws ElementNotFoundException
 480       * @throws DriverException
 481       * @throws ExpectationException
 482       * @param string $element Element we look for
 483       * @param string $selectortype The type of what we look for
 484       * @param string $nodeelement Element we look in
 485       * @param string $nodeselectortype The type of selector where we look in
 486       */
 487      public function in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
 488  
 489          if (!$this->running_javascript()) {
 490              throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
 491          }
 492  
 493          $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
 494          if (!$node->isVisible()) {
 495              throw new ExpectationException(
 496                  '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is not visible',
 497                  $this->getSession()
 498              );
 499          }
 500      }
 501  
 502      /**
 503       * Checks, that the existing element is not visible inside the existing container. Only available in tests using Javascript.
 504       *
 505       * As a "not" method, it's performance could not be good, but in this
 506       * case the performance is good because the element must exist,
 507       * otherwise there would be a ElementNotFoundException, also here we are
 508       * not spinning until the element is visible.
 509       *
 510       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should not be visible$/
 511       * @throws ElementNotFoundException
 512       * @throws ExpectationException
 513       * @param string $element Element we look for
 514       * @param string $selectortype The type of what we look for
 515       * @param string $nodeelement Element we look in
 516       * @param string $nodeselectortype The type of selector where we look in
 517       */
 518      public function in_the_should_not_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
 519  
 520          try {
 521              $this->in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype);
 522          } catch (ExpectationException $e) {
 523              // All as expected.
 524              return;
 525          }
 526          throw new ExpectationException(
 527              '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is visible',
 528              $this->getSession()
 529          );
 530      }
 531  
 532      /**
 533       * Checks, that page contains specified text. It also checks if the text is visible when running Javascript tests.
 534       *
 535       * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
 536       * @throws ExpectationException
 537       * @param string $text
 538       */
 539      public function assert_page_contains_text($text) {
 540  
 541          // Looking for all the matching nodes without any other descendant matching the
 542          // same xpath (we are using contains(., ....).
 543          $xpathliteral = behat_context_helper::escape($text);
 544          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
 545              "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
 546  
 547          try {
 548              $nodes = $this->find_all('xpath', $xpath);
 549          } catch (ElementNotFoundException $e) {
 550              throw new ExpectationException('"' . $text . '" text was not found in the page', $this->getSession());
 551          }
 552  
 553          // If we are not running javascript we have enough with the
 554          // element existing as we can't check if it is visible.
 555          if (!$this->running_javascript()) {
 556              return;
 557          }
 558  
 559          // We spin as we don't have enough checking that the element is there, we
 560          // should also ensure that the element is visible. Using microsleep as this
 561          // is a repeated step and global performance is important.
 562          $this->spin(
 563              function($context, $args) {
 564  
 565                  foreach ($args['nodes'] as $node) {
 566                      if ($node->isVisible()) {
 567                          return true;
 568                      }
 569                  }
 570  
 571                  // If non of the nodes is visible we loop again.
 572                  throw new ExpectationException('"' . $args['text'] . '" text was found but was not visible', $context->getSession());
 573              },
 574              array('nodes' => $nodes, 'text' => $text),
 575              false,
 576              false,
 577              true
 578          );
 579  
 580      }
 581  
 582      /**
 583       * Checks, that page doesn't contain specified text. When running Javascript tests it also considers that texts may be hidden.
 584       *
 585       * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
 586       * @throws ExpectationException
 587       * @param string $text
 588       */
 589      public function assert_page_not_contains_text($text) {
 590  
 591          // Looking for all the matching nodes without any other descendant matching the
 592          // same xpath (we are using contains(., ....).
 593          $xpathliteral = behat_context_helper::escape($text);
 594          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
 595              "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
 596  
 597          // We should wait a while to ensure that the page is not still loading elements.
 598          // Waiting less than self::TIMEOUT as we already waited for the DOM to be ready and
 599          // all JS to be executed.
 600          try {
 601              $nodes = $this->find_all('xpath', $xpath, false, false, self::REDUCED_TIMEOUT);
 602          } catch (ElementNotFoundException $e) {
 603              // All ok.
 604              return;
 605          }
 606  
 607          // If we are not running javascript we have enough with the
 608          // element existing as we can't check if it is hidden.
 609          if (!$this->running_javascript()) {
 610              throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
 611          }
 612  
 613          // If the element is there we should be sure that it is not visible.
 614          $this->spin(
 615              function($context, $args) {
 616  
 617                  foreach ($args['nodes'] as $node) {
 618                      if ($node->isVisible()) {
 619                          throw new ExpectationException('"' . $args['text'] . '" text was found in the page', $context->getSession());
 620                      }
 621                  }
 622  
 623                  // If non of the found nodes is visible we consider that the text is not visible.
 624                  return true;
 625              },
 626              array('nodes' => $nodes, 'text' => $text),
 627              self::REDUCED_TIMEOUT,
 628              false,
 629              true
 630          );
 631  
 632      }
 633  
 634      /**
 635       * Checks, that the specified element contains the specified text. When running Javascript tests it also considers that texts may be hidden.
 636       *
 637       * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
 638       * @throws ElementNotFoundException
 639       * @throws ExpectationException
 640       * @param string $text
 641       * @param string $element Element we look in.
 642       * @param string $selectortype The type of element where we are looking in.
 643       */
 644      public function assert_element_contains_text($text, $element, $selectortype) {
 645  
 646          // Getting the container where the text should be found.
 647          $container = $this->get_selected_node($selectortype, $element);
 648  
 649          // Looking for all the matching nodes without any other descendant matching the
 650          // same xpath (we are using contains(., ....).
 651          $xpathliteral = behat_context_helper::escape($text);
 652          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
 653              "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
 654  
 655          // Wait until it finds the text inside the container, otherwise custom exception.
 656          try {
 657              $nodes = $this->find_all('xpath', $xpath, false, $container);
 658          } catch (ElementNotFoundException $e) {
 659              throw new ExpectationException('"' . $text . '" text was not found in the "' . $element . '" element', $this->getSession());
 660          }
 661  
 662          // If we are not running javascript we have enough with the
 663          // element existing as we can't check if it is visible.
 664          if (!$this->running_javascript()) {
 665              return;
 666          }
 667  
 668          // We also check the element visibility when running JS tests. Using microsleep as this
 669          // is a repeated step and global performance is important.
 670          $this->spin(
 671              function($context, $args) {
 672  
 673                  foreach ($args['nodes'] as $node) {
 674                      if ($node->isVisible()) {
 675                          return true;
 676                      }
 677                  }
 678  
 679                  throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element but was not visible', $context->getSession());
 680              },
 681              array('nodes' => $nodes, 'text' => $text, 'element' => $element),
 682              false,
 683              false,
 684              true
 685          );
 686      }
 687  
 688      /**
 689       * Checks, that the specified element does not contain the specified text. When running Javascript tests it also considers that texts may be hidden.
 690       *
 691       * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
 692       * @throws ElementNotFoundException
 693       * @throws ExpectationException
 694       * @param string $text
 695       * @param string $element Element we look in.
 696       * @param string $selectortype The type of element where we are looking in.
 697       */
 698      public function assert_element_not_contains_text($text, $element, $selectortype) {
 699  
 700          // Getting the container where the text should be found.
 701          $container = $this->get_selected_node($selectortype, $element);
 702  
 703          // Looking for all the matching nodes without any other descendant matching the
 704          // same xpath (we are using contains(., ....).
 705          $xpathliteral = behat_context_helper::escape($text);
 706          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
 707              "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
 708  
 709          // We should wait a while to ensure that the page is not still loading elements.
 710          // Giving preference to the reliability of the results rather than to the performance.
 711          try {
 712              $nodes = $this->find_all('xpath', $xpath, false, $container, self::REDUCED_TIMEOUT);
 713          } catch (ElementNotFoundException $e) {
 714              // All ok.
 715              return;
 716          }
 717  
 718          // If we are not running javascript we have enough with the
 719          // element not being found as we can't check if it is visible.
 720          if (!$this->running_javascript()) {
 721              throw new ExpectationException('"' . $text . '" text was found in the "' . $element . '" element', $this->getSession());
 722          }
 723  
 724          // We need to ensure all the found nodes are hidden.
 725          $this->spin(
 726              function($context, $args) {
 727  
 728                  foreach ($args['nodes'] as $node) {
 729                      if ($node->isVisible()) {
 730                          throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element', $context->getSession());
 731                      }
 732                  }
 733  
 734                  // If all the found nodes are hidden we are happy.
 735                  return true;
 736              },
 737              array('nodes' => $nodes, 'text' => $text, 'element' => $element),
 738              self::REDUCED_TIMEOUT,
 739              false,
 740              true
 741          );
 742      }
 743  
 744      /**
 745       * Checks, that the first specified element appears before the second one.
 746       *
 747       * @Given /^"(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear before "(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
 748       * @throws ExpectationException
 749       * @param string $preelement The locator of the preceding element
 750       * @param string $preselectortype The locator of the preceding element
 751       * @param string $postelement The locator of the latest element
 752       * @param string $postselectortype The selector type of the latest element
 753       */
 754      public function should_appear_before($preelement, $preselectortype, $postelement, $postselectortype) {
 755  
 756          // We allow postselectortype as a non-text based selector.
 757          list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
 758          list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
 759  
 760          $prexpath = $this->find($preselector, $prelocator)->getXpath();
 761          $postxpath = $this->find($postselector, $postlocator)->getXpath();
 762  
 763          // Using following xpath axe to find it.
 764          $msg = '"'.$preelement.'" "'.$preselectortype.'" does not appear before "'.$postelement.'" "'.$postselectortype.'"';
 765          $xpath = $prexpath.'/following::*[contains(., '.$postxpath.')]';
 766          if (!$this->getSession()->getDriver()->find($xpath)) {
 767              throw new ExpectationException($msg, $this->getSession());
 768          }
 769      }
 770  
 771      /**
 772       * Checks, that the first specified element appears after the second one.
 773       *
 774       * @Given /^"(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear after "(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
 775       * @throws ExpectationException
 776       * @param string $postelement The locator of the latest element
 777       * @param string $postselectortype The selector type of the latest element
 778       * @param string $preelement The locator of the preceding element
 779       * @param string $preselectortype The locator of the preceding element
 780       */
 781      public function should_appear_after($postelement, $postselectortype, $preelement, $preselectortype) {
 782  
 783          // We allow postselectortype as a non-text based selector.
 784          list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
 785          list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
 786  
 787          $postxpath = $this->find($postselector, $postlocator)->getXpath();
 788          $prexpath = $this->find($preselector, $prelocator)->getXpath();
 789  
 790          // Using preceding xpath axe to find it.
 791          $msg = '"'.$postelement.'" "'.$postselectortype.'" does not appear after "'.$preelement.'" "'.$preselectortype.'"';
 792          $xpath = $postxpath.'/preceding::*[contains(., '.$prexpath.')]';
 793          if (!$this->getSession()->getDriver()->find($xpath)) {
 794              throw new ExpectationException($msg, $this->getSession());
 795          }
 796      }
 797  
 798      /**
 799       * Checks, that element of specified type is disabled.
 800       *
 801       * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be disabled$/
 802       * @throws ExpectationException Thrown by behat_base::find
 803       * @param string $element Element we look in
 804       * @param string $selectortype The type of element where we are looking in.
 805       */
 806      public function the_element_should_be_disabled($element, $selectortype) {
 807  
 808          // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
 809          $node = $this->get_selected_node($selectortype, $element);
 810  
 811          if (!$node->hasAttribute('disabled')) {
 812              throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
 813          }
 814      }
 815  
 816      /**
 817       * Checks, that element of specified type is enabled.
 818       *
 819       * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be enabled$/
 820       * @throws ExpectationException Thrown by behat_base::find
 821       * @param string $element Element we look on
 822       * @param string $selectortype The type of where we look
 823       */
 824      public function the_element_should_be_enabled($element, $selectortype) {
 825  
 826          // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
 827          $node = $this->get_selected_node($selectortype, $element);
 828  
 829          if ($node->hasAttribute('disabled')) {
 830              throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
 831          }
 832      }
 833  
 834      /**
 835       * Checks the provided element and selector type are readonly on the current page.
 836       *
 837       * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be readonly$/
 838       * @throws ExpectationException Thrown by behat_base::find
 839       * @param string $element Element we look in
 840       * @param string $selectortype The type of element where we are looking in.
 841       */
 842      public function the_element_should_be_readonly($element, $selectortype) {
 843          // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
 844          $node = $this->get_selected_node($selectortype, $element);
 845  
 846          if (!$node->hasAttribute('readonly')) {
 847              throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
 848          }
 849      }
 850  
 851      /**
 852       * Checks the provided element and selector type are not readonly on the current page.
 853       *
 854       * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not be readonly$/
 855       * @throws ExpectationException Thrown by behat_base::find
 856       * @param string $element Element we look in
 857       * @param string $selectortype The type of element where we are looking in.
 858       */
 859      public function the_element_should_not_be_readonly($element, $selectortype) {
 860          // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
 861          $node = $this->get_selected_node($selectortype, $element);
 862  
 863          if ($node->hasAttribute('readonly')) {
 864              throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
 865          }
 866      }
 867  
 868      /**
 869       * Checks the provided element and selector type exists in the current page.
 870       *
 871       * This step is for advanced users, use it if you don't find anything else suitable for what you need.
 872       *
 873       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist$/
 874       * @throws ElementNotFoundException Thrown by behat_base::find
 875       * @param string $element The locator of the specified selector
 876       * @param string $selectortype The selector type
 877       */
 878      public function should_exist($element, $selectortype) {
 879  
 880          // Getting Mink selector and locator.
 881          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 882  
 883          // Will throw an ElementNotFoundException if it does not exist.
 884          $this->find($selector, $locator);
 885      }
 886  
 887      /**
 888       * Checks that the provided element and selector type not exists in the current page.
 889       *
 890       * This step is for advanced users, use it if you don't find anything else suitable for what you need.
 891       *
 892       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist$/
 893       * @throws ExpectationException
 894       * @param string $element The locator of the specified selector
 895       * @param string $selectortype The selector type
 896       */
 897      public function should_not_exist($element, $selectortype) {
 898  
 899          // Getting Mink selector and locator.
 900          list($selector, $locator) = $this->transform_selector($selectortype, $element);
 901  
 902          try {
 903  
 904              // Using directly the spin method as we want a reduced timeout but there is no
 905              // need for a 0.1 seconds interval because in the optimistic case we will timeout.
 906              $params = array('selector' => $selector, 'locator' => $locator);
 907              // The exception does not really matter as we will catch it and will never "explode".
 908              $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $element);
 909  
 910              // If all goes good it will throw an ElementNotFoundExceptionn that we will catch.
 911              $this->spin(
 912                  function($context, $args) {
 913                      return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
 914                  },
 915                  $params,
 916                  self::REDUCED_TIMEOUT,
 917                  $exception,
 918                  false
 919              );
 920          } catch (ElementNotFoundException $e) {
 921              // It passes.
 922              return;
 923          }
 924  
 925          throw new ExpectationException('The "' . $element . '" "' . $selectortype .
 926                  '" exists in the current page', $this->getSession());
 927      }
 928  
 929      /**
 930       * This step triggers cron like a user would do going to admin/cron.php.
 931       *
 932       * @Given /^I trigger cron$/
 933       */
 934      public function i_trigger_cron() {
 935          $this->getSession()->visit($this->locate_path('/admin/cron.php'));
 936      }
 937  
 938      /**
 939       * Runs a scheduled task immediately, given full class name.
 940       *
 941       * This is faster and more reliable than running cron (running cron won't
 942       * work more than once in the same test, for instance). However it is
 943       * a little less 'realistic'.
 944       *
 945       * While the task is running, we suppress mtrace output because it makes
 946       * the Behat result look ugly.
 947       *
 948       * Note: Most of the code relating to running a task is based on
 949       * admin/tool/task/cli/schedule_task.php.
 950       *
 951       * @Given /^I run the scheduled task "(?P<task_name>[^"]+)"$/
 952       * @param string $taskname Name of task e.g. 'mod_whatever\task\do_something'
 953       */
 954      public function i_run_the_scheduled_task($taskname) {
 955          $task = \core\task\manager::get_scheduled_task($taskname);
 956          if (!$task) {
 957              throw new DriverException('The "' . $taskname . '" scheduled task does not exist');
 958          }
 959  
 960          // Do setup for cron task.
 961          raise_memory_limit(MEMORY_EXTRA);
 962          cron_setup_user();
 963  
 964          // Get lock.
 965          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 966          if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 967              throw new DriverException('Unable to obtain core_cron lock for scheduled task');
 968          }
 969          if (!$lock = $cronlockfactory->get_lock('\\' . get_class($task), 10)) {
 970              $cronlock->release();
 971              throw new DriverException('Unable to obtain task lock for scheduled task');
 972          }
 973          $task->set_lock($lock);
 974          if (!$task->is_blocking()) {
 975              $cronlock->release();
 976          } else {
 977              $task->set_cron_lock($cronlock);
 978          }
 979  
 980          try {
 981              // Discard task output as not appropriate for Behat output!
 982              ob_start();
 983              $task->execute();
 984              ob_end_clean();
 985  
 986              // Mark task complete.
 987              \core\task\manager::scheduled_task_complete($task);
 988          } catch (Exception $e) {
 989              // Mark task failed and throw exception.
 990              \core\task\manager::scheduled_task_failed($task);
 991              throw new DriverException('The "' . $taskname . '" scheduled task failed', 0, $e);
 992          }
 993      }
 994  
 995      /**
 996       * Checks that an element and selector type exists in another element and selector type on the current page.
 997       *
 998       * This step is for advanced users, use it if you don't find anything else suitable for what you need.
 999       *
1000       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
1001       * @throws ElementNotFoundException Thrown by behat_base::find
1002       * @param string $element The locator of the specified selector
1003       * @param string $selectortype The selector type
1004       * @param string $containerelement The container selector type
1005       * @param string $containerselectortype The container locator
1006       */
1007      public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
1008          // Get the container node.
1009          $containernode = $this->get_selected_node($containerselectortype, $containerelement);
1010  
1011          list($selector, $locator) = $this->transform_selector($selectortype, $element);
1012  
1013          // Specific exception giving info about where can't we find the element.
1014          $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
1015          $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
1016  
1017          // Looks for the requested node inside the container node.
1018          $this->find($selector, $locator, $exception, $containernode);
1019      }
1020  
1021      /**
1022       * Checks that an element and selector type does not exist in another element and selector type on the current page.
1023       *
1024       * This step is for advanced users, use it if you don't find anything else suitable for what you need.
1025       *
1026       * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
1027       * @throws ExpectationException
1028       * @param string $element The locator of the specified selector
1029       * @param string $selectortype The selector type
1030       * @param string $containerelement The container selector type
1031       * @param string $containerselectortype The container locator
1032       */
1033      public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
1034  
1035          // Get the container node; here we throw an exception
1036          // if the container node does not exist.
1037          $containernode = $this->get_selected_node($containerselectortype, $containerelement);
1038  
1039          list($selector, $locator) = $this->transform_selector($selectortype, $element);
1040  
1041          // Will throw an ElementNotFoundException if it does not exist, but, actually
1042          // it should not exist, so we try & catch it.
1043          try {
1044              // Would be better to use a 1 second sleep because the element should not be there,
1045              // but we would need to duplicate the whole find_all() logic to do it, the benefit of
1046              // changing to 1 second sleep is not significant.
1047              $this->find($selector, $locator, false, $containernode, self::REDUCED_TIMEOUT);
1048          } catch (ElementNotFoundException $e) {
1049              // It passes.
1050              return;
1051          }
1052          throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the "' .
1053                  $containerelement . '" "' . $containerselectortype . '"', $this->getSession());
1054      }
1055  
1056      /**
1057       * Change browser window size small: 640x480, medium: 1024x768, large: 2560x1600, custom: widthxheight
1058       *
1059       * Example: I change window size to "small" or I change window size to "1024x768"
1060       * or I change viewport size to "800x600". The viewport option is useful to guarantee that the
1061       * browser window has same viewport size even when you run Behat on multiple operating systems.
1062       *
1063       * @throws ExpectationException
1064       * @Then /^I change (window|viewport) size to "(small|medium|large|\d+x\d+)"$/
1065       * @param string $windowsize size of the window (small|medium|large|wxh).
1066       */
1067      public function i_change_window_size_to($windowviewport, $windowsize) {
1068          $this->resize_window($windowsize, $windowviewport === 'viewport');
1069      }
1070  
1071      /**
1072       * Checks whether there is an attribute on the given element that contains the specified text.
1073       *
1074       * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should contain "(?P<text_string>(?:[^"]|\\")*)"$/
1075       * @throws ExpectationException
1076       * @param string $attribute Name of attribute
1077       * @param string $element The locator of the specified selector
1078       * @param string $selectortype The selector type
1079       * @param string $text Expected substring
1080       */
1081      public function the_attribute_of_should_contain($attribute, $element, $selectortype, $text) {
1082          // Get the container node (exception if it doesn't exist).
1083          $containernode = $this->get_selected_node($selectortype, $element);
1084          $value = $containernode->getAttribute($attribute);
1085          if ($value == null) {
1086              throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1087                      $this->getSession());
1088          } else if (strpos($value, $text) === false) {
1089              throw new ExpectationException('The attribute "' . $attribute .
1090                      '" does not contain "' . $text . '" (actual value: "' . $value . '")',
1091                      $this->getSession());
1092          }
1093      }
1094  
1095      /**
1096       * Checks that the attribute on the given element does not contain the specified text.
1097       *
1098       * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not contain "(?P<text_string>(?:[^"]|\\")*)"$/
1099       * @throws ExpectationException
1100       * @param string $attribute Name of attribute
1101       * @param string $element The locator of the specified selector
1102       * @param string $selectortype The selector type
1103       * @param string $text Expected substring
1104       */
1105      public function the_attribute_of_should_not_contain($attribute, $element, $selectortype, $text) {
1106          // Get the container node (exception if it doesn't exist).
1107          $containernode = $this->get_selected_node($selectortype, $element);
1108          $value = $containernode->getAttribute($attribute);
1109          if ($value == null) {
1110              throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1111                      $this->getSession());
1112          } else if (strpos($value, $text) !== false) {
1113              throw new ExpectationException('The attribute "' . $attribute .
1114                      '" contains "' . $text . '" (value: "' . $value . '")',
1115                      $this->getSession());
1116          }
1117      }
1118  
1119      /**
1120       * Checks the provided value exists in specific row/column of table.
1121       *
1122       * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should contain "(?P<value_string>[^"]*)"$/
1123       * @throws ElementNotFoundException
1124       * @param string $row row text which will be looked in.
1125       * @param string $column column text to search (or numeric value for the column position)
1126       * @param string $table table id/class/caption
1127       * @param string $value text to check.
1128       */
1129      public function row_column_of_table_should_contain($row, $column, $table, $value) {
1130          $tablenode = $this->get_selected_node('table', $table);
1131          $tablexpath = $tablenode->getXpath();
1132  
1133          $rowliteral = behat_context_helper::escape($row);
1134          $valueliteral = behat_context_helper::escape($value);
1135          $columnliteral = behat_context_helper::escape($column);
1136  
1137          if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
1138              // Column indicated as a number, just use it as position of the column.
1139              $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
1140          } else {
1141              // Header can be in thead or tbody (first row), following xpath should work.
1142              $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1143                      $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
1144              $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1145                      $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
1146  
1147              // Check if column exists.
1148              $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
1149              $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
1150              if (empty($columnheader)) {
1151                  $columnexceptionmsg = $column . '" in table "' . $table . '"';
1152                  throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg);
1153              }
1154              // Following conditions were considered before finding column count.
1155              // 1. Table header can be in thead/tr/th or tbody/tr/td[1].
1156              // 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
1157              $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
1158                  "/preceding-sibling::*) + 1]";
1159          }
1160  
1161          // Check if value exists in specific row/column.
1162          // Get row xpath.
1163          // GoutteDriver uses DomCrawler\Crawler and it is making XPath relative to the current context, so use descendant.
1164          $rowxpath = $tablexpath."/tbody/tr[descendant::th[normalize-space(.)=" . $rowliteral .
1165                      "] | descendant::td[normalize-space(.)=" . $rowliteral . "]]";
1166  
1167          $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
1168  
1169          // Looks for the requested node inside the container node.
1170          $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
1171          if (empty($coumnnode)) {
1172              $locatorexceptionmsg = $value . '" in "' . $row . '" row with column "' . $column;
1173              throw new ElementNotFoundException($this->getSession(), "\n$columnvaluexpath\n\n".'Column value', null, $locatorexceptionmsg);
1174          }
1175      }
1176  
1177      /**
1178       * Checks the provided value should not exist in specific row/column of table.
1179       *
1180       * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should not contain "(?P<value_string>[^"]*)"$/
1181       * @throws ElementNotFoundException
1182       * @param string $row row text which will be looked in.
1183       * @param string $column column text to search
1184       * @param string $table table id/class/caption
1185       * @param string $value text to check.
1186       */
1187      public function row_column_of_table_should_not_contain($row, $column, $table, $value) {
1188          try {
1189              $this->row_column_of_table_should_contain($row, $column, $table, $value);
1190          } catch (ElementNotFoundException $e) {
1191              // Table row/column doesn't contain this value. Nothing to do.
1192              return;
1193          }
1194          // Throw exception if found.
1195          throw new ExpectationException(
1196              '"' . $column . '" with value "' . $value . '" is present in "' . $row . '"  row for table "' . $table . '"',
1197              $this->getSession()
1198          );
1199      }
1200  
1201      /**
1202       * Checks that the provided value exist in table.
1203       * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1204       *
1205       * First row may contain column headers or numeric indexes of the columns
1206       * (syntax -1- is also considered to be column index). Column indexes are
1207       * useful in case of multirow headers and/or presence of cells with colspan.
1208       *
1209       * @Then /^the following should exist in the "(?P<table_string>[^"]*)" table:$/
1210       * @throws ExpectationException
1211       * @param string $table name of table
1212       * @param TableNode $data table with first row as header and following values
1213       *        | Header 1 | Header 2 | Header 3 |
1214       *        | Value 1 | Value 2 | Value 3|
1215       */
1216      public function following_should_exist_in_the_table($table, TableNode $data) {
1217          $datahash = $data->getHash();
1218  
1219          foreach ($datahash as $row) {
1220              $firstcell = null;
1221              foreach ($row as $column => $value) {
1222                  if ($firstcell === null) {
1223                      $firstcell = $value;
1224                  } else {
1225                      $this->row_column_of_table_should_contain($firstcell, $column, $table, $value);
1226                  }
1227              }
1228          }
1229      }
1230  
1231      /**
1232       * Checks that the provided value exist in table.
1233       * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1234       *
1235       * @Then /^the following should not exist in the "(?P<table_string>[^"]*)" table:$/
1236       * @throws ExpectationException
1237       * @param string $table name of table
1238       * @param TableNode $data table with first row as header and following values
1239       *        | Header 1 | Header 2 | Header 3 |
1240       *        | Value 1 | Value 2 | Value 3|
1241       */
1242      public function following_should_not_exist_in_the_table($table, TableNode $data) {
1243          $datahash = $data->getHash();
1244  
1245          foreach ($datahash as $value) {
1246              $row = array_shift($value);
1247              foreach ($value as $column => $value) {
1248                  try {
1249                      $this->row_column_of_table_should_contain($row, $column, $table, $value);
1250                      // Throw exception if found.
1251                  } catch (ElementNotFoundException $e) {
1252                      // Table row/column doesn't contain this value. Nothing to do.
1253                      continue;
1254                  }
1255                  throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' .
1256                      $row . '"  row for table "' . $table . '"', $this->getSession()
1257                  );
1258              }
1259          }
1260      }
1261  
1262      /**
1263       * Given the text of a link, download the linked file and return the contents.
1264       *
1265       * This is a helper method used by {@link following_should_download_bytes()}
1266       * and {@link following_should_download_between_and_bytes()}
1267       *
1268       * @param string $link the text of the link.
1269       * @return string the content of the downloaded file.
1270       */
1271      public function download_file_from_link($link) {
1272          // Find the link.
1273          $linknode = $this->find_link($link);
1274          $this->ensure_node_is_visible($linknode);
1275  
1276          // Get the href and check it.
1277          $url = $linknode->getAttribute('href');
1278          if (!$url) {
1279              throw new ExpectationException('Download link does not have href attribute',
1280                      $this->getSession());
1281          }
1282          if (!preg_match('~^https?://~', $url)) {
1283              throw new ExpectationException('Download link not an absolute URL: ' . $url,
1284                      $this->getSession());
1285          }
1286  
1287          // Download the URL and check the size.
1288          $session = $this->getSession()->getCookie('MoodleSession');
1289          return download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
1290      }
1291  
1292      /**
1293       * Downloads the file from a link on the page and checks the size.
1294       *
1295       * Only works if the link has an href attribute. Javascript downloads are
1296       * not supported. Currently, the href must be an absolute URL.
1297       *
1298       * @Then /^following "(?P<link_string>[^"]*)" should download "(?P<expected_bytes>\d+)" bytes$/
1299       * @throws ExpectationException
1300       * @param string $link the text of the link.
1301       * @param number $expectedsize the expected file size in bytes.
1302       */
1303      public function following_should_download_bytes($link, $expectedsize) {
1304          $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1305  
1306          // It will stop spinning once file is downloaded or time out.
1307          $result = $this->spin(
1308              function($context, $args) {
1309                  $link = $args['link'];
1310                  return $this->download_file_from_link($link);
1311              },
1312              array('link' => $link),
1313              self::EXTENDED_TIMEOUT,
1314              $exception
1315          );
1316  
1317          // Check download size.
1318          $actualsize = (int)strlen($result);
1319          if ($actualsize !== (int)$expectedsize) {
1320              throw new ExpectationException('Downloaded data was ' . $actualsize .
1321                      ' bytes, expecting ' . $expectedsize, $this->getSession());
1322          }
1323      }
1324  
1325      /**
1326       * Downloads the file from a link on the page and checks the size is in a given range.
1327       *
1328       * Only works if the link has an href attribute. Javascript downloads are
1329       * not supported. Currently, the href must be an absolute URL.
1330       *
1331       * The range includes the endpoints. That is, a 10 byte file in considered to
1332       * be between "5" and "10" bytes, and between "10" and "20" bytes.
1333       *
1334       * @Then /^following "(?P<link_string>[^"]*)" should download between "(?P<min_bytes>\d+)" and "(?P<max_bytes>\d+)" bytes$/
1335       * @throws ExpectationException
1336       * @param string $link the text of the link.
1337       * @param number $minexpectedsize the minimum expected file size in bytes.
1338       * @param number $maxexpectedsize the maximum expected file size in bytes.
1339       */
1340      public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
1341          // If the minimum is greater than the maximum then swap the values.
1342          if ((int)$minexpectedsize > (int)$maxexpectedsize) {
1343              list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
1344          }
1345  
1346          $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1347  
1348          // It will stop spinning once file is downloaded or time out.
1349          $result = $this->spin(
1350              function($context, $args) {
1351                  $link = $args['link'];
1352  
1353                  return $this->download_file_from_link($link);
1354              },
1355              array('link' => $link),
1356              self::EXTENDED_TIMEOUT,
1357              $exception
1358          );
1359  
1360          // Check download size.
1361          $actualsize = (int)strlen($result);
1362          if ($actualsize < $minexpectedsize || $actualsize > $maxexpectedsize) {
1363              throw new ExpectationException('Downloaded data was ' . $actualsize .
1364                      ' bytes, expecting between ' . $minexpectedsize . ' and ' .
1365                      $maxexpectedsize, $this->getSession());
1366          }
1367      }
1368  
1369      /**
1370       * Prepare to detect whether or not a new page has loaded (or the same page reloaded) some time in the future.
1371       *
1372       * @Given /^I start watching to see if a new page loads$/
1373       */
1374      public function i_start_watching_to_see_if_a_new_page_loads() {
1375          if (!$this->running_javascript()) {
1376              throw new DriverException('Page load detection requires JavaScript.');
1377          }
1378  
1379          $session = $this->getSession();
1380  
1381          if ($this->pageloaddetectionrunning || $session->getPage()->find('xpath', $this->get_page_load_xpath())) {
1382              // If we find this node at this point we are already watching for a reload and the behat steps
1383              // are out of order. We will treat this as an error - really it needs to be fixed as it indicates a problem.
1384              throw new ExpectationException(
1385                  'Page load expectation error: page reloads are already been watched for.', $session);
1386          }
1387  
1388          $this->pageloaddetectionrunning = true;
1389  
1390          $session->executeScript(
1391                  'var span = document.createElement("span");
1392                  span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
1393                  span.setAttribute("style", "display: none;");
1394                  document.body.appendChild(span);');
1395      }
1396  
1397      /**
1398       * Verify that a new page has loaded (or the same page has reloaded) since the
1399       * last "I start watching to see if a new page loads" step.
1400       *
1401       * @Given /^a new page should have loaded since I started watching$/
1402       */
1403      public function a_new_page_should_have_loaded_since_i_started_watching() {
1404          $session = $this->getSession();
1405  
1406          // Make sure page load tracking was started.
1407          if (!$this->pageloaddetectionrunning) {
1408              throw new ExpectationException(
1409                  'Page load expectation error: page load tracking was not started.', $session);
1410          }
1411  
1412          // As the node is inserted by code above it is either there or not, and we do not need spin and it is safe
1413          // to use the native API here which is great as exception handling (the alternative is slow).
1414          if ($session->getPage()->find('xpath', $this->get_page_load_xpath())) {
1415              // We don't want to find this node, if we do we have an error.
1416              throw new ExpectationException(
1417                  'Page load expectation error: a new page has not been loaded when it should have been.', $session);
1418          }
1419  
1420          // Cancel the tracking of pageloaddetectionrunning.
1421          $this->pageloaddetectionrunning = false;
1422      }
1423  
1424      /**
1425       * Verify that a new page has not loaded (or the same page has reloaded) since the
1426       * last "I start watching to see if a new page loads" step.
1427       *
1428       * @Given /^a new page should not have loaded since I started watching$/
1429       */
1430      public function a_new_page_should_not_have_loaded_since_i_started_watching() {
1431          $session = $this->getSession();
1432  
1433          // Make sure page load tracking was started.
1434          if (!$this->pageloaddetectionrunning) {
1435              throw new ExpectationException(
1436                  'Page load expectation error: page load tracking was not started.', $session);
1437          }
1438  
1439          // We use our API here as we can use the exception handling provided by it.
1440          $this->find(
1441              'xpath',
1442              $this->get_page_load_xpath(),
1443              new ExpectationException(
1444                  'Page load expectation error: A new page has been loaded when it should not have been.',
1445                  $this->getSession()
1446              )
1447          );
1448      }
1449  
1450      /**
1451       * Helper used by {@link a_new_page_should_have_loaded_since_i_started_watching}
1452       * and {@link a_new_page_should_not_have_loaded_since_i_started_watching}
1453       * @return string xpath expression.
1454       */
1455      protected function get_page_load_xpath() {
1456          return "//span[@data-rel = '" . self::PAGE_LOAD_DETECTION_STRING . "']";
1457      }
1458  
1459      /**
1460       * Wait unit user press Enter/Return key. Useful when debugging a scenario.
1461       *
1462       * @Then /^(?:|I )pause(?:| scenario execution)$/
1463       */
1464      public function i_pause_scenario_executon() {
1465          global $CFG;
1466  
1467          $posixexists = function_exists('posix_isatty');
1468  
1469          // Make sure this step is only used with interactive terminal (if detected).
1470          if ($posixexists && !@posix_isatty(STDOUT)) {
1471              $session = $this->getSession();
1472              throw new ExpectationException('Break point should only be used with interative terminal.', $session);
1473          }
1474  
1475          // Windows don't support ANSI code by default, but with ANSICON.
1476          $isansicon = getenv('ANSICON');
1477          if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) {
1478              fwrite(STDOUT, "Paused. Press Enter/Return to continue.");
1479              fread(STDIN, 1024);
1480          } else {
1481              fwrite(STDOUT, "\033[s\n\033[0;93mPaused. Press \033[1;31mEnter/Return\033[0;93m to continue.\033[0m");
1482              fread(STDIN, 1024);
1483              fwrite(STDOUT, "\033[2A\033[u\033[2B");
1484          }
1485      }
1486  
1487      /**
1488       * Presses a given button in the browser.
1489       * NOTE: Phantomjs and goutte driver reloads page while navigating back and forward.
1490       *
1491       * @Then /^I press the "(back|forward|reload)" button in the browser$/
1492       * @param string $button the button to press.
1493       * @throws ExpectationException
1494       */
1495      public function i_press_in_the_browser($button) {
1496          $session = $this->getSession();
1497  
1498          if ($button == 'back') {
1499              $session->back();
1500          } else if ($button == 'forward') {
1501              $session->forward();
1502          } else if ($button == 'reload') {
1503              $session->reload();
1504          } else {
1505              throw new ExpectationException('Unknown browser button.', $session);
1506          }
1507      }
1508  
1509      /**
1510       * Trigger a keydown event for a key on a specific element.
1511       *
1512       * @When /^I press key "(?P<key_string>(?:[^"]|\\")*)" in "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
1513       * @param string $key either char-code or character itself,
1514       *               may optionally be prefixed with ctrl-, alt-, shift- or meta-
1515       * @param string $element Element we look for
1516       * @param string $selectortype The type of what we look for
1517       * @throws DriverException
1518       * @throws ExpectationException
1519       */
1520      public function i_press_key_in_element($key, $element, $selectortype) {
1521          if (!$this->running_javascript()) {
1522              throw new DriverException('Key down step is not available with Javascript disabled');
1523          }
1524          // Gets the node based on the requested selector type and locator.
1525          $node = $this->get_selected_node($selectortype, $element);
1526          $modifier = null;
1527          $validmodifiers = array('ctrl', 'alt', 'shift', 'meta');
1528          $char = $key;
1529          if (strpos($key, '-')) {
1530              list($modifier, $char) = preg_split('/-/', $key, 2);
1531              $modifier = strtolower($modifier);
1532              if (!in_array($modifier, $validmodifiers)) {
1533                  throw new ExpectationException(sprintf('Unknown key modifier: %s.', $modifier));
1534              }
1535          }
1536          if (is_numeric($char)) {
1537              $char = (int)$char;
1538          }
1539  
1540          $node->keyDown($char, $modifier);
1541          $node->keyPress($char, $modifier);
1542          $node->keyUp($char, $modifier);
1543      }
1544  
1545      /**
1546       * Press tab key on a specific element.
1547       *
1548       * @When /^I press tab key in "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
1549       * @param string $element Element we look for
1550       * @param string $selectortype The type of what we look for
1551       * @throws DriverException
1552       * @throws ExpectationException
1553       */
1554      public function i_post_tab_key_in_element($element, $selectortype) {
1555          if (!$this->running_javascript()) {
1556              throw new DriverException('Tab press step is not available with Javascript disabled');
1557          }
1558          // Gets the node based on the requested selector type and locator.
1559          $node = $this->get_selected_node($selectortype, $element);
1560          $this->getSession()->getDriver()->post_key("\xEE\x80\x84", $node->getXpath());
1561      }
1562  
1563      /**
1564       * Checks if database family used is using one of the specified, else skip. (mysql, postgres, mssql, oracle, etc.)
1565       *
1566       * @Given /^database family used is one of the following:$/
1567       * @param TableNode $databasefamilies list of database.
1568       * @return void.
1569       * @throws \Moodle\BehatExtension\Exception\SkippedException
1570       */
1571      public function database_family_used_is_one_of_the_following(TableNode $databasefamilies) {
1572          global $DB;
1573  
1574          $dbfamily = $DB->get_dbfamily();
1575  
1576          // Check if used db family is one of the specified ones. If yes then return.
1577          foreach ($databasefamilies->getRows() as $dbfamilytocheck) {
1578              if ($dbfamilytocheck[0] == $dbfamily) {
1579                  return;
1580              }
1581          }
1582  
1583          throw new \Moodle\BehatExtension\Exception\SkippedException();
1584      }
1585  }


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