MDL-47494 qtype: Fix the behat tests for OU dd question types
[moodle.git] / lib / tests / behat / behat_general.php
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/>.
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  */
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/../../behat/behat_base.php');
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     Behat\Behat\Context\Step\Given as Given;
38 /**
39  * Cross component steps definitions.
40  *
41  * Basic web application definitions from MinkExtension and
42  * BehatchExtension. Definitions modified according to our needs
43  * when necessary and including only the ones we need to avoid
44  * overlapping and confusion.
45  *
46  * @package   core
47  * @category  test
48  * @copyright 2012 David MonllaĆ³
49  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50  */
51 class behat_general extends behat_base {
53     /**
54      * @var string used by {@link switch_to_window()} and
55      * {@link switch_to_the_main_window()} to work-around a Chrome browser issue.
56      */
57     const MAIN_WINDOW_NAME = '__moodle_behat_main_window_name';
59     /**
60      * @var string when we want to check whether or not a new page has loaded,
61      * we first write this unique string into the page. Then later, by checking
62      * whether it is still there, we can tell if a new page has been loaded.
63      */
64     const PAGE_LOAD_DETECTION_STRING = 'new_page_not_loaded_since_behat_started_watching';
66     /**
67      * @var $pageloaddetectionrunning boolean Used to ensure that page load detection was started before a page reload
68      * was checked for.
69      */
70     private $pageloaddetectionrunning = false;
72     /**
73      * Opens Moodle homepage.
74      *
75      * @Given /^I am on homepage$/
76      */
77     public function i_am_on_homepage() {
78         $this->getSession()->visit($this->locate_path('/'));
79     }
81     /**
82      * Opens Moodle site homepage.
83      *
84      * @Given /^I am on site homepage$/
85      */
86     public function i_am_on_site_homepage() {
87         $this->getSession()->visit($this->locate_path('/?redirect=0'));
88     }
90     /**
91      * Reloads the current page.
92      *
93      * @Given /^I reload the page$/
94      */
95     public function reload() {
96         $this->getSession()->reload();
97     }
99     /**
100      * Follows the page redirection. Use this step after any action that shows a message and waits for a redirection
101      *
102      * @Given /^I wait to be redirected$/
103      */
104     public function i_wait_to_be_redirected() {
106         // Xpath and processes based on core_renderer::redirect_message(), core_renderer::$metarefreshtag and
107         // moodle_page::$periodicrefreshdelay possible values.
108         if (!$metarefresh = $this->getSession()->getPage()->find('xpath', "//head/descendant::meta[@http-equiv='refresh']")) {
109             // We don't fail the scenario if no redirection with message is found to avoid race condition false failures.
110             return true;
111         }
113         // Wrapped in try & catch in case the redirection has already been executed.
114         try {
115             $content = $metarefresh->getAttribute('content');
116         } catch (NoSuchElement $e) {
117             return true;
118         } catch (StaleElementReference $e) {
119             return true;
120         }
122         // Getting the refresh time and the url if present.
123         if (strstr($content, 'url') != false) {
125             list($waittime, $url) = explode(';', $content);
127             // Cleaning the URL value.
128             $url = trim(substr($url, strpos($url, 'http')));
130         } else {
131             // Just wait then.
132             $waittime = $content;
133         }
136         // Wait until the URL change is executed.
137         if ($this->running_javascript()) {
138             $this->getSession()->wait($waittime * 1000, false);
140         } else if (!empty($url)) {
141             // We redirect directly as we can not wait for an automatic redirection.
142             $this->getSession()->getDriver()->getClient()->request('get', $url);
144         } else {
145             // Reload the page if no URL was provided.
146             $this->getSession()->getDriver()->reload();
147         }
148     }
150     /**
151      * Switches to the specified iframe.
152      *
153      * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe$/
154      * @param string $iframename
155      */
156     public function switch_to_iframe($iframename) {
158         // We spin to give time to the iframe to be loaded.
159         // Using extended timeout as we don't know about which
160         // kind of iframe will be loaded.
161         $this->spin(
162             function($context, $iframename) {
163                 $context->getSession()->switchToIFrame($iframename);
165                 // If no exception we are done.
166                 return true;
167             },
168             $iframename,
169             self::EXTENDED_TIMEOUT
170         );
171     }
173     /**
174      * Switches to the main Moodle frame.
175      *
176      * @Given /^I switch to the main frame$/
177      */
178     public function switch_to_the_main_frame() {
179         $this->getSession()->switchToIFrame();
180     }
182     /**
183      * Switches to the specified window. Useful when interacting with popup windows.
184      *
185      * @Given /^I switch to "(?P<window_name_string>(?:[^"]|\\")*)" window$/
186      * @param string $windowname
187      */
188     public function switch_to_window($windowname) {
189         // In Behat, some browsers (e.g. Chrome) are unable to switch to a
190         // window without a name, and by default the main browser window does
191         // not have a name. To work-around this, when we switch away from an
192         // unnamed window (presumably the main window) to some other named
193         // window, then we first set the main window name to a conventional
194         // value that we can later use this name to switch back.
195         $this->getSession()->evaluateScript(
196                 'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
198         $this->getSession()->switchToWindow($windowname);
199     }
201     /**
202      * Switches to the main Moodle window. Useful when you finish interacting with popup windows.
203      *
204      * @Given /^I switch to the main window$/
205      */
206     public function switch_to_the_main_window() {
207         $this->getSession()->switchToWindow(self::MAIN_WINDOW_NAME);
208     }
210     /**
211      * Accepts the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
212      * @Given /^I accept the currently displayed dialog$/
213      */
214     public function accept_currently_displayed_alert_dialog() {
215         $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
216     }
218     /**
219      * Dismisses the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
220      * @Given /^I dismiss the currently displayed dialog$/
221      */
222     public function dismiss_currently_displayed_alert_dialog() {
223         $this->getSession()->getDriver()->getWebDriverSession()->dismiss_alert();
224     }
226     /**
227      * Clicks link with specified id|title|alt|text.
228      *
229      * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
230      * @throws ElementNotFoundException Thrown by behat_base::find
231      * @param string $link
232      */
233     public function click_link($link) {
235         $linknode = $this->find_link($link);
236         $this->ensure_node_is_visible($linknode);
237         $linknode->click();
238     }
240     /**
241      * Waits X seconds. Required after an action that requires data from an AJAX request.
242      *
243      * @Then /^I wait "(?P<seconds_number>\d+)" seconds$/
244      * @param int $seconds
245      */
246     public function i_wait_seconds($seconds) {
247         if ($this->running_javascript()) {
248             $this->getSession()->wait($seconds * 1000, false);
249         } else {
250             sleep($seconds);
251         }
252     }
254     /**
255      * Waits until the page is completely loaded. This step is auto-executed after every step.
256      *
257      * @Given /^I wait until the page is ready$/
258      */
259     public function wait_until_the_page_is_ready() {
261         if (!$this->running_javascript()) {
262             throw new DriverException('Waits are disabled in scenarios without Javascript support');
263         }
265         $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
266     }
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.
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     }
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.
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     }
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) {
311         // Gets the node based on the requested selector type and locator.
312         $node = $this->get_selected_node($selectortype, $element);
313         $node->mouseOver();
314     }
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) {
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     }
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);
346         // Ensure element is focused before taking it off.
347         $node->focus();
348         $node->blur();
349     }
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     }
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     }
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) {
388         $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
389         $this->ensure_node_is_visible($node);
390         $node->click();
391     }
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) {
408         list($sourceselector, $sourcelocator) = $this->transform_selector($selectortype, $element);
409         $sourcexpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($sourceselector, $sourcelocator);
411         list($containerselector, $containerlocator) = $this->transform_selector($containerselectortype, $containerelement);
412         $destinationxpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($containerselector, $containerlocator);
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         }
423         $this->getSession()->getDriver()->dragTo($sourcexpath, $destinationxpath);
424     }
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) {
439         if (!$this->running_javascript()) {
440             throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
441         }
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     }
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) {
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     }
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) {
489         if (!$this->running_javascript()) {
490             throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
491         }
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     }
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) {
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     }
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) {
541         // Looking for all the matching nodes without any other descendant matching the
542         // same xpath (we are using contains(., ....).
543         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
544         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
545             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
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         }
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         }
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) {
565                 foreach ($args['nodes'] as $node) {
566                     if ($node->isVisible()) {
567                         return true;
568                     }
569                 }
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         );
580     }
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) {
591         // Looking for all the matching nodes without any other descendant matching the
592         // same xpath (we are using contains(., ....).
593         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
594         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
595             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
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         }
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         }
613         // If the element is there we should be sure that it is not visible.
614         $this->spin(
615             function($context, $args) {
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                 }
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         );
632     }
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) {
646         // Getting the container where the text should be found.
647         $container = $this->get_selected_node($selectortype, $element);
649         // Looking for all the matching nodes without any other descendant matching the
650         // same xpath (we are using contains(., ....).
651         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
652         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
653             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
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         }
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         }
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) {
673                 foreach ($args['nodes'] as $node) {
674                     if ($node->isVisible()) {
675                         return true;
676                     }
677                 }
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     }
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) {
700         // Getting the container where the text should be found.
701         $container = $this->get_selected_node($selectortype, $element);
703         // Looking for all the matching nodes without any other descendant matching the
704         // same xpath (we are using contains(., ....).
705         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
706         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
707             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
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         }
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         }
724         // We need to ensure all the found nodes are hidden.
725         $this->spin(
726             function($context, $args) {
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                 }
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     }
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) {
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);
760         $prexpath = $this->find($preselector, $prelocator)->getXpath();
761         $postxpath = $this->find($postselector, $postlocator)->getXpath();
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     }
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) {
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);
787         $postxpath = $this->find($postselector, $postlocator)->getXpath();
788         $prexpath = $this->find($preselector, $prelocator)->getXpath();
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     }
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) {
808         // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
809         $node = $this->get_selected_node($selectortype, $element);
811         if (!$node->hasAttribute('disabled')) {
812             throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
813         }
814     }
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) {
826         // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
827         $node = $this->get_selected_node($selectortype, $element);
829         if ($node->hasAttribute('disabled')) {
830             throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
831         }
832     }
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);
846         if (!$node->hasAttribute('readonly')) {
847             throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
848         }
849     }
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);
863         if ($node->hasAttribute('readonly')) {
864             throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
865         }
866     }
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) {
880         // Getting Mink selector and locator.
881         list($selector, $locator) = $this->transform_selector($selectortype, $element);
883         // Will throw an ElementNotFoundException if it does not exist.
884         $this->find($selector, $locator);
885     }
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) {
899         // Getting Mink selector and locator.
900         list($selector, $locator) = $this->transform_selector($selectortype, $element);
902         try {
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);
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         }
925         throw new ExpectationException('The "' . $element . '" "' . $selectortype .
926                 '" exists in the current page', $this->getSession());
927     }
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     }
938     /**
939      * Checks that an element and selector type exists in another element and selector type on the current page.
940      *
941      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
942      *
943      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
944      * @throws ElementNotFoundException Thrown by behat_base::find
945      * @param string $element The locator of the specified selector
946      * @param string $selectortype The selector type
947      * @param string $containerelement The container selector type
948      * @param string $containerselectortype The container locator
949      */
950     public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
951         // Get the container node.
952         $containernode = $this->get_selected_node($containerselectortype, $containerelement);
954         list($selector, $locator) = $this->transform_selector($selectortype, $element);
956         // Specific exception giving info about where can't we find the element.
957         $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
958         $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
960         // Looks for the requested node inside the container node.
961         $this->find($selector, $locator, $exception, $containernode);
962     }
964     /**
965      * Checks that an element and selector type does not exist in another element and selector type on the current page.
966      *
967      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
968      *
969      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
970      * @throws ExpectationException
971      * @param string $element The locator of the specified selector
972      * @param string $selectortype The selector type
973      * @param string $containerelement The container selector type
974      * @param string $containerselectortype The container locator
975      */
976     public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
978         // Get the container node; here we throw an exception
979         // if the container node does not exist.
980         $containernode = $this->get_selected_node($containerselectortype, $containerelement);
982         list($selector, $locator) = $this->transform_selector($selectortype, $element);
984         // Will throw an ElementNotFoundException if it does not exist, but, actually
985         // it should not exist, so we try & catch it.
986         try {
987             // Would be better to use a 1 second sleep because the element should not be there,
988             // but we would need to duplicate the whole find_all() logic to do it, the benefit of
989             // changing to 1 second sleep is not significant.
990             $this->find($selector, $locator, false, $containernode, self::REDUCED_TIMEOUT);
991         } catch (ElementNotFoundException $e) {
992             // It passes.
993             return;
994         }
995         throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the "' .
996                 $containerelement . '" "' . $containerselectortype . '"', $this->getSession());
997     }
999     /**
1000      * Change browser window size small: 640x480, medium: 1024x768, large: 2560x1600, custom: widthxheight
1001      *
1002      * Example: I change window size to "small" or I change window size to "1024x768"
1003      *
1004      * @throws ExpectationException
1005      * @Then /^I change window size to "(small|medium|large|\d+x\d+)"$/
1006      * @param string $windowsize size of the window (small|medium|large|wxh).
1007      */
1008     public function i_change_window_size_to($windowsize) {
1009         $this->resize_window($windowsize);
1010     }
1012     /**
1013      * Checks whether there is an attribute on the given element that contains the specified text.
1014      *
1015      * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should contain "(?P<text_string>(?:[^"]|\\")*)"$/
1016      * @throws ExpectationException
1017      * @param string $attribute Name of attribute
1018      * @param string $element The locator of the specified selector
1019      * @param string $selectortype The selector type
1020      * @param string $text Expected substring
1021      */
1022     public function the_attribute_of_should_contain($attribute, $element, $selectortype, $text) {
1023         // Get the container node (exception if it doesn't exist).
1024         $containernode = $this->get_selected_node($selectortype, $element);
1025         $value = $containernode->getAttribute($attribute);
1026         if ($value == null) {
1027             throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1028                     $this->getSession());
1029         } else if (strpos($value, $text) === false) {
1030             throw new ExpectationException('The attribute "' . $attribute .
1031                     '" does not contain "' . $text . '" (actual value: "' . $value . '")',
1032                     $this->getSession());
1033         }
1034     }
1036     /**
1037      * Checks that the attribute on the given element does not contain the specified text.
1038      *
1039      * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not contain "(?P<text_string>(?:[^"]|\\")*)"$/
1040      * @throws ExpectationException
1041      * @param string $attribute Name of attribute
1042      * @param string $element The locator of the specified selector
1043      * @param string $selectortype The selector type
1044      * @param string $text Expected substring
1045      */
1046     public function the_attribute_of_should_not_contain($attribute, $element, $selectortype, $text) {
1047         // Get the container node (exception if it doesn't exist).
1048         $containernode = $this->get_selected_node($selectortype, $element);
1049         $value = $containernode->getAttribute($attribute);
1050         if ($value == null) {
1051             throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1052                     $this->getSession());
1053         } else if (strpos($value, $text) !== false) {
1054             throw new ExpectationException('The attribute "' . $attribute .
1055                     '" contains "' . $text . '" (value: "' . $value . '")',
1056                     $this->getSession());
1057         }
1058     }
1060     /**
1061      * Checks the provided value exists in specific row/column of table.
1062      *
1063      * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should contain "(?P<value_string>[^"]*)"$/
1064      * @throws ElementNotFoundException
1065      * @param string $row row text which will be looked in.
1066      * @param string $column column text to search (or numeric value for the column position)
1067      * @param string $table table id/class/caption
1068      * @param string $value text to check.
1069      */
1070     public function row_column_of_table_should_contain($row, $column, $table, $value) {
1071         $tablenode = $this->get_selected_node('table', $table);
1072         $tablexpath = $tablenode->getXpath();
1074         $rowliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($row);
1075         $valueliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($value);
1076         $columnliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($column);
1078         if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
1079             // Column indicated as a number, just use it as position of the column.
1080             $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
1081         } else {
1082             // Header can be in thead or tbody (first row), following xpath should work.
1083             $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1084                     $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
1085             $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1086                     $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
1088             // Check if column exists.
1089             $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
1090             $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
1091             if (empty($columnheader)) {
1092                 $columnexceptionmsg = $column . '" in table "' . $table . '"';
1093                 throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg);
1094             }
1095             // Following conditions were considered before finding column count.
1096             // 1. Table header can be in thead/tr/th or tbody/tr/td[1].
1097             // 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
1098             $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
1099                 "/preceding-sibling::*) + 1]";
1100         }
1102         // Check if value exists in specific row/column.
1103         // Get row xpath.
1104         $rowxpath = $tablexpath."/tbody/tr[th[normalize-space(.)=" . $rowliteral . "] or td[normalize-space(.)=" . $rowliteral . "]]";
1106         $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
1108         // Looks for the requested node inside the container node.
1109         $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
1110         if (empty($coumnnode)) {
1111             $locatorexceptionmsg = $value . '" in "' . $row . '" row with column "' . $column;
1112             throw new ElementNotFoundException($this->getSession(), "\n$columnvaluexpath\n\n".'Column value', null, $locatorexceptionmsg);
1113         }
1114     }
1116     /**
1117      * Checks the provided value should not exist in specific row/column of table.
1118      *
1119      * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should not contain "(?P<value_string>[^"]*)"$/
1120      * @throws ElementNotFoundException
1121      * @param string $row row text which will be looked in.
1122      * @param string $column column text to search
1123      * @param string $table table id/class/caption
1124      * @param string $value text to check.
1125      */
1126     public function row_column_of_table_should_not_contain($row, $column, $table, $value) {
1127         try {
1128             $this->row_column_of_table_should_contain($row, $column, $table, $value);
1129         } catch (ElementNotFoundException $e) {
1130             // Table row/column doesn't contain this value. Nothing to do.
1131             return;
1132         }
1133         // Throw exception if found.
1134         throw new ExpectationException(
1135             '"' . $column . '" with value "' . $value . '" is present in "' . $row . '"  row for table "' . $table . '"',
1136             $this->getSession()
1137         );
1138     }
1140     /**
1141      * Checks that the provided value exist in table.
1142      * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1143      *
1144      * First row may contain column headers or numeric indexes of the columns
1145      * (syntax -1- is also considered to be column index). Column indexes are
1146      * useful in case of multirow headers and/or presence of cells with colspan.
1147      *
1148      * @Then /^the following should exist in the "(?P<table_string>[^"]*)" table:$/
1149      * @throws ExpectationException
1150      * @param string $table name of table
1151      * @param TableNode $data table with first row as header and following values
1152      *        | Header 1 | Header 2 | Header 3 |
1153      *        | Value 1 | Value 2 | Value 3|
1154      */
1155     public function following_should_exist_in_the_table($table, TableNode $data) {
1156         $datahash = $data->getHash();
1158         foreach ($datahash as $row) {
1159             $firstcell = null;
1160             foreach ($row as $column => $value) {
1161                 if ($firstcell === null) {
1162                     $firstcell = $value;
1163                 } else {
1164                     $this->row_column_of_table_should_contain($firstcell, $column, $table, $value);
1165                 }
1166             }
1167         }
1168     }
1170     /**
1171      * Checks that the provided value exist in table.
1172      * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1173      *
1174      * @Then /^the following should not exist in the "(?P<table_string>[^"]*)" table:$/
1175      * @throws ExpectationException
1176      * @param string $table name of table
1177      * @param TableNode $data table with first row as header and following values
1178      *        | Header 1 | Header 2 | Header 3 |
1179      *        | Value 1 | Value 2 | Value 3|
1180      */
1181     public function following_should_not_exist_in_the_table($table, TableNode $data) {
1182         $datahash = $data->getHash();
1184         foreach ($datahash as $value) {
1185             $row = array_shift($value);
1186             foreach ($value as $column => $value) {
1187                 try {
1188                     $this->row_column_of_table_should_contain($row, $column, $table, $value);
1189                     // Throw exception if found.
1190                 } catch (ElementNotFoundException $e) {
1191                     // Table row/column doesn't contain this value. Nothing to do.
1192                     continue;
1193                 }
1194                 throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' .
1195                     $row . '"  row for table "' . $table . '"', $this->getSession()
1196                 );
1197             }
1198         }
1199     }
1201     /**
1202      * Given the text of a link, download the linked file and return the contents.
1203      *
1204      * This is a helper method used by {@link following_should_download_bytes()}
1205      * and {@link following_should_download_between_and_bytes()}
1206      *
1207      * @param string $link the text of the link.
1208      * @return string the content of the downloaded file.
1209      */
1210     protected function download_file_from_link($link) {
1211         // Find the link.
1212         $linknode = $this->find_link($link);
1213         $this->ensure_node_is_visible($linknode);
1215         // Get the href and check it.
1216         $url = $linknode->getAttribute('href');
1217         if (!$url) {
1218             throw new ExpectationException('Download link does not have href attribute',
1219                     $this->getSession());
1220         }
1221         if (!preg_match('~^https?://~', $url)) {
1222             throw new ExpectationException('Download link not an absolute URL: ' . $url,
1223                     $this->getSession());
1224         }
1226         // Download the URL and check the size.
1227         $session = $this->getSession()->getCookie('MoodleSession');
1228         return download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
1229     }
1231     /**
1232      * Downloads the file from a link on the page and checks the size.
1233      *
1234      * Only works if the link has an href attribute. Javascript downloads are
1235      * not supported. Currently, the href must be an absolute URL.
1236      *
1237      * @Then /^following "(?P<link_string>[^"]*)" should download "(?P<expected_bytes>\d+)" bytes$/
1238      * @throws ExpectationException
1239      * @param string $link the text of the link.
1240      * @param number $expectedsize the expected file size in bytes.
1241      */
1242     public function following_should_download_bytes($link, $expectedsize) {
1243         $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1245         // It will stop spinning once file is downloaded or time out.
1246         $result = $this->spin(
1247             function($context, $args) {
1248                 $link = $args['link'];
1249                 return $this->download_file_from_link($link);
1250             },
1251             array('link' => $link),
1252             self::EXTENDED_TIMEOUT,
1253             $exception
1254         );
1256         // Check download size.
1257         $actualsize = (int)strlen($result);
1258         if ($actualsize !== (int)$expectedsize) {
1259             throw new ExpectationException('Downloaded data was ' . $actualsize .
1260                     ' bytes, expecting ' . $expectedsize, $this->getSession());
1261         }
1262     }
1264     /**
1265      * Downloads the file from a link on the page and checks the size is in a given range.
1266      *
1267      * Only works if the link has an href attribute. Javascript downloads are
1268      * not supported. Currently, the href must be an absolute URL.
1269      *
1270      * The range includes the endpoints. That is, a 10 byte file in considered to
1271      * be between "5" and "10" bytes, and between "10" and "20" bytes.
1272      *
1273      * @Then /^following "(?P<link_string>[^"]*)" should download between "(?P<min_bytes>\d+)" and "(?P<max_bytes>\d+)" bytes$/
1274      * @throws ExpectationException
1275      * @param string $link the text of the link.
1276      * @param number $minexpectedsize the minimum expected file size in bytes.
1277      * @param number $maxexpectedsize the maximum expected file size in bytes.
1278      */
1279     public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
1280         // If the minimum is greater than the maximum then swap the values.
1281         if ((int)$minexpectedsize > (int)$maxexpectedsize) {
1282             list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
1283         }
1285         $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1287         // It will stop spinning once file is downloaded or time out.
1288         $result = $this->spin(
1289             function($context, $args) {
1290                 $link = $args['link'];
1292                 return $this->download_file_from_link($link);
1293             },
1294             array('link' => $link),
1295             self::EXTENDED_TIMEOUT,
1296             $exception
1297         );
1299         // Check download size.
1300         $actualsize = (int)strlen($result);
1301         if ($actualsize < $minexpectedsize || $actualsize > $maxexpectedsize) {
1302             throw new ExpectationException('Downloaded data was ' . $actualsize .
1303                     ' bytes, expecting between ' . $minexpectedsize . ' and ' .
1304                     $maxexpectedsize, $this->getSession());
1305         }
1306     }
1308     /**
1309      * Prepare to detect whether or not a new page has loaded (or the same page reloaded) some time in the future.
1310      *
1311      * @Given /^I start watching to see if a new page loads$/
1312      */
1313     public function i_start_watching_to_see_if_a_new_page_loads() {
1314         if (!$this->running_javascript()) {
1315             throw new DriverException('Page load detection requires JavaScript.');
1316         }
1318         $session = $this->getSession();
1320         if ($this->pageloaddetectionrunning || $session->getPage()->find('xpath', $this->get_page_load_xpath())) {
1321             // If we find this node at this point we are already watching for a reload and the behat steps
1322             // are out of order. We will treat this as an error - really it needs to be fixed as it indicates a problem.
1323             throw new ExpectationException(
1324                 'Page load expectation error: page reloads are already been watched for.', $session);
1325         }
1327         $this->pageloaddetectionrunning = true;
1329         $session->evaluateScript(
1330                 'var span = document.createElement("span");
1331                 span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
1332                 span.setAttribute("style", "display: none;");
1333                 document.body.appendChild(span);');
1334     }
1336     /**
1337      * Verify that a new page has loaded (or the same page has reloaded) since the
1338      * last "I start watching to see if a new page loads" step.
1339      *
1340      * @Given /^a new page should have loaded since I started watching$/
1341      */
1342     public function a_new_page_should_have_loaded_since_i_started_watching() {
1343         $session = $this->getSession();
1345         // Make sure page load tracking was started.
1346         if (!$this->pageloaddetectionrunning) {
1347             throw new ExpectationException(
1348                 'Page load expectation error: page load tracking was not started.', $session);
1349         }
1351         // As the node is inserted by code above it is either there or not, and we do not need spin and it is safe
1352         // to use the native API here which is great as exception handling (the alternative is slow).
1353         if ($session->getPage()->find('xpath', $this->get_page_load_xpath())) {
1354             // We don't want to find this node, if we do we have an error.
1355             throw new ExpectationException(
1356                 'Page load expectation error: a new page has not been loaded when it should have been.', $session);
1357         }
1359         // Cancel the tracking of pageloaddetectionrunning.
1360         $this->pageloaddetectionrunning = false;
1361     }
1363     /**
1364      * Verify that a new page has not loaded (or the same page has reloaded) since the
1365      * last "I start watching to see if a new page loads" step.
1366      *
1367      * @Given /^a new page should not have loaded since I started watching$/
1368      */
1369     public function a_new_page_should_not_have_loaded_since_i_started_watching() {
1370         $session = $this->getSession();
1372         // Make sure page load tracking was started.
1373         if (!$this->pageloaddetectionrunning) {
1374             throw new ExpectationException(
1375                 'Page load expectation error: page load tracking was not started.', $session);
1376         }
1378         // We use our API here as we can use the exception handling provided by it.
1379         $this->find(
1380             'xpath',
1381             $this->get_page_load_xpath(),
1382             new ExpectationException(
1383                 'Page load expectation error: A new page has been loaded when it should not have been.',
1384                 $this->getSession()
1385             )
1386         );
1387     }
1389     /**
1390      * Helper used by {@link a_new_page_should_have_loaded_since_i_started_watching}
1391      * and {@link a_new_page_should_not_have_loaded_since_i_started_watching}
1392      * @return string xpath expression.
1393      */
1394     protected function get_page_load_xpath() {
1395         return "//span[@data-rel = '" . self::PAGE_LOAD_DETECTION_STRING . "']";
1396     }
1398     /**
1399      * Wait unit user press Enter/Return key. Useful when debugging a scenario.
1400      *
1401      * @Then /^(?:|I )pause(?:| scenario execution)$/
1402      */
1403     public function i_pause_scenario_executon() {
1404         global $CFG;
1406         $posixexists = function_exists('posix_isatty');
1408         // Make sure this step is only used with interactive terminal (if detected).
1409         if ($posixexists && !@posix_isatty(STDOUT)) {
1410             $session = $this->getSession();
1411             throw new ExpectationException('Break point should only be used with interative terminal.', $session);
1412         }
1414         // Windows don't support ANSI code by default, but with ANSICON.
1415         $isansicon = getenv('ANSICON');
1416         if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) {
1417             fwrite(STDOUT, "Paused. Press Enter/Return to continue.");
1418             fread(STDIN, 1024);
1419         } else {
1420             fwrite(STDOUT, "\033[s\n\033[0;93mPaused. Press \033[1;31mEnter/Return\033[0;93m to continue.\033[0m");
1421             fread(STDIN, 1024);
1422             fwrite(STDOUT, "\033[2A\033[u\033[2B");
1423         }
1424     }
1426     /**
1427      * Presses a given button in the browser.
1428      * NOTE: Phantomjs and goutte driver reloads page while navigating back and forward.
1429      *
1430      * @Then /^I press the "(back|forward|reload)" button in the browser$/
1431      * @param string $button the button to press.
1432      * @throws ExpectationException
1433      */
1434     public function i_press_in_the_browser($button) {
1435         $session = $this->getSession();
1437         if ($button == 'back') {
1438             $session->back();
1439         } else if ($button == 'forward') {
1440             $session->forward();
1441         } else if ($button == 'reload') {
1442             $session->reload();
1443         } else {
1444             throw new ExpectationException('Unknown browser button.', $session);
1445         }
1446     }