ada004e97dfb4d3e3e2bbeaac8e8fd13b591cd25
[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;
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 {
52     /**
53      * Opens Moodle homepage.
54      *
55      * @Given /^I am on homepage$/
56      */
57     public function i_am_on_homepage() {
58         $this->getSession()->visit($this->locate_path('/'));
59     }
61     /**
62      * Reloads the current page.
63      *
64      * @Given /^I reload the page$/
65      */
66     public function reload() {
67         $this->getSession()->reload();
68     }
70     /**
71      * Follows the page redirection. Use this step after any action that shows a message and waits for a redirection
72      *
73      * @Given /^I wait to be redirected$/
74      */
75     public function i_wait_to_be_redirected() {
77         // Xpath and processes based on core_renderer::redirect_message(), core_renderer::$metarefreshtag and
78         // moodle_page::$periodicrefreshdelay possible values.
79         if (!$metarefresh = $this->getSession()->getPage()->find('xpath', "//head/descendant::meta[@http-equiv='refresh']")) {
80             // We don't fail the scenario if no redirection with message is found to avoid race condition false failures.
81             return true;
82         }
84         // Wrapped in try & catch in case the redirection has already been executed.
85         try {
86             $content = $metarefresh->getAttribute('content');
87         } catch (NoSuchElement $e) {
88             return true;
89         } catch (StaleElementReference $e) {
90             return true;
91         }
93         // Getting the refresh time and the url if present.
94         if (strstr($content, 'url') != false) {
96             list($waittime, $url) = explode(';', $content);
98             // Cleaning the URL value.
99             $url = trim(substr($url, strpos($url, 'http')));
101         } else {
102             // Just wait then.
103             $waittime = $content;
104         }
107         // Wait until the URL change is executed.
108         if ($this->running_javascript()) {
109             $this->getSession()->wait($waittime * 1000, false);
111         } else if (!empty($url)) {
112             // We redirect directly as we can not wait for an automatic redirection.
113             $this->getSession()->getDriver()->getClient()->request('get', $url);
115         } else {
116             // Reload the page if no URL was provided.
117             $this->getSession()->getDriver()->reload();
118         }
119     }
121     /**
122      * Switches to the specified iframe.
123      *
124      * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe$/
125      * @param string $iframename
126      */
127     public function switch_to_iframe($iframename) {
129         // We spin to give time to the iframe to be loaded.
130         // Using extended timeout as we don't know about which
131         // kind of iframe will be loaded.
132         $this->spin(
133             function($context, $iframename) {
134                 $context->getSession()->switchToIFrame($iframename);
136                 // If no exception we are done.
137                 return true;
138             },
139             $iframename,
140             self::EXTENDED_TIMEOUT
141         );
142     }
144     /**
145      * Switches to the main Moodle frame.
146      *
147      * @Given /^I switch to the main frame$/
148      */
149     public function switch_to_the_main_frame() {
150         $this->getSession()->switchToIFrame();
151     }
153     /**
154      * Switches to the specified window. Useful when interacting with popup windows.
155      *
156      * @Given /^I switch to "(?P<window_name_string>(?:[^"]|\\")*)" window$/
157      * @param string $windowname
158      */
159     public function switch_to_window($windowname) {
160         $this->getSession()->switchToWindow($windowname);
161     }
163     /**
164      * Switches to the main Moodle window. Useful when you finish interacting with popup windows.
165      *
166      * @Given /^I switch to the main window$/
167      */
168     public function switch_to_the_main_window() {
169         $this->getSession()->switchToWindow();
170     }
172     /**
173      * Accepts the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
174      * @Given /^I accept the currently displayed dialog$/
175      */
176     public function accept_currently_displayed_alert_dialog() {
177         $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
178     }
180     /**
181      * Clicks link with specified id|title|alt|text.
182      *
183      * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
184      * @throws ElementNotFoundException Thrown by behat_base::find
185      * @param string $link
186      */
187     public function click_link($link) {
189         $linknode = $this->find_link($link);
190         $this->ensure_node_is_visible($linknode);
191         $linknode->click();
192     }
194     /**
195      * Waits X seconds. Required after an action that requires data from an AJAX request.
196      *
197      * @Then /^I wait "(?P<seconds_number>\d+)" seconds$/
198      * @param int $seconds
199      */
200     public function i_wait_seconds($seconds) {
202         if (!$this->running_javascript()) {
203             throw new DriverException('Waits are disabled in scenarios without Javascript support');
204         }
206         $this->getSession()->wait($seconds * 1000, false);
207     }
209     /**
210      * Waits until the page is completely loaded. This step is auto-executed after every step.
211      *
212      * @Given /^I wait until the page is ready$/
213      */
214     public function wait_until_the_page_is_ready() {
216         if (!$this->running_javascript()) {
217             throw new DriverException('Waits are disabled in scenarios without Javascript support');
218         }
220         $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
221     }
223     /**
224      * Waits until the provided element selector exists in the DOM
225      *
226      * Using the protected method as this method will be usually
227      * called by other methods which are not returning a set of
228      * steps and performs the actions directly, so it would not
229      * be executed if it returns another step.
231      * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
232      * @param string $element
233      * @param string $selector
234      * @return void
235      */
236     public function wait_until_exists($element, $selectortype) {
237         $this->ensure_element_exists($element, $selectortype);
238     }
240     /**
241      * Waits until the provided element does not exist in the DOM
242      *
243      * Using the protected method as this method will be usually
244      * called by other methods which are not returning a set of
245      * steps and performs the actions directly, so it would not
246      * be executed if it returns another step.
248      * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
249      * @param string $element
250      * @param string $selector
251      * @return void
252      */
253     public function wait_until_does_not_exists($element, $selectortype) {
254         $this->ensure_element_does_not_exist($element, $selectortype);
255     }
257     /**
258      * Generic mouse over action. Mouse over a element of the specified type.
259      *
260      * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
261      * @param string $element Element we look for
262      * @param string $selectortype The type of what we look for
263      */
264     public function i_hover($element, $selectortype) {
266         // Gets the node based on the requested selector type and locator.
267         $node = $this->get_selected_node($selectortype, $element);
268         $node->mouseOver();
269     }
271     /**
272      * Generic click action. Click on the element of the specified type.
273      *
274      * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
275      * @param string $element Element we look for
276      * @param string $selectortype The type of what we look for
277      */
278     public function i_click_on($element, $selectortype) {
280         // Gets the node based on the requested selector type and locator.
281         $node = $this->get_selected_node($selectortype, $element);
282         $this->ensure_node_is_visible($node);
283         $node->click();
284     }
286     /**
287      * Sets the focus and takes away the focus from an element, generating blur JS event.
288      *
289      * @When /^I take focus off "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
290      * @param string $element Element we look for
291      * @param string $selectortype The type of what we look for
292      */
293     public function i_take_focus_off_field($element, $selectortype) {
294         if (!$this->running_javascript()) {
295             throw new ExpectationException('Can\'t take focus off from "' . $element . '" in non-js mode', $this->getSession());
296         }
297         // Gets the node based on the requested selector type and locator.
298         $node = $this->get_selected_node($selectortype, $element);
299         $this->ensure_node_is_visible($node);
301         // Ensure element is focused before taking it off.
302         $node->focus();
303         $node->blur();
304     }
306     /**
307      * Clicks the specified element and confirms the expected dialogue.
308      *
309      * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" confirming the dialogue$/
310      * @throws ElementNotFoundException Thrown by behat_base::find
311      * @param string $link
312      */
313     public function i_click_on_confirming_the_dialogue($element, $selectortype) {
314         $this->i_click_on($element, $selectortype);
315         $this->accept_currently_displayed_alert_dialog();
316     }
318     /**
319      * Click on the element of the specified type which is located inside the second element.
320      *
321      * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
322      * @param string $element Element we look for
323      * @param string $selectortype The type of what we look for
324      * @param string $nodeelement Element we look in
325      * @param string $nodeselectortype The type of selector where we look in
326      */
327     public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {
329         $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
330         $this->ensure_node_is_visible($node);
331         $node->click();
332     }
334     /**
335      * Drags and drops the specified element to the specified container. This step does not work in all the browsers, consider it experimental.
336      *
337      * The steps definitions calling this step as part of them should
338      * manage the wait times by themselves as the times and when the
339      * waits should be done depends on what is being dragged & dropper.
340      *
341      * @Given /^I drag "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" and I drop it in "(?P<container_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
342      * @param string $element
343      * @param string $selectortype
344      * @param string $containerelement
345      * @param string $containerselectortype
346      */
347     public function i_drag_and_i_drop_it_in($element, $selectortype, $containerelement, $containerselectortype) {
349         list($sourceselector, $sourcelocator) = $this->transform_selector($selectortype, $element);
350         $sourcexpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($sourceselector, $sourcelocator);
352         list($containerselector, $containerlocator) = $this->transform_selector($containerselectortype, $containerelement);
353         $destinationxpath = $this->getSession()->getSelectorsHandler()->selectorToXpath($containerselector, $containerlocator);
355         $this->getSession()->getDriver()->dragTo($sourcexpath, $destinationxpath);
356     }
358     /**
359      * Checks, that the specified element is visible. Only available in tests using Javascript.
360      *
361      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should be visible$/
362      * @throws ElementNotFoundException
363      * @throws ExpectationException
364      * @throws DriverException
365      * @param string $element
366      * @param string $selectortype
367      * @return void
368      */
369     public function should_be_visible($element, $selectortype) {
371         if (!$this->running_javascript()) {
372             throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
373         }
375         $node = $this->get_selected_node($selectortype, $element);
376         if (!$node->isVisible()) {
377             throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is not visible', $this->getSession());
378         }
379     }
381     /**
382      * Checks, that the existing element is not visible. Only available in tests using Javascript.
383      *
384      * As a "not" method, it's performance could not be good, but in this
385      * case the performance is good because the element must exist,
386      * otherwise there would be a ElementNotFoundException, also here we are
387      * not spinning until the element is visible.
388      *
389      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should not be visible$/
390      * @throws ElementNotFoundException
391      * @throws ExpectationException
392      * @param string $element
393      * @param string $selectortype
394      * @return void
395      */
396     public function should_not_be_visible($element, $selectortype) {
398         try {
399             $this->should_be_visible($element, $selectortype);
400             throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is visible', $this->getSession());
401         } catch (ExpectationException $e) {
402             // All as expected.
403         }
404     }
406     /**
407      * Checks, that the specified element is visible inside the specified container. Only available in tests using Javascript.
408      *
409      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should be visible$/
410      * @throws ElementNotFoundException
411      * @throws DriverException
412      * @throws ExpectationException
413      * @param string $element Element we look for
414      * @param string $selectortype The type of what we look for
415      * @param string $nodeelement Element we look in
416      * @param string $nodeselectortype The type of selector where we look in
417      */
418     public function in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
420         if (!$this->running_javascript()) {
421             throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
422         }
424         $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
425         if (!$node->isVisible()) {
426             throw new ExpectationException(
427                 '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is not visible',
428                 $this->getSession()
429             );
430         }
431     }
433     /**
434      * Checks, that the existing element is not visible inside the existing container. Only available in tests using Javascript.
435      *
436      * As a "not" method, it's performance could not be good, but in this
437      * case the performance is good because the element must exist,
438      * otherwise there would be a ElementNotFoundException, also here we are
439      * not spinning until the element is visible.
440      *
441      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should not be visible$/
442      * @throws ElementNotFoundException
443      * @throws ExpectationException
444      * @param string $element Element we look for
445      * @param string $selectortype The type of what we look for
446      * @param string $nodeelement Element we look in
447      * @param string $nodeselectortype The type of selector where we look in
448      */
449     public function in_the_should_not_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
451         try {
452             $this->in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype);
453             throw new ExpectationException(
454                 '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is visible',
455                 $this->getSession()
456             );
457         } catch (ExpectationException $e) {
458             // All as expected.
459         }
460     }
462     /**
463      * Checks, that page contains specified text. It also checks if the text is visible when running Javascript tests.
464      *
465      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
466      * @throws ExpectationException
467      * @param string $text
468      */
469     public function assert_page_contains_text($text) {
471         // Looking for all the matching nodes without any other descendant matching the
472         // same xpath (we are using contains(., ....).
473         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
474         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
475             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
477         try {
478             $nodes = $this->find_all('xpath', $xpath);
479         } catch (ElementNotFoundException $e) {
480             throw new ExpectationException('"' . $text . '" text was not found in the page', $this->getSession());
481         }
483         // If we are not running javascript we have enough with the
484         // element existing as we can't check if it is visible.
485         if (!$this->running_javascript()) {
486             return;
487         }
489         // We spin as we don't have enough checking that the element is there, we
490         // should also ensure that the element is visible. Using microsleep as this
491         // is a repeated step and global performance is important.
492         $this->spin(
493             function($context, $args) {
495                 foreach ($args['nodes'] as $node) {
496                     if ($node->isVisible()) {
497                         return true;
498                     }
499                 }
501                 // If non of the nodes is visible we loop again.
502                 throw new ExpectationException('"' . $args['text'] . '" text was found but was not visible', $context->getSession());
503             },
504             array('nodes' => $nodes, 'text' => $text),
505             false,
506             false,
507             true
508         );
510     }
512     /**
513      * Checks, that page doesn't contain specified text. When running Javascript tests it also considers that texts may be hidden.
514      *
515      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
516      * @throws ExpectationException
517      * @param string $text
518      */
519     public function assert_page_not_contains_text($text) {
521         // Looking for all the matching nodes without any other descendant matching the
522         // same xpath (we are using contains(., ....).
523         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
524         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
525             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
527         // We should wait a while to ensure that the page is not still loading elements.
528         // Waiting less than self::TIMEOUT as we already waited for the DOM to be ready and
529         // all JS to be executed.
530         try {
531             $nodes = $this->find_all('xpath', $xpath, false, false, self::REDUCED_TIMEOUT);
532         } catch (ElementNotFoundException $e) {
533             // All ok.
534             return;
535         }
537         // If we are not running javascript we have enough with the
538         // element existing as we can't check if it is hidden.
539         if (!$this->running_javascript()) {
540             throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
541         }
543         // If the element is there we should be sure that it is not visible.
544         $this->spin(
545             function($context, $args) {
547                 foreach ($args['nodes'] as $node) {
548                     if ($node->isVisible()) {
549                         throw new ExpectationException('"' . $args['text'] . '" text was found in the page', $context->getSession());
550                     }
551                 }
553                 // If non of the found nodes is visible we consider that the text is not visible.
554                 return true;
555             },
556             array('nodes' => $nodes, 'text' => $text),
557             self::REDUCED_TIMEOUT,
558             false,
559             true
560         );
562     }
564     /**
565      * Checks, that the specified element contains the specified text. When running Javascript tests it also considers that texts may be hidden.
566      *
567      * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
568      * @throws ElementNotFoundException
569      * @throws ExpectationException
570      * @param string $text
571      * @param string $element Element we look in.
572      * @param string $selectortype The type of element where we are looking in.
573      */
574     public function assert_element_contains_text($text, $element, $selectortype) {
576         // Getting the container where the text should be found.
577         $container = $this->get_selected_node($selectortype, $element);
579         // Looking for all the matching nodes without any other descendant matching the
580         // same xpath (we are using contains(., ....).
581         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
582         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
583             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
585         // Wait until it finds the text inside the container, otherwise custom exception.
586         try {
587             $nodes = $this->find_all('xpath', $xpath, false, $container);
588         } catch (ElementNotFoundException $e) {
589             throw new ExpectationException('"' . $text . '" text was not found in the "' . $element . '" element', $this->getSession());
590         }
592         // If we are not running javascript we have enough with the
593         // element existing as we can't check if it is visible.
594         if (!$this->running_javascript()) {
595             return;
596         }
598         // We also check the element visibility when running JS tests. Using microsleep as this
599         // is a repeated step and global performance is important.
600         $this->spin(
601             function($context, $args) {
603                 foreach ($args['nodes'] as $node) {
604                     if ($node->isVisible()) {
605                         return true;
606                     }
607                 }
609                 throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element but was not visible', $context->getSession());
610             },
611             array('nodes' => $nodes, 'text' => $text, 'element' => $element),
612             false,
613             false,
614             true
615         );
616     }
618     /**
619      * Checks, that the specified element does not contain the specified text. When running Javascript tests it also considers that texts may be hidden.
620      *
621      * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
622      * @throws ElementNotFoundException
623      * @throws ExpectationException
624      * @param string $text
625      * @param string $element Element we look in.
626      * @param string $selectortype The type of element where we are looking in.
627      */
628     public function assert_element_not_contains_text($text, $element, $selectortype) {
630         // Getting the container where the text should be found.
631         $container = $this->get_selected_node($selectortype, $element);
633         // Looking for all the matching nodes without any other descendant matching the
634         // same xpath (we are using contains(., ....).
635         $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
636         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
637             "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
639         // We should wait a while to ensure that the page is not still loading elements.
640         // Giving preference to the reliability of the results rather than to the performance.
641         try {
642             $nodes = $this->find_all('xpath', $xpath, false, $container, self::REDUCED_TIMEOUT);
643         } catch (ElementNotFoundException $e) {
644             // All ok.
645             return;
646         }
648         // If we are not running javascript we have enough with the
649         // element not being found as we can't check if it is visible.
650         if (!$this->running_javascript()) {
651             throw new ExpectationException('"' . $text . '" text was found in the "' . $element . '" element', $this->getSession());
652         }
654         // We need to ensure all the found nodes are hidden.
655         $this->spin(
656             function($context, $args) {
658                 foreach ($args['nodes'] as $node) {
659                     if ($node->isVisible()) {
660                         throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element', $context->getSession());
661                     }
662                 }
664                 // If all the found nodes are hidden we are happy.
665                 return true;
666             },
667             array('nodes' => $nodes, 'text' => $text, 'element' => $element),
668             self::REDUCED_TIMEOUT,
669             false,
670             true
671         );
672     }
674     /**
675      * Checks, that the first specified element appears before the second one.
676      *
677      * @Given /^"(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear before "(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
678      * @throws ExpectationException
679      * @param string $preelement The locator of the preceding element
680      * @param string $preselectortype The locator of the preceding element
681      * @param string $postelement The locator of the latest element
682      * @param string $postselectortype The selector type of the latest element
683      */
684     public function should_appear_before($preelement, $preselectortype, $postelement, $postselectortype) {
686         // We allow postselectortype as a non-text based selector.
687         list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
688         list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
690         $prexpath = $this->find($preselector, $prelocator)->getXpath();
691         $postxpath = $this->find($postselector, $postlocator)->getXpath();
693         // Using following xpath axe to find it.
694         $msg = '"'.$preelement.'" "'.$preselectortype.'" does not appear before "'.$postelement.'" "'.$postselectortype.'"';
695         $xpath = $prexpath.'/following::*[contains(., '.$postxpath.')]';
696         if (!$this->getSession()->getDriver()->find($xpath)) {
697             throw new ExpectationException($msg, $this->getSession());
698         }
699     }
701     /**
702      * Checks, that the first specified element appears after the second one.
703      *
704      * @Given /^"(?P<following_element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" should appear after "(?P<preceding_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
705      * @throws ExpectationException
706      * @param string $postelement The locator of the latest element
707      * @param string $postselectortype The selector type of the latest element
708      * @param string $preelement The locator of the preceding element
709      * @param string $preselectortype The locator of the preceding element
710      */
711     public function should_appear_after($postelement, $postselectortype, $preelement, $preselectortype) {
713         // We allow postselectortype as a non-text based selector.
714         list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
715         list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
717         $postxpath = $this->find($postselector, $postlocator)->getXpath();
718         $prexpath = $this->find($preselector, $prelocator)->getXpath();
720         // Using preceding xpath axe to find it.
721         $msg = '"'.$postelement.'" "'.$postselectortype.'" does not appear after "'.$preelement.'" "'.$preselectortype.'"';
722         $xpath = $postxpath.'/preceding::*[contains(., '.$prexpath.')]';
723         if (!$this->getSession()->getDriver()->find($xpath)) {
724             throw new ExpectationException($msg, $this->getSession());
725         }
726     }
728     /**
729      * Checks, that element of specified type is disabled.
730      *
731      * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be disabled$/
732      * @throws ExpectationException Thrown by behat_base::find
733      * @param string $element Element we look in
734      * @param string $selectortype The type of element where we are looking in.
735      */
736     public function the_element_should_be_disabled($element, $selectortype) {
738         // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
739         $node = $this->get_selected_node($selectortype, $element);
741         if (!$node->hasAttribute('disabled')) {
742             throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
743         }
744     }
746     /**
747      * Checks, that element of specified type is enabled.
748      *
749      * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be enabled$/
750      * @throws ExpectationException Thrown by behat_base::find
751      * @param string $element Element we look on
752      * @param string $selectortype The type of where we look
753      */
754     public function the_element_should_be_enabled($element, $selectortype) {
756         // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
757         $node = $this->get_selected_node($selectortype, $element);
759         if ($node->hasAttribute('disabled')) {
760             throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
761         }
762     }
764     /**
765      * Checks the provided element and selector type are readonly on the current page.
766      *
767      * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be readonly$/
768      * @throws ExpectationException Thrown by behat_base::find
769      * @param string $element Element we look in
770      * @param string $selectortype The type of element where we are looking in.
771      */
772     public function the_element_should_be_readonly($element, $selectortype) {
773         // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
774         $node = $this->get_selected_node($selectortype, $element);
776         if (!$node->hasAttribute('readonly')) {
777             throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
778         }
779     }
781     /**
782      * Checks the provided element and selector type are not readonly on the current page.
783      *
784      * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not be readonly$/
785      * @throws ExpectationException Thrown by behat_base::find
786      * @param string $element Element we look in
787      * @param string $selectortype The type of element where we are looking in.
788      */
789     public function the_element_should_not_be_readonly($element, $selectortype) {
790         // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
791         $node = $this->get_selected_node($selectortype, $element);
793         if ($node->hasAttribute('readonly')) {
794             throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
795         }
796     }
798     /**
799      * Checks the provided element and selector type exists in the current page.
800      *
801      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
802      *
803      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist$/
804      * @throws ElementNotFoundException Thrown by behat_base::find
805      * @param string $element The locator of the specified selector
806      * @param string $selectortype The selector type
807      */
808     public function should_exist($element, $selectortype) {
810         // Getting Mink selector and locator.
811         list($selector, $locator) = $this->transform_selector($selectortype, $element);
813         // Will throw an ElementNotFoundException if it does not exist.
814         $this->find($selector, $locator);
815     }
817     /**
818      * Checks that the provided element and selector type not exists in the current page.
819      *
820      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
821      *
822      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist$/
823      * @throws ExpectationException
824      * @param string $element The locator of the specified selector
825      * @param string $selectortype The selector type
826      */
827     public function should_not_exist($element, $selectortype) {
829         // Getting Mink selector and locator.
830         list($selector, $locator) = $this->transform_selector($selectortype, $element);
832         try {
834             // Using directly the spin method as we want a reduced timeout but there is no
835             // need for a 0.1 seconds interval because in the optimistic case we will timeout.
836             $params = array('selector' => $selector, 'locator' => $locator);
837             // The exception does not really matter as we will catch it and will never "explode".
838             $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $element);
840             // If all goes good it will throw an ElementNotFoundExceptionn that we will catch.
841             $this->spin(
842                 function($context, $args) {
843                     return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']);
844                 },
845                 $params,
846                 self::REDUCED_TIMEOUT,
847                 $exception,
848                 false
849             );
851             throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the current page', $this->getSession());
852         } catch (ElementNotFoundException $e) {
853             // It passes.
854             return;
855         }
856     }
858     /**
859      * This step triggers cron like a user would do going to admin/cron.php.
860      *
861      * @Given /^I trigger cron$/
862      */
863     public function i_trigger_cron() {
864         $this->getSession()->visit($this->locate_path('/admin/cron.php'));
865     }
867     /**
868      * Checks that an element and selector type exists in another element and selector type on the current page.
869      *
870      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
871      *
872      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
873      * @throws ElementNotFoundException Thrown by behat_base::find
874      * @param string $element The locator of the specified selector
875      * @param string $selectortype The selector type
876      * @param string $containerelement The container selector type
877      * @param string $containerselectortype The container locator
878      */
879     public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
880         // Get the container node.
881         $containernode = $this->get_selected_node($containerselectortype, $containerelement);
883         list($selector, $locator) = $this->transform_selector($selectortype, $element);
885         // Specific exception giving info about where can't we find the element.
886         $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"';
887         $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
889         // Looks for the requested node inside the container node.
890         $this->find($selector, $locator, $exception, $containernode);
891     }
893     /**
894      * Checks that an element and selector type does not exist in another element and selector type on the current page.
895      *
896      * This step is for advanced users, use it if you don't find anything else suitable for what you need.
897      *
898      * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
899      * @throws ExpectationException
900      * @param string $element The locator of the specified selector
901      * @param string $selectortype The selector type
902      * @param string $containerelement The container selector type
903      * @param string $containerselectortype The container locator
904      */
905     public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
907         // Get the container node; here we throw an exception
908         // if the container node does not exist.
909         $containernode = $this->get_selected_node($containerselectortype, $containerelement);
911         list($selector, $locator) = $this->transform_selector($selectortype, $element);
913         // Will throw an ElementNotFoundException if it does not exist, but, actually
914         // it should not exist, so we try & catch it.
915         try {
916             // Would be better to use a 1 second sleep because the element should not be there,
917             // but we would need to duplicate the whole find_all() logic to do it, the benefit of
918             // changing to 1 second sleep is not significant.
919             $this->find($selector, $locator, false, $containernode, self::REDUCED_TIMEOUT);
920             throw new ExpectationException('The "' . $element . '" "' . $selectortype . '" exists in the "' .
921                 $containerelement . '" "' . $containerselectortype . '"', $this->getSession());
922         } catch (ElementNotFoundException $e) {
923             // It passes.
924             return;
925         }
926     }
928     /**
929      * Change browser window size small: 640x480, medium: 1024x768, large: 2560x1600, custom: widthxheight
930      *
931      * Example: I change window size to "small" or I change window size to "1024x768"
932      *
933      * @throws ExpectationException
934      * @Then /^I change window size to "([^"](small|medium|large|\d+x\d+))"$/
935      * @param string $windowsize size of the window (small|medium|large|wxh).
936      */
937     public function i_change_window_size_to($windowsize) {
938         $this->resize_window($windowsize);
939     }
941     /**
942      * Checks whether there is an attribute on the given element that contains the specified text.
943      *
944      * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should contain "(?P<text_string>(?:[^"]|\\")*)"$/
945      * @throws ExpectationException
946      * @param string $attribute Name of attribute
947      * @param string $element The locator of the specified selector
948      * @param string $selectortype The selector type
949      * @param string $text Expected substring
950      */
951     public function the_attribute_of_should_contain($attribute, $element, $selectortype, $text) {
952         // Get the container node (exception if it doesn't exist).
953         $containernode = $this->get_selected_node($selectortype, $element);
954         $value = $containernode->getAttribute($attribute);
955         if ($value == null) {
956             throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
957                     $this->getSession());
958         } else if (strpos($value, $text) === false) {
959             throw new ExpectationException('The attribute "' . $attribute .
960                     '" does not contain "' . $text . '" (actual value: "' . $value . '")',
961                     $this->getSession());
962         }
963     }
965     /**
966      * Checks that the attribute on the given element does not contain the specified text.
967      *
968      * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not contain "(?P<text_string>(?:[^"]|\\")*)"$/
969      * @throws ExpectationException
970      * @param string $attribute Name of attribute
971      * @param string $element The locator of the specified selector
972      * @param string $selectortype The selector type
973      * @param string $text Expected substring
974      */
975     public function the_attribute_of_should_not_contain($attribute, $element, $selectortype, $text) {
976         // Get the container node (exception if it doesn't exist).
977         $containernode = $this->get_selected_node($selectortype, $element);
978         $value = $containernode->getAttribute($attribute);
979         if ($value == null) {
980             throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
981                     $this->getSession());
982         } else if (strpos($value, $text) !== false) {
983             throw new ExpectationException('The attribute "' . $attribute .
984                     '" contains "' . $text . '" (value: "' . $value . '")',
985                     $this->getSession());
986         }
987     }
989     /**
990      * Checks the provided value exists in specific row/column of table.
991      *
992      * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should contain "(?P<value_string>[^"]*)"$/
993      * @throws ElementNotFoundException
994      * @param string $row row text which will be looked in.
995      * @param string $column column text to search (or numeric value for the column position)
996      * @param string $table table id/class/caption
997      * @param string $value text to check.
998      */
999     public function row_column_of_table_should_contain($row, $column, $table, $value) {
1000         $tablenode = $this->get_selected_node('table', $table);
1001         $tablexpath = $tablenode->getXpath();
1003         $rowliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($row);
1004         $valueliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($value);
1005         $columnliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($column);
1007         if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
1008             // Column indicated as a number, just use it as position of the column.
1009             $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
1010         } else {
1011             // Header can be in thead or tbody (first row), following xpath should work.
1012             $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1013                 $columnliteral . "])]";
1014             $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
1015                 $columnliteral . "])]";
1017             // Check if column exists.
1018             $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
1019             $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
1020             if (empty($columnheader)) {
1021                 $columnexceptionmsg = $column . '" in table "' . $table . '"';
1022                 throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg);
1023             }
1024             // Following conditions were considered before finding column count.
1025             // 1. Table header can be in thead/tr/th or tbody/tr/td[1].
1026             // 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
1027             $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
1028                 "/preceding-sibling::*) + 1]";
1029         }
1031         // Check if value exists in specific row/column.
1032         // Get row xpath.
1033         $rowxpath = $tablexpath."/tbody/tr[th[normalize-space(.)=" . $rowliteral . "] | td[normalize-space(.)=" . $rowliteral . "]]";
1035         $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
1037         // Looks for the requested node inside the container node.
1038         $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
1039         if (empty($coumnnode)) {
1040             $locatorexceptionmsg = $value . '" in "' . $row . '" row with column "' . $column;
1041             throw new ElementNotFoundException($this->getSession(), "\n$columnvaluexpath\n\n".'Column value', null, $locatorexceptionmsg);
1042         }
1043     }
1045     /**
1046      * Checks the provided value should not exist in specific row/column of table.
1047      *
1048      * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should not contain "(?P<value_string>[^"]*)"$/
1049      * @throws ElementNotFoundException
1050      * @param string $row row text which will be looked in.
1051      * @param string $column column text to search
1052      * @param string $table table id/class/caption
1053      * @param string $value text to check.
1054      */
1055     public function row_column_of_table_should_not_contain($row, $column, $table, $value) {
1056         try {
1057             $this->row_column_of_table_should_contain($row, $column, $table, $value);
1058             // Throw exception if found.
1059             throw new ExpectationException(
1060                 '"' . $column . '" with value "' . $value . '" is present in "' . $row . '"  row for table "' . $table . '"',
1061                 $this->getSession()
1062             );
1063         } catch (ElementNotFoundException $e) {
1064             // Table row/column doesn't contain this value. Nothing to do.
1065             return;
1066         }
1067     }
1069     /**
1070      * Checks that the provided value exist in table.
1071      * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1072      *
1073      * First row may contain column headers or numeric indexes of the columns
1074      * (syntax -1- is also considered to be column index). Column indexes are
1075      * useful in case of multirow headers and/or presence of cells with colspan.
1076      *
1077      * @Then /^the following should exist in the "(?P<table_string>[^"]*)" table:$/
1078      * @throws ExpectationException
1079      * @param string $table name of table
1080      * @param TableNode $data table with first row as header and following values
1081      *        | Header 1 | Header 2 | Header 3 |
1082      *        | Value 1 | Value 2 | Value 3|
1083      */
1084     public function following_should_exist_in_the_table($table, TableNode $data) {
1085         $datahash = $data->getHash();
1087         foreach ($datahash as $row) {
1088             $firstcell = null;
1089             foreach ($row as $column => $value) {
1090                 if ($firstcell === null) {
1091                     $firstcell = $value;
1092                 } else {
1093                     $this->row_column_of_table_should_contain($firstcell, $column, $table, $value);
1094                 }
1095             }
1096         }
1097     }
1099     /**
1100      * Checks that the provided value exist in table.
1101      * More info in http://docs.moodle.org/dev/Acceptance_testing#Providing_values_to_steps.
1102      *
1103      * @Then /^the following should not exist in the "(?P<table_string>[^"]*)" table:$/
1104      * @throws ExpectationException
1105      * @param string $table name of table
1106      * @param TableNode $data table with first row as header and following values
1107      *        | Header 1 | Header 2 | Header 3 |
1108      *        | Value 1 | Value 2 | Value 3|
1109      */
1110     public function following_should_not_exist_in_the_table($table, TableNode $data) {
1111         $datahash = $data->getHash();
1113         foreach ($datahash as $value) {
1114             $row = array_shift($value);
1115             foreach ($value as $column => $value) {
1116                 try {
1117                     $this->row_column_of_table_should_contain($row, $column, $table, $value);
1118                     // Throw exception if found.
1119                     throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' .
1120                         $row . '"  row for table "' . $table . '"', $this->getSession()
1121                     );
1122                 } catch (ElementNotFoundException $e) {
1123                     // Table row/column doesn't contain this value. Nothing to do.
1124                     continue;
1125                 }
1126             }
1127         }
1128     }
1130     /**
1131      * Given the text of a link, download the linked file and return the contents.
1132      *
1133      * This is a helper method used by {@link following_should_download_bytes()}
1134      * and {@link following_should_download_between_and_bytes()}
1135      *
1136      * @param string $link the text of the link.
1137      * @return string the content of the downloaded file.
1138      */
1139     protected function download_file_from_link($link) {
1140         // Find the link.
1141         $linknode = $this->find_link($link);
1142         $this->ensure_node_is_visible($linknode);
1144         // Get the href and check it.
1145         $url = $linknode->getAttribute('href');
1146         if (!$url) {
1147             throw new ExpectationException('Download link does not have href attribute',
1148                     $this->getSession());
1149         }
1150         if (!preg_match('~^https?://~', $url)) {
1151             throw new ExpectationException('Download link not an absolute URL: ' . $url,
1152                     $this->getSession());
1153         }
1155         // Download the URL and check the size.
1156         $session = $this->getSession()->getCookie('MoodleSession');
1157         return download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
1158     }
1160     /**
1161      * Downloads the file from a link on the page and checks the size.
1162      *
1163      * Only works if the link has an href attribute. Javascript downloads are
1164      * not supported. Currently, the href must be an absolute URL.
1165      *
1166      * @Then /^following "(?P<link_string>[^"]*)" should download "(?P<expected_bytes>\d+)" bytes$/
1167      * @throws ExpectationException
1168      * @param string $link the text of the link.
1169      * @param number $expectedsize the expected file size in bytes.
1170      */
1171     public function following_should_download_bytes($link, $expectedsize) {
1172         $result = $this->download_file_from_link($link);
1173         $actualsize = (int)strlen($result);
1174         if ($actualsize !== (int)$expectedsize) {
1175             throw new ExpectationException('Downloaded data was ' . $actualsize .
1176                     ' bytes, expecting ' . $expectedsize, $this->getSession());
1177         }
1178     }
1180     /**
1181      * Downloads the file from a link on the page and checks the size is in a given range.
1182      *
1183      * Only works if the link has an href attribute. Javascript downloads are
1184      * not supported. Currently, the href must be an absolute URL.
1185      *
1186      * The range includes the endpoints. That is, a 10 byte file in considered to
1187      * be between "5" and "10" bytes, and between "10" and "20" bytes.
1188      *
1189      * @Then /^following "(?P<link_string>[^"]*)" should download between "(?P<min_bytes>\d+)" and "(?P<max_bytes>\d+)" bytes$/
1190      * @throws ExpectationException
1191      * @param string $link the text of the link.
1192      * @param number $minexpectedsize the minimum expected file size in bytes.
1193      * @param number $maxexpectedsize the maximum expected file size in bytes.
1194      */
1195     public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
1196         // If the minimum is greater than the maximum then swap the values.
1197         if ((int)$minexpectedsize > (int)$maxexpectedsize) {
1198             list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
1199         }
1201         $result = $this->download_file_from_link($link);
1202         $actualsize = (int)strlen($result);
1203         if ($actualsize < $minexpectedsize || $actualsize > $maxexpectedsize) {
1204             throw new ExpectationException('Downloaded data was ' . $actualsize .
1205                     ' bytes, expecting between ' . $minexpectedsize . ' and ' .
1206                     $maxexpectedsize, $this->getSession());
1207         }
1208     }