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