"Fossies" - the Fresh Open Source Software Archive

Member "moodle/lib/tests/behat/behat_general.php" (6 Sep 2019, 79219 Bytes) of package /linux/www/moodle-3.6.6.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "behat_general.php": 3.6.5_vs_3.6.6.

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