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