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