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