Merge branch 'MDL-67813-master-1' of git://github.com/mihailges/moodle
[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,
eb9ca848 35 Behat\Gherkin\Node\TableNode as TableNode;
786ea937
DM
36
37/**
38 * Cross component steps definitions.
39 *
40 * Basic web application definitions from MinkExtension and
41 * BehatchExtension. Definitions modified according to our needs
42 * when necessary and including only the ones we need to avoid
43 * overlapping and confusion.
44 *
45 * @package core
46 * @category test
47 * @copyright 2012 David MonllaĆ³
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 */
50class behat_general extends behat_base {
51
a92105fd
TH
52 /**
53 * @var string used by {@link switch_to_window()} and
54 * {@link switch_to_the_main_window()} to work-around a Chrome browser issue.
55 */
56 const MAIN_WINDOW_NAME = '__moodle_behat_main_window_name';
57
a109a3ca
TH
58 /**
59 * @var string when we want to check whether or not a new page has loaded,
60 * we first write this unique string into the page. Then later, by checking
61 * whether it is still there, we can tell if a new page has been loaded.
62 */
63 const PAGE_LOAD_DETECTION_STRING = 'new_page_not_loaded_since_behat_started_watching';
64
1b2c35af
AN
65 /**
66 * @var $pageloaddetectionrunning boolean Used to ensure that page load detection was started before a page reload
67 * was checked for.
68 */
9f3a68fe 69 private $pageloaddetectionrunning = false;
1b2c35af 70
786ea937
DM
71 /**
72 * Opens Moodle homepage.
73 *
786ea937
DM
74 * @Given /^I am on homepage$/
75 */
76 public function i_am_on_homepage() {
40923977 77 $this->getSession()->visit($this->locate_path('/'));
786ea937
DM
78 }
79
7a437e36
AA
80 /**
81 * Opens Moodle site homepage.
82 *
83 * @Given /^I am on site homepage$/
84 */
85 public function i_am_on_site_homepage() {
86 $this->getSession()->visit($this->locate_path('/?redirect=0'));
87 }
88
ba5c5083
DW
89 /**
90 * Opens course index page.
91 *
92 * @Given /^I am on course index$/
93 */
94 public function i_am_on_course_index() {
95 $this->getSession()->visit($this->locate_path('/course/index.php'));
96 }
97
18c84063
DM
98 /**
99 * Reloads the current page.
100 *
101 * @Given /^I reload the page$/
102 */
103 public function reload() {
104 $this->getSession()->reload();
105 }
106
d0a9a29b
DM
107 /**
108 * Follows the page redirection. Use this step after any action that shows a message and waits for a redirection
109 *
110 * @Given /^I wait to be redirected$/
111 */
112 public function i_wait_to_be_redirected() {
113
114 // Xpath and processes based on core_renderer::redirect_message(), core_renderer::$metarefreshtag and
115 // moodle_page::$periodicrefreshdelay possible values.
116 if (!$metarefresh = $this->getSession()->getPage()->find('xpath', "//head/descendant::meta[@http-equiv='refresh']")) {
117 // We don't fail the scenario if no redirection with message is found to avoid race condition false failures.
fb99ef1d 118 return true;
d0a9a29b
DM
119 }
120
bda1dea4
DM
121 // Wrapped in try & catch in case the redirection has already been executed.
122 try {
123 $content = $metarefresh->getAttribute('content');
124 } catch (NoSuchElement $e) {
fb99ef1d 125 return true;
39ec8285 126 } catch (StaleElementReference $e) {
fb99ef1d 127 return true;
bda1dea4
DM
128 }
129
130 // Getting the refresh time and the url if present.
d0a9a29b
DM
131 if (strstr($content, 'url') != false) {
132
bda1dea4 133 list($waittime, $url) = explode(';', $content);
d0a9a29b
DM
134
135 // Cleaning the URL value.
136 $url = trim(substr($url, strpos($url, 'http')));
137
138 } else {
139 // Just wait then.
140 $waittime = $content;
141 }
142
143
144 // Wait until the URL change is executed.
145 if ($this->running_javascript()) {
3cf0d01a 146 $this->getSession()->wait($waittime * 1000);
d0a9a29b
DM
147
148 } else if (!empty($url)) {
149 // We redirect directly as we can not wait for an automatic redirection.
150 $this->getSession()->getDriver()->getClient()->request('get', $url);
151
152 } else {
153 // Reload the page if no URL was provided.
154 $this->getSession()->getDriver()->reload();
155 }
156 }
157
e5eff0b6
AA
158 /**
159 * Switches to the specified iframe.
160 *
161 * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" iframe$/
162 * @param string $iframename
163 */
164 public function switch_to_iframe($iframename) {
d1e55a47
DM
165
166 // We spin to give time to the iframe to be loaded.
167 // Using extended timeout as we don't know about which
168 // kind of iframe will be loaded.
169 $this->spin(
170 function($context, $iframename) {
171 $context->getSession()->switchToIFrame($iframename);
172
173 // If no exception we are done.
174 return true;
175 },
176 $iframename,
05a5d547 177 behat_base::get_extended_timeout()
d1e55a47 178 );
e5eff0b6
AA
179 }
180
0d97e610
AA
181 /**
182 * Switches to the iframe containing specified class.
183 *
184 * @Given /^I switch to "(?P<iframe_name_string>(?:[^"]|\\")*)" class iframe$/
185 * @param string $classname
186 */
187 public function switch_to_class_iframe($classname) {
188 // We spin to give time to the iframe to be loaded.
189 // Using extended timeout as we don't know about which
190 // kind of iframe will be loaded.
191 $this->spin(
192 function($context, $classname) {
193 $iframe = $this->find('iframe', $classname);
194 if (!empty($iframe->getAttribute('id'))) {
195 $iframename = $iframe->getAttribute('id');
196 } else {
197 $iframename = $iframe->getAttribute('name');
198 }
199 $context->getSession()->switchToIFrame($iframename);
200
201 // If no exception we are done.
202 return true;
203 },
204 $classname,
205 behat_base::get_extended_timeout()
206 );
207 }
208
e5eff0b6
AA
209 /**
210 * Switches to the main Moodle frame.
211 *
212 * @Given /^I switch to the main frame$/
213 */
214 public function switch_to_the_main_frame() {
215 $this->getSession()->switchToIFrame();
216 }
217
1303eb29
DM
218 /**
219 * Switches to the specified window. Useful when interacting with popup windows.
220 *
221 * @Given /^I switch to "(?P<window_name_string>(?:[^"]|\\")*)" window$/
222 * @param string $windowname
223 */
224 public function switch_to_window($windowname) {
a92105fd
TH
225 // In Behat, some browsers (e.g. Chrome) are unable to switch to a
226 // window without a name, and by default the main browser window does
227 // not have a name. To work-around this, when we switch away from an
228 // unnamed window (presumably the main window) to some other named
229 // window, then we first set the main window name to a conventional
230 // value that we can later use this name to switch back.
42ad096f 231 $this->getSession()->executeScript(
a92105fd
TH
232 'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
233
1303eb29
DM
234 $this->getSession()->switchToWindow($windowname);
235 }
236
237 /**
238 * Switches to the main Moodle window. Useful when you finish interacting with popup windows.
239 *
240 * @Given /^I switch to the main window$/
241 */
242 public function switch_to_the_main_window() {
a92105fd 243 $this->getSession()->switchToWindow(self::MAIN_WINDOW_NAME);
1303eb29
DM
244 }
245
563514b1 246 /**
7daab401 247 * Accepts the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
563514b1
DM
248 * @Given /^I accept the currently displayed dialog$/
249 */
250 public function accept_currently_displayed_alert_dialog() {
251 $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
252 }
253
20dd5a7a
TH
254 /**
255 * Dismisses the currently displayed alert dialog. This step does not work in all the browsers, consider it experimental.
256 * @Given /^I dismiss the currently displayed dialog$/
257 */
258 public function dismiss_currently_displayed_alert_dialog() {
259 $this->getSession()->getDriver()->getWebDriverSession()->dismiss_alert();
260 }
261
786ea937
DM
262 /**
263 * Clicks link with specified id|title|alt|text.
264 *
786ea937 265 * @When /^I follow "(?P<link_string>(?:[^"]|\\")*)"$/
1f9ffbdb 266 * @throws ElementNotFoundException Thrown by behat_base::find
40923977 267 * @param string $link
786ea937
DM
268 */
269 public function click_link($link) {
1f9ffbdb
DM
270
271 $linknode = $this->find_link($link);
d1e55a47 272 $this->ensure_node_is_visible($linknode);
1f9ffbdb 273 $linknode->click();
786ea937
DM
274 }
275
276 /**
277 * Waits X seconds. Required after an action that requires data from an AJAX request.
278 *
279 * @Then /^I wait "(?P<seconds_number>\d+)" seconds$/
280 * @param int $seconds
281 */
282 public function i_wait_seconds($seconds) {
d245f675 283 if ($this->running_javascript()) {
3cf0d01a 284 $this->getSession()->wait($seconds * 1000);
d245f675
AN
285 } else {
286 sleep($seconds);
d0a9a29b 287 }
786ea937
DM
288 }
289
290 /**
291 * Waits until the page is completely loaded. This step is auto-executed after every step.
292 *
293 * @Given /^I wait until the page is ready$/
294 */
295 public function wait_until_the_page_is_ready() {
d0a9a29b 296
eb9ca848 297 // No need to wait if not running JS.
d0a9a29b 298 if (!$this->running_javascript()) {
eb9ca848 299 return;
d0a9a29b
DM
300 }
301
05a5d547 302 $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
d1e55a47
DM
303 }
304
305 /**
306 * Waits until the provided element selector exists in the DOM
307 *
308 * Using the protected method as this method will be usually
309 * called by other methods which are not returning a set of
310 * steps and performs the actions directly, so it would not
311 * be executed if it returns another step.
312
313 * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
314 * @param string $element
315 * @param string $selector
316 * @return void
317 */
318 public function wait_until_exists($element, $selectortype) {
319 $this->ensure_element_exists($element, $selectortype);
320 }
321
322 /**
323 * Waits until the provided element does not exist in the DOM
324 *
325 * Using the protected method as this method will be usually
326 * called by other methods which are not returning a set of
327 * steps and performs the actions directly, so it would not
328 * be executed if it returns another step.
329
330 * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
331 * @param string $element
332 * @param string $selector
333 * @return void
334 */
335 public function wait_until_does_not_exists($element, $selectortype) {
336 $this->ensure_element_does_not_exist($element, $selectortype);
786ea937
DM
337 }
338
339 /**
40923977 340 * Generic mouse over action. Mouse over a element of the specified type.
786ea937 341 *
40923977
DM
342 * @When /^I hover "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
343 * @param string $element Element we look for
344 * @param string $selectortype The type of what we look for
786ea937 345 */
40923977 346 public function i_hover($element, $selectortype) {
1f9ffbdb 347
40923977
DM
348 // Gets the node based on the requested selector type and locator.
349 $node = $this->get_selected_node($selectortype, $element);
786ea937
DM
350 $node->mouseOver();
351 }
352
40923977
DM
353 /**
354 * Generic click action. Click on the element of the specified type.
355 *
356 * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
357 * @param string $element Element we look for
358 * @param string $selectortype The type of what we look for
359 */
360 public function i_click_on($element, $selectortype) {
361
362 // Gets the node based on the requested selector type and locator.
363 $node = $this->get_selected_node($selectortype, $element);
d1e55a47 364 $this->ensure_node_is_visible($node);
40923977
DM
365 $node->click();
366 }
367
d7a0b721
RT
368 /**
369 * Sets the focus and takes away the focus from an element, generating blur JS event.
370 *
371 * @When /^I take focus off "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
372 * @param string $element Element we look for
373 * @param string $selectortype The type of what we look for
374 */
375 public function i_take_focus_off_field($element, $selectortype) {
376 if (!$this->running_javascript()) {
377 throw new ExpectationException('Can\'t take focus off from "' . $element . '" in non-js mode', $this->getSession());
378 }
379 // Gets the node based on the requested selector type and locator.
380 $node = $this->get_selected_node($selectortype, $element);
381 $this->ensure_node_is_visible($node);
382
383 // Ensure element is focused before taking it off.
384 $node->focus();
385 $node->blur();
386 }
387
b28b374f
DM
388 /**
389 * Clicks the specified element and confirms the expected dialogue.
390 *
391 * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" confirming the dialogue$/
392 * @throws ElementNotFoundException Thrown by behat_base::find
e04cf8d8
DP
393 * @param string $element Element we look for
394 * @param string $selectortype The type of what we look for
b28b374f
DM
395 */
396 public function i_click_on_confirming_the_dialogue($element, $selectortype) {
397 $this->i_click_on($element, $selectortype);
398 $this->accept_currently_displayed_alert_dialog();
399 }
400
20dd5a7a
TH
401 /**
402 * Clicks the specified element and dismissing the expected dialogue.
403 *
404 * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" dismissing the dialogue$/
405 * @throws ElementNotFoundException Thrown by behat_base::find
e04cf8d8
DP
406 * @param string $element Element we look for
407 * @param string $selectortype The type of what we look for
20dd5a7a
TH
408 */
409 public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
410 $this->i_click_on($element, $selectortype);
411 $this->dismiss_currently_displayed_alert_dialog();
412 }
413
072f67fc
DM
414 /**
415 * Click on the element of the specified type which is located inside the second element.
416 *
417 * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
418 * @param string $element Element we look for
419 * @param string $selectortype The type of what we look for
420 * @param string $nodeelement Element we look in
421 * @param string $nodeselectortype The type of selector where we look in
422 */
423 public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {
424
425 $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
d1e55a47 426 $this->ensure_node_is_visible($node);
072f67fc
DM
427 $node->click();
428 }
429
563514b1 430 /**
7daab401 431 * Drags and drops the specified element to the specified container. This step does not work in all the browsers, consider it experimental.
563514b1
DM
432 *
433 * The steps definitions calling this step as part of them should
434 * manage the wait times by themselves as the times and when the
435 * waits should be done depends on what is being dragged & dropper.
436 *
437 * @Given /^I drag "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector1_string>(?:[^"]|\\")*)" and I drop it in "(?P<container_element_string>(?:[^"]|\\")*)" "(?P<selector2_string>(?:[^"]|\\")*)"$/
438 * @param string $element
439 * @param string $selectortype
440 * @param string $containerelement
441 * @param string $containerselectortype
442 */
d10ed4d2
AN
443 public function i_drag_and_i_drop_it_in($source, $sourcetype, $target, $targettype) {
444 if (!$this->running_javascript()) {
445 throw new DriverException('Drag and drop steps require javascript');
446 }
563514b1 447
d10ed4d2
AN
448 $source = $this->find($sourcetype, $source);
449 $target = $this->find($targettype, $target);
563514b1 450
d10ed4d2
AN
451 if (!$source->isVisible()) {
452 throw new ExpectationException("'{$source}' '{$sourcetype}' is not visible", $this->getSession());
031152cc 453 }
d10ed4d2
AN
454 if (!$target->isVisible()) {
455 throw new ExpectationException("'{$target}' '{$targettype}' is not visible", $this->getSession());
031152cc
DW
456 }
457
d10ed4d2 458 $this->getSession()->getDriver()->dragTo($source->getXpath(), $target->getXpath());
563514b1
DM
459 }
460
63950e4d
DM
461 /**
462 * Checks, that the specified element is visible. Only available in tests using Javascript.
463 *
464 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should be visible$/
465 * @throws ElementNotFoundException
466 * @throws ExpectationException
467 * @throws DriverException
468 * @param string $element
469 * @param string $selectortype
470 * @return void
471 */
472 public function should_be_visible($element, $selectortype) {
473
474 if (!$this->running_javascript()) {
475 throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
476 }
477
478 $node = $this->get_selected_node($selectortype, $element);
479 if (!$node->isVisible()) {
480 throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is not visible', $this->getSession());
481 }
482 }
483
484 /**
74c78e74 485 * Checks, that the existing element is not visible. Only available in tests using Javascript.
63950e4d 486 *
74c78e74
DM
487 * As a "not" method, it's performance could not be good, but in this
488 * case the performance is good because the element must exist,
489 * otherwise there would be a ElementNotFoundException, also here we are
490 * not spinning until the element is visible.
c1faf86b 491 *
63950e4d
DM
492 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should not be visible$/
493 * @throws ElementNotFoundException
494 * @throws ExpectationException
495 * @param string $element
496 * @param string $selectortype
497 * @return void
498 */
499 public function should_not_be_visible($element, $selectortype) {
500
501 try {
502 $this->should_be_visible($element, $selectortype);
63950e4d
DM
503 } catch (ExpectationException $e) {
504 // All as expected.
ca0ceacd 505 return;
63950e4d 506 }
ca0ceacd 507 throw new ExpectationException('"' . $element . '" "' . $selectortype . '" is visible', $this->getSession());
63950e4d
DM
508 }
509
510 /**
511 * Checks, that the specified element is visible inside the specified container. Only available in tests using Javascript.
512 *
513 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should be visible$/
514 * @throws ElementNotFoundException
515 * @throws DriverException
516 * @throws ExpectationException
517 * @param string $element Element we look for
518 * @param string $selectortype The type of what we look for
519 * @param string $nodeelement Element we look in
520 * @param string $nodeselectortype The type of selector where we look in
521 */
522 public function in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
523
524 if (!$this->running_javascript()) {
525 throw new DriverException('Visible checks are disabled in scenarios without Javascript support');
526 }
527
528 $node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
529 if (!$node->isVisible()) {
530 throw new ExpectationException(
531 '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is not visible',
532 $this->getSession()
533 );
534 }
535 }
536
537 /**
74c78e74
DM
538 * Checks, that the existing element is not visible inside the existing container. Only available in tests using Javascript.
539 *
540 * As a "not" method, it's performance could not be good, but in this
541 * case the performance is good because the element must exist,
542 * otherwise there would be a ElementNotFoundException, also here we are
543 * not spinning until the element is visible.
63950e4d
DM
544 *
545 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" should not be visible$/
546 * @throws ElementNotFoundException
547 * @throws ExpectationException
548 * @param string $element Element we look for
549 * @param string $selectortype The type of what we look for
550 * @param string $nodeelement Element we look in
551 * @param string $nodeselectortype The type of selector where we look in
552 */
553 public function in_the_should_not_be_visible($element, $selectortype, $nodeelement, $nodeselectortype) {
554
555 try {
556 $this->in_the_should_be_visible($element, $selectortype, $nodeelement, $nodeselectortype);
63950e4d
DM
557 } catch (ExpectationException $e) {
558 // All as expected.
ca0ceacd 559 return;
63950e4d 560 }
ca0ceacd
TH
561 throw new ExpectationException(
562 '"' . $element . '" "' . $selectortype . '" in the "' . $nodeelement . '" "' . $nodeselectortype . '" is visible',
563 $this->getSession()
564 );
63950e4d
DM
565 }
566
786ea937 567 /**
e9af3ed3 568 * Checks, that page contains specified text. It also checks if the text is visible when running Javascript tests.
786ea937 569 *
786ea937 570 * @Then /^I should see "(?P<text_string>(?:[^"]|\\")*)"$/
9a1f4922 571 * @throws ExpectationException
40923977 572 * @param string $text
786ea937
DM
573 */
574 public function assert_page_contains_text($text) {
9a1f4922 575
e9af3ed3
DM
576 // Looking for all the matching nodes without any other descendant matching the
577 // same xpath (we are using contains(., ....).
921faad9 578 $xpathliteral = behat_context_helper::escape($text);
e9af3ed3
DM
579 $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
580 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
9a1f4922 581
9a1f4922 582 try {
e9af3ed3 583 $nodes = $this->find_all('xpath', $xpath);
c1faf86b
DM
584 } catch (ElementNotFoundException $e) {
585 throw new ExpectationException('"' . $text . '" text was not found in the page', $this->getSession());
586 }
e9af3ed3 587
c1faf86b
DM
588 // If we are not running javascript we have enough with the
589 // element existing as we can't check if it is visible.
590 if (!$this->running_javascript()) {
591 return;
592 }
593
594 // We spin as we don't have enough checking that the element is there, we
74c78e74
DM
595 // should also ensure that the element is visible. Using microsleep as this
596 // is a repeated step and global performance is important.
c1faf86b
DM
597 $this->spin(
598 function($context, $args) {
599
600 foreach ($args['nodes'] as $node) {
e9af3ed3 601 if ($node->isVisible()) {
c1faf86b 602 return true;
e9af3ed3
DM
603 }
604 }
605
c1faf86b
DM
606 // If non of the nodes is visible we loop again.
607 throw new ExpectationException('"' . $args['text'] . '" text was found but was not visible', $context->getSession());
608 },
74c78e74
DM
609 array('nodes' => $nodes, 'text' => $text),
610 false,
611 false,
612 true
c1faf86b 613 );
e9af3ed3 614
786ea937
DM
615 }
616
617 /**
e9af3ed3 618 * Checks, that page doesn't contain specified text. When running Javascript tests it also considers that texts may be hidden.
786ea937 619 *
786ea937 620 * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)"$/
9a1f4922 621 * @throws ExpectationException
40923977 622 * @param string $text
786ea937
DM
623 */
624 public function assert_page_not_contains_text($text) {
9a1f4922 625
c1faf86b
DM
626 // Looking for all the matching nodes without any other descendant matching the
627 // same xpath (we are using contains(., ....).
921faad9 628 $xpathliteral = behat_context_helper::escape($text);
c1faf86b
DM
629 $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
630 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
631
632 // We should wait a while to ensure that the page is not still loading elements.
05a5d547 633 // Waiting less than self::get_timeout() as we already waited for the DOM to be ready and
74c78e74 634 // all JS to be executed.
9a1f4922 635 try {
05a5d547 636 $nodes = $this->find_all('xpath', $xpath, false, false, self::get_reduced_timeout());
c1faf86b
DM
637 } catch (ElementNotFoundException $e) {
638 // All ok.
5458ab3e 639 return;
9a1f4922 640 }
5458ab3e 641
c1faf86b
DM
642 // If we are not running javascript we have enough with the
643 // element existing as we can't check if it is hidden.
644 if (!$this->running_javascript()) {
645 throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
646 }
647
648 // If the element is there we should be sure that it is not visible.
649 $this->spin(
650 function($context, $args) {
651
652 foreach ($args['nodes'] as $node) {
ade50a11
RT
653 // If element is removed from dom, then just exit.
654 try {
655 // If element is visible then throw exception, so we keep spinning.
656 if ($node->isVisible()) {
657 throw new ExpectationException('"' . $args['text'] . '" text was found in the page',
658 $context->getSession());
659 }
660 } catch (WebDriver\Exception\NoSuchElement $e) {
661 // Do nothing just return, as element is no more on page.
662 return true;
f5459099
MN
663 } catch (ElementNotFoundException $e) {
664 // Do nothing just return, as element is no more on page.
665 return true;
c1faf86b
DM
666 }
667 }
668
669 // If non of the found nodes is visible we consider that the text is not visible.
670 return true;
671 },
74c78e74 672 array('nodes' => $nodes, 'text' => $text),
05a5d547 673 behat_base::get_reduced_timeout(),
74c78e74
DM
674 false,
675 true
c1faf86b 676 );
786ea937
DM
677 }
678
679 /**
e9af3ed3 680 * Checks, that the specified element contains the specified text. When running Javascript tests it also considers that texts may be hidden.
786ea937 681 *
40923977 682 * @Then /^I should 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_contains_text($text, $element, $selectortype) {
690
5458ab3e
DM
691 // Getting the container where the text should be found.
692 $container = $this->get_selected_node($selectortype, $element);
693
e9af3ed3
DM
694 // Looking for all the matching nodes without any other descendant matching the
695 // same xpath (we are using contains(., ....).
921faad9 696 $xpathliteral = behat_context_helper::escape($text);
e9af3ed3
DM
697 $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
698 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
5458ab3e
DM
699
700 // Wait until it finds the text inside the container, otherwise custom exception.
701 try {
e9af3ed3 702 $nodes = $this->find_all('xpath', $xpath, false, $container);
c1faf86b
DM
703 } catch (ElementNotFoundException $e) {
704 throw new ExpectationException('"' . $text . '" text was not found in the "' . $element . '" element', $this->getSession());
705 }
e9af3ed3 706
c1faf86b
DM
707 // If we are not running javascript we have enough with the
708 // element existing as we can't check if it is visible.
709 if (!$this->running_javascript()) {
710 return;
711 }
712
74c78e74
DM
713 // We also check the element visibility when running JS tests. Using microsleep as this
714 // is a repeated step and global performance is important.
c1faf86b
DM
715 $this->spin(
716 function($context, $args) {
717
718 foreach ($args['nodes'] as $node) {
e9af3ed3 719 if ($node->isVisible()) {
c1faf86b 720 return true;
e9af3ed3
DM
721 }
722 }
723
c1faf86b
DM
724 throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element but was not visible', $context->getSession());
725 },
74c78e74
DM
726 array('nodes' => $nodes, 'text' => $text, 'element' => $element),
727 false,
728 false,
729 true
c1faf86b 730 );
786ea937
DM
731 }
732
733 /**
e9af3ed3 734 * Checks, that the specified element does not contain the specified text. When running Javascript tests it also considers that texts may be hidden.
786ea937 735 *
40923977 736 * @Then /^I should not see "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
5458ab3e
DM
737 * @throws ElementNotFoundException
738 * @throws ExpectationException
40923977
DM
739 * @param string $text
740 * @param string $element Element we look in.
741 * @param string $selectortype The type of element where we are looking in.
786ea937 742 */
40923977
DM
743 public function assert_element_not_contains_text($text, $element, $selectortype) {
744
c1faf86b
DM
745 // Getting the container where the text should be found.
746 $container = $this->get_selected_node($selectortype, $element);
747
748 // Looking for all the matching nodes without any other descendant matching the
749 // same xpath (we are using contains(., ....).
921faad9 750 $xpathliteral = behat_context_helper::escape($text);
c1faf86b
DM
751 $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
752 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
753
754 // We should wait a while to ensure that the page is not still loading elements.
755 // Giving preference to the reliability of the results rather than to the performance.
5458ab3e 756 try {
05a5d547 757 $nodes = $this->find_all('xpath', $xpath, false, $container, self::get_reduced_timeout());
c1faf86b
DM
758 } catch (ElementNotFoundException $e) {
759 // All ok.
5458ab3e
DM
760 return;
761 }
762
c1faf86b
DM
763 // If we are not running javascript we have enough with the
764 // element not being found as we can't check if it is visible.
765 if (!$this->running_javascript()) {
766 throw new ExpectationException('"' . $text . '" text was found in the "' . $element . '" element', $this->getSession());
767 }
768
769 // We need to ensure all the found nodes are hidden.
770 $this->spin(
771 function($context, $args) {
772
773 foreach ($args['nodes'] as $node) {
774 if ($node->isVisible()) {
775 throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element', $context->getSession());
776 }
777 }
778
779 // If all the found nodes are hidden we are happy.
780 return true;
781 },
74c78e74 782 array('nodes' => $nodes, 'text' => $text, 'element' => $element),
05a5d547 783 behat_base::get_reduced_timeout(),
74c78e74
DM
784 false,
785 true
c1faf86b 786 );
786ea937
DM
787 }
788
60054942
DM
789 /**
790 * Checks, that the first specified element appears before the second one.
791 *
17a7dc9d 792 * @Then :preelement :preselectortype should appear before :postelement :postselectortype
7d6d2329 793 * @Then :preelement :preselectortype should appear before :postelement :postselectortype in the :containerelement :containerselectortype
60054942
DM
794 * @throws ExpectationException
795 * @param string $preelement The locator of the preceding element
f8038edd 796 * @param string $preselectortype The selector type of the preceding element
60054942
DM
797 * @param string $postelement The locator of the latest element
798 * @param string $postselectortype The selector type of the latest element
7d6d2329
AN
799 * @param string $containerelement
800 * @param string $containerselectortype
60054942 801 */
17a7dc9d
AN
802 public function should_appear_before(
803 string $preelement,
804 string $preselectortype,
805 string $postelement,
7d6d2329
AN
806 string $postselectortype,
807 ?string $containerelement = null,
808 ?string $containerselectortype = null
17a7dc9d 809 ) {
f8038edd 810 $msg = "'{$preelement}' '{$preselectortype}' does not appear before '{$postelement}' '{$postselectortype}'";
17a7dc9d 811 $this->check_element_order(
7d6d2329
AN
812 $containerelement,
813 $containerselectortype,
17a7dc9d
AN
814 $preelement,
815 $preselectortype,
816 $postelement,
817 $postselectortype,
818 $msg
819 );
60054942
DM
820 }
821
822 /**
823 * Checks, that the first specified element appears after the second one.
824 *
17a7dc9d 825 * @Then :postelement :postselectortype should appear after :preelement :preselectortype
7d6d2329 826 * @Then :postelement :postselectortype should appear after :preelement :preselectortype in the :containerelement :containerselectortype
60054942
DM
827 * @throws ExpectationException
828 * @param string $postelement The locator of the latest element
829 * @param string $postselectortype The selector type of the latest element
830 * @param string $preelement The locator of the preceding element
f8038edd 831 * @param string $preselectortype The selector type of the preceding element
7d6d2329
AN
832 * @param string $containerelement
833 * @param string $containerselectortype
60054942 834 */
17a7dc9d
AN
835 public function should_appear_after(
836 string $postelement,
837 string $postselectortype,
838 string $preelement,
7d6d2329
AN
839 string $preselectortype,
840 ?string $containerelement = null,
841 ?string $containerselectortype = null
17a7dc9d
AN
842 ) {
843 $msg = "'{$postelement}' '{$postselectortype}' does not appear after '{$preelement}' '{$preselectortype}'";
844 $this->check_element_order(
7d6d2329
AN
845 $containerelement,
846 $containerselectortype,
17a7dc9d
AN
847 $preelement,
848 $preselectortype,
849 $postelement,
850 $postselectortype,
851 $msg
852 );
5298a6bc 853 }
60054942 854
5298a6bc 855 /**
856 * Shared code to check whether an element is before or after another one.
857 *
7d6d2329
AN
858 * @param string $containerelement
859 * @param string $containerselectortype
5298a6bc 860 * @param string $preelement The locator of the preceding element
861 * @param string $preselectortype The locator of the preceding element
862 * @param string $postelement The locator of the following element
863 * @param string $postselectortype The selector type of the following element
864 * @param string $msg Message to output if this fails
865 */
17a7dc9d 866 protected function check_element_order(
7d6d2329
AN
867 ?string $containerelement,
868 ?string $containerselectortype,
17a7dc9d
AN
869 string $preelement,
870 string $preselectortype,
871 string $postelement,
872 string $postselectortype,
873 string $msg
874 ) {
7d6d2329
AN
875 $containernode = false;
876 if ($containerselectortype && $containerelement) {
877 // Get the container node.
878 $containernode = $this->get_selected_node($containerselectortype, $containerelement);
879 $msg .= " in the '{$containerelement}' '{$containerselectortype}'";
880 }
881
60054942 882 list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
5298a6bc 883 list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
60054942 884
460a0a74
AN
885 $newlines = [
886 "\r\n",
887 "\r",
888 "\n",
889 ];
890 $prexpath = str_replace($newlines, ' ', $this->find($preselector, $prelocator, false, $containernode)->getXpath());
891 $postxpath = str_replace($newlines, ' ', $this->find($postselector, $postlocator, false, $containernode)->getXpath());
5298a6bc 892
17a7dc9d
AN
893 if ($this->running_javascript()) {
894 // The xpath to do this was running really slowly on certain Chrome versions so we are using
895 // this DOM method instead.
896 $js = <<<EOF
897(function() {
a6df6800
AN
898 var a = document.evaluate("{$prexpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
899 var b = document.evaluate("{$postxpath}", document, null, XPathResult.ANY_TYPE, null).iterateNext();
17a7dc9d
AN
900 return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING;
901})()
902EOF;
903 $ok = $this->getSession()->getDriver()->evaluateScript($js);
904 } else {
905
906 // Using following xpath axe to find it.
907 $xpath = "{$prexpath}/following::*[contains(., {$postxpath})]";
908 $ok = $this->getSession()->getDriver()->find($xpath);
909 }
60054942 910
5298a6bc 911 if (!$ok) {
60054942
DM
912 throw new ExpectationException($msg, $this->getSession());
913 }
914 }
915
786ea937 916 /**
40923977 917 * Checks, that element of specified type is disabled.
786ea937 918 *
40923977 919 * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be disabled$/
1f9ffbdb 920 * @throws ExpectationException Thrown by behat_base::find
40923977
DM
921 * @param string $element Element we look in
922 * @param string $selectortype The type of element where we are looking in.
786ea937 923 */
40923977 924 public function the_element_should_be_disabled($element, $selectortype) {
786ea937 925
40923977
DM
926 // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
927 $node = $this->get_selected_node($selectortype, $element);
786ea937
DM
928
929 if (!$node->hasAttribute('disabled')) {
930 throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
931 }
932 }
933
934 /**
40923977 935 * Checks, that element of specified type is enabled.
786ea937 936 *
40923977 937 * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be enabled$/
1f9ffbdb 938 * @throws ExpectationException Thrown by behat_base::find
40923977
DM
939 * @param string $element Element we look on
940 * @param string $selectortype The type of where we look
786ea937 941 */
40923977 942 public function the_element_should_be_enabled($element, $selectortype) {
1f9ffbdb 943
40923977
DM
944 // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
945 $node = $this->get_selected_node($selectortype, $element);
786ea937
DM
946
947 if ($node->hasAttribute('disabled')) {
948 throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
949 }
950 }
951
a2d3e3b6
MN
952 /**
953 * Checks the provided element and selector type are readonly on the current page.
954 *
955 * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be readonly$/
956 * @throws ExpectationException Thrown by behat_base::find
957 * @param string $element Element we look in
958 * @param string $selectortype The type of element where we are looking in.
959 */
960 public function the_element_should_be_readonly($element, $selectortype) {
961 // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
962 $node = $this->get_selected_node($selectortype, $element);
963
964 if (!$node->hasAttribute('readonly')) {
965 throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
966 }
967 }
968
969 /**
970 * Checks the provided element and selector type are not readonly on the current page.
971 *
972 * @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not be readonly$/
973 * @throws ExpectationException Thrown by behat_base::find
974 * @param string $element Element we look in
975 * @param string $selectortype The type of element where we are looking in.
976 */
977 public function the_element_should_not_be_readonly($element, $selectortype) {
978 // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
979 $node = $this->get_selected_node($selectortype, $element);
980
981 if ($node->hasAttribute('readonly')) {
982 throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
983 }
984 }
985
ca4f33a7 986 /**
62eb5c46
EL
987 * Checks the provided element and selector type exists in the current page.
988 *
989 * This step is for advanced users, use it if you don't find anything else suitable for what you need.
ca4f33a7 990 *
c51c3b55 991 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist$/
ca4f33a7
DM
992 * @throws ElementNotFoundException Thrown by behat_base::find
993 * @param string $element The locator of the specified selector
994 * @param string $selectortype The selector type
995 */
c51c3b55 996 public function should_exist($element, $selectortype) {
ca4f33a7 997 // Will throw an ElementNotFoundException if it does not exist.
d10ed4d2 998 $this->find($selectortype, $element);
ca4f33a7
DM
999 }
1000
1001 /**
62eb5c46
EL
1002 * Checks that the provided element and selector type not exists in the current page.
1003 *
1004 * This step is for advanced users, use it if you don't find anything else suitable for what you need.
ca4f33a7 1005 *
c51c3b55 1006 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist$/
ca4f33a7
DM
1007 * @throws ExpectationException
1008 * @param string $element The locator of the specified selector
1009 * @param string $selectortype The selector type
1010 */
c51c3b55 1011 public function should_not_exist($element, $selectortype) {
d10ed4d2
AN
1012 // Will throw an ElementNotFoundException if it does not exist, but, actually it should not exist, so we try &
1013 // catch it.
ca4f33a7 1014 try {
74c78e74
DM
1015 // The exception does not really matter as we will catch it and will never "explode".
1016 $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $element);
1017
d10ed4d2
AN
1018 // Using the spin method as we want a reduced timeout but there is no need for a 0.1 seconds interval
1019 // because in the optimistic case we will timeout.
74c78e74 1020 // If all goes good it will throw an ElementNotFoundExceptionn that we will catch.
9b1e5805 1021 return $this->find($selectortype, $element, $exception, false, behat_base::get_reduced_timeout());
62eb5c46 1022 } catch (ElementNotFoundException $e) {
d10ed4d2 1023 // We expect the element to not be found.
ca4f33a7
DM
1024 return;
1025 }
ca0ceacd 1026
d10ed4d2
AN
1027 // The element was found and should not have been. Throw an exception.
1028 throw new ExpectationException("The '{$element}' '{$selectortype}' exists in the current page", $this->getSession());
ca4f33a7
DM
1029 }
1030
066ef320
JM
1031 /**
1032 * This step triggers cron like a user would do going to admin/cron.php.
1033 *
1034 * @Given /^I trigger cron$/
1035 */
1036 public function i_trigger_cron() {
1037 $this->getSession()->visit($this->locate_path('/admin/cron.php'));
1038 }
1039
a4ce565f 1040 /**
1041 * Runs a scheduled task immediately, given full class name.
1042 *
1043 * This is faster and more reliable than running cron (running cron won't
1044 * work more than once in the same test, for instance). However it is
1045 * a little less 'realistic'.
1046 *
1047 * While the task is running, we suppress mtrace output because it makes
1048 * the Behat result look ugly.
1049 *
1050 * Note: Most of the code relating to running a task is based on
1051 * admin/tool/task/cli/schedule_task.php.
1052 *
1053 * @Given /^I run the scheduled task "(?P<task_name>[^"]+)"$/
1054 * @param string $taskname Name of task e.g. 'mod_whatever\task\do_something'
1055 */
1056 public function i_run_the_scheduled_task($taskname) {
df95c479
AN
1057 global $CFG;
1058 require_once("{$CFG->libdir}/cronlib.php");
1059
a4ce565f 1060 $task = \core\task\manager::get_scheduled_task($taskname);
1061 if (!$task) {
1062 throw new DriverException('The "' . $taskname . '" scheduled task does not exist');
1063 }
1064
1065 // Do setup for cron task.
1066 raise_memory_limit(MEMORY_EXTRA);
1067 cron_setup_user();
1068
1069 // Get lock.
1070 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
1071 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
1072 throw new DriverException('Unable to obtain core_cron lock for scheduled task');
1073 }
1074 if (!$lock = $cronlockfactory->get_lock('\\' . get_class($task), 10)) {
1075 $cronlock->release();
1076 throw new DriverException('Unable to obtain task lock for scheduled task');
1077 }
1078 $task->set_lock($lock);
1079 if (!$task->is_blocking()) {
1080 $cronlock->release();
1081 } else {
1082 $task->set_cron_lock($cronlock);
1083 }
1084
1085 try {
df95c479
AN
1086 // Prepare the renderer.
1087 cron_prepare_core_renderer();
1088
a4ce565f 1089 // Discard task output as not appropriate for Behat output!
1090 ob_start();
1091 $task->execute();
1092 ob_end_clean();
1093
df95c479
AN
1094 // Restore the previous renderer.
1095 cron_prepare_core_renderer(true);
1096
a4ce565f 1097 // Mark task complete.
1098 \core\task\manager::scheduled_task_complete($task);
1099 } catch (Exception $e) {
df95c479
AN
1100 // Restore the previous renderer.
1101 cron_prepare_core_renderer(true);
1102
a4ce565f 1103 // Mark task failed and throw exception.
1104 \core\task\manager::scheduled_task_failed($task);
df95c479 1105
a4ce565f 1106 throw new DriverException('The "' . $taskname . '" scheduled task failed', 0, $e);
1107 }
1108 }
1109
ff4230d8
JD
1110 /**
1111 * Runs all ad-hoc tasks in the queue.
1112 *
1113 * This is faster and more reliable than running cron (running cron won't
1114 * work more than once in the same test, for instance). However it is
1115 * a little less 'realistic'.
1116 *
1117 * While the task is running, we suppress mtrace output because it makes
1118 * the Behat result look ugly.
1119 *
1120 * @Given /^I run all adhoc tasks$/
1121 * @throws DriverException
1122 */
1123 public function i_run_all_adhoc_tasks() {
df95c479
AN
1124 global $CFG, $DB;
1125 require_once("{$CFG->libdir}/cronlib.php");
1126
ff4230d8
JD
1127 // Do setup for cron task.
1128 cron_setup_user();
1129
df95c479
AN
1130 // Discard task output as not appropriate for Behat output!
1131 ob_start();
1132
1133 // Run all tasks which have a scheduled runtime of before now.
1134 $timenow = time();
1135
1136 while (!\core\task\manager::static_caches_cleared_since($timenow) &&
1137 $task = \core\task\manager::get_next_adhoc_task($timenow)) {
1138 // Clean the output buffer between tasks.
1139 ob_clean();
1140
1141 // Run the task.
1142 cron_run_inner_adhoc_task($task);
1143
1144 // Check whether the task record still exists.
1145 // If a task was successful it will be removed.
1146 // If it failed then it will still exist.
1147 if ($DB->record_exists('task_adhoc', ['id' => $task->get_id()])) {
1148 // End ouptut buffering and flush the current buffer.
1149 // This should be from just the current task.
1150 ob_end_flush();
1151
1152 throw new DriverException('An adhoc task failed', 0);
ff4230d8
JD
1153 }
1154 }
1155 ob_end_clean();
1156 }
1157
a2d3e3b6
MN
1158 /**
1159 * Checks that an element and selector type exists in another element and selector type on the current page.
1160 *
1161 * This step is for advanced users, use it if you don't find anything else suitable for what you need.
1162 *
1163 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
1164 * @throws ElementNotFoundException Thrown by behat_base::find
1165 * @param string $element The locator of the specified selector
1166 * @param string $selectortype The selector type
1167 * @param string $containerelement The container selector type
1168 * @param string $containerselectortype The container locator
1169 */
1170 public function should_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
1171 // Get the container node.
d10ed4d2 1172 $containernode = $this->find($containerselectortype, $containerelement);
a2d3e3b6
MN
1173
1174 // Specific exception giving info about where can't we find the element.
d10ed4d2 1175 $locatorexceptionmsg = "{$element} in the {$containerelement} {$containerselectortype}";
a2d3e3b6
MN
1176 $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
1177
1178 // Looks for the requested node inside the container node.
d10ed4d2 1179 $this->find($selectortype, $element, $exception, $containernode);
a2d3e3b6
MN
1180 }
1181
1182 /**
1183 * Checks that an element and selector type does not exist in another element and selector type on the current page.
1184 *
1185 * This step is for advanced users, use it if you don't find anything else suitable for what you need.
1186 *
1187 * @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in the "(?P<element2_string>(?:[^"]|\\")*)" "(?P<selector2_string>[^"]*)"$/
1188 * @throws ExpectationException
1189 * @param string $element The locator of the specified selector
1190 * @param string $selectortype The selector type
1191 * @param string $containerelement The container selector type
1192 * @param string $containerselectortype The container locator
1193 */
1194 public function should_not_exist_in_the($element, $selectortype, $containerelement, $containerselectortype) {
d10ed4d2
AN
1195 // Get the container node.
1196 $containernode = $this->find($containerselectortype, $containerelement);
74c78e74 1197
d10ed4d2
AN
1198 // Will throw an ElementNotFoundException if it does not exist, but, actually it should not exist, so we try &
1199 // catch it.
a2d3e3b6 1200 try {
d10ed4d2
AN
1201 // Looks for the requested node inside the container node.
1202 $this->find($selectortype, $element, false, $containernode, behat_base::get_reduced_timeout());
a2d3e3b6 1203 } catch (ElementNotFoundException $e) {
d10ed4d2 1204 // We expect the element to not be found.
a2d3e3b6
MN
1205 return;
1206 }
d10ed4d2
AN
1207
1208 // The element was found and should not have been. Throw an exception.
1209 throw new ExpectationException(
1210 "The '{$element}' '{$selectortype}' exists in the '{$containerelement}' '{$containerselectortype}'",
1211 $this->getSession()
1212 );
a2d3e3b6 1213 }
3b0b5e57
RT
1214
1215 /**
1216 * Change browser window size small: 640x480, medium: 1024x768, large: 2560x1600, custom: widthxheight
1217 *
1218 * Example: I change window size to "small" or I change window size to "1024x768"
5a14c4d9 1219 * or I change viewport size to "800x600". The viewport option is useful to guarantee that the
1220 * browser window has same viewport size even when you run Behat on multiple operating systems.
3b0b5e57
RT
1221 *
1222 * @throws ExpectationException
5a14c4d9 1223 * @Then /^I change (window|viewport) size to "(small|medium|large|\d+x\d+)"$/
6fd975cc 1224 * @Then /^I change the (window|viewport) size to "(small|medium|large|\d+x\d+)"$/
3b0b5e57
RT
1225 * @param string $windowsize size of the window (small|medium|large|wxh).
1226 */
5a14c4d9 1227 public function i_change_window_size_to($windowviewport, $windowsize) {
1228 $this->resize_window($windowsize, $windowviewport === 'viewport');
3b0b5e57 1229 }
c0fb7f44 1230
1231 /**
1232 * Checks whether there is an attribute on the given element that contains the specified text.
1233 *
1234 * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should contain "(?P<text_string>(?:[^"]|\\")*)"$/
1235 * @throws ExpectationException
1236 * @param string $attribute Name of attribute
1237 * @param string $element The locator of the specified selector
1238 * @param string $selectortype The selector type
1239 * @param string $text Expected substring
1240 */
1241 public function the_attribute_of_should_contain($attribute, $element, $selectortype, $text) {
1242 // Get the container node (exception if it doesn't exist).
1243 $containernode = $this->get_selected_node($selectortype, $element);
1244 $value = $containernode->getAttribute($attribute);
1245 if ($value == null) {
1246 throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1247 $this->getSession());
1248 } else if (strpos($value, $text) === false) {
1249 throw new ExpectationException('The attribute "' . $attribute .
1250 '" does not contain "' . $text . '" (actual value: "' . $value . '")',
1251 $this->getSession());
1252 }
1253 }
90c3ffe3 1254
1255 /**
1256 * Checks that the attribute on the given element does not contain the specified text.
1257 *
1258 * @Then /^the "(?P<attribute_string>[^"]*)" attribute of "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not contain "(?P<text_string>(?:[^"]|\\")*)"$/
1259 * @throws ExpectationException
1260 * @param string $attribute Name of attribute
1261 * @param string $element The locator of the specified selector
1262 * @param string $selectortype The selector type
1263 * @param string $text Expected substring
1264 */
1265 public function the_attribute_of_should_not_contain($attribute, $element, $selectortype, $text) {
1266 // Get the container node (exception if it doesn't exist).
1267 $containernode = $this->get_selected_node($selectortype, $element);
1268 $value = $containernode->getAttribute($attribute);
1269 if ($value == null) {
1270 throw new ExpectationException('The attribute "' . $attribute. '" does not exist',
1271 $this->getSession());
1272 } else if (strpos($value, $text) !== false) {
1273 throw new ExpectationException('The attribute "' . $attribute .
1274 '" contains "' . $text . '" (value: "' . $value . '")',
1275 $this->getSession());
1276 }
1277 }
641459a8
RT
1278
1279 /**
1280 * Checks the provided value exists in specific row/column of table.
1281 *
1282 * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should contain "(?P<value_string>[^"]*)"$/
1283 * @throws ElementNotFoundException
1284 * @param string $row row text which will be looked in.
97329f1b 1285 * @param string $column column text to search (or numeric value for the column position)
641459a8
RT
1286 * @param string $table table id/class/caption
1287 * @param string $value text to check.
1288 */
1289 public function row_column_of_table_should_contain($row, $column, $table, $value) {
1290 $tablenode = $this->get_selected_node('table', $table);
1291 $tablexpath = $tablenode->getXpath();
1292
921faad9
RT
1293 $rowliteral = behat_context_helper::escape($row);
1294 $valueliteral = behat_context_helper::escape($value);
1295 $columnliteral = behat_context_helper::escape($column);
bd855fd5 1296
97329f1b
MG
1297 if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
1298 // Column indicated as a number, just use it as position of the column.
1299 $columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
1300 } else {
1301 // Header can be in thead or tbody (first row), following xpath should work.
1302 $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
dd2e1c22 1303 $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
97329f1b 1304 $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
dd2e1c22 1305 $columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
97329f1b
MG
1306
1307 // Check if column exists.
1308 $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
1309 $columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
1310 if (empty($columnheader)) {
1311 $columnexceptionmsg = $column . '" in table "' . $table . '"';
1312 throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg);
1313 }
1314 // Following conditions were considered before finding column count.
1315 // 1. Table header can be in thead/tr/th or tbody/tr/td[1].
1316 // 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
1317 $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
1318 "/preceding-sibling::*) + 1]";
641459a8
RT
1319 }
1320
1321 // Check if value exists in specific row/column.
1322 // Get row xpath.
e01012e9
RT
1323 // GoutteDriver uses DomCrawler\Crawler and it is making XPath relative to the current context, so use descendant.
1324 $rowxpath = $tablexpath."/tbody/tr[descendant::th[normalize-space(.)=" . $rowliteral .
1325 "] | descendant::td[normalize-space(.)=" . $rowliteral . "]]";
641459a8 1326
bd855fd5 1327 $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
97329f1b 1328
641459a8
RT
1329 // Looks for the requested node inside the container node.
1330 $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
1331 if (empty($coumnnode)) {
97329f1b
MG
1332 $locatorexceptionmsg = $value . '" in "' . $row . '" row with column "' . $column;
1333 throw new ElementNotFoundException($this->getSession(), "\n$columnvaluexpath\n\n".'Column value', null, $locatorexceptionmsg);
641459a8
RT
1334 }
1335 }
1336
1337 /**
1338 * Checks the provided value should not exist in specific row/column of table.
1339 *
1340 * @Then /^"(?P<row_string>[^"]*)" row "(?P<column_string>[^"]*)" column of "(?P<table_string>[^"]*)" table should not contain "(?P<value_string>[^"]*)"$/
1341 * @throws ElementNotFoundException
1342 * @param string $row row text which will be looked in.
1343 * @param string $column column text to search
1344 * @param string $table table id/class/caption
1345 * @param string $value text to check.
1346 */
1347 public function row_column_of_table_should_not_contain($row, $column, $table, $value) {
1348 try {
1349 $this->row_column_of_table_should_contain($row, $column, $table, $value);
641459a8
RT
1350 } catch (ElementNotFoundException $e) {
1351 // Table row/column doesn't contain this value. Nothing to do.
1352 return;
1353 }
ca0ceacd
TH
1354 // Throw exception if found.
1355 throw new ExpectationException(
1356 '"' . $column . '" with value "' . $value . '" is present in "' . $row . '" row for table "' . $table . '"',
1357 $this->getSession()
1358 );
641459a8
RT
1359 }
1360
1361 /**
1362 * Checks that the provided value exist in table.
641459a8 1363 *
97329f1b
MG
1364 * First row may contain column headers or numeric indexes of the columns
1365 * (syntax -1- is also considered to be column index). Column indexes are
1366 * useful in case of multirow headers and/or presence of cells with colspan.
1367 *
641459a8
RT
1368 * @Then /^the following should exist in the "(?P<table_string>[^"]*)" table:$/
1369 * @throws ExpectationException
1370 * @param string $table name of table
1371 * @param TableNode $data table with first row as header and following values
1372 * | Header 1 | Header 2 | Header 3 |
1373 * | Value 1 | Value 2 | Value 3|
1374 */
08678f6f 1375 public function following_should_exist_in_the_table($table, TableNode $data) {
641459a8
RT
1376 $datahash = $data->getHash();
1377
97329f1b
MG
1378 foreach ($datahash as $row) {
1379 $firstcell = null;
1380 foreach ($row as $column => $value) {
1381 if ($firstcell === null) {
1382 $firstcell = $value;
1383 } else {
1384 $this->row_column_of_table_should_contain($firstcell, $column, $table, $value);
1385 }
641459a8
RT
1386 }
1387 }
1388 }
1389
1390 /**
d1ac356a 1391 * Checks that the provided values do not exist in a table.
641459a8
RT
1392 *
1393 * @Then /^the following should not exist in the "(?P<table_string>[^"]*)" table:$/
1394 * @throws ExpectationException
1395 * @param string $table name of table
1396 * @param TableNode $data table with first row as header and following values
1397 * | Header 1 | Header 2 | Header 3 |
1398 * | Value 1 | Value 2 | Value 3|
1399 */
08678f6f 1400 public function following_should_not_exist_in_the_table($table, TableNode $data) {
641459a8
RT
1401 $datahash = $data->getHash();
1402
1403 foreach ($datahash as $value) {
1404 $row = array_shift($value);
1405 foreach ($value as $column => $value) {
1406 try {
1407 $this->row_column_of_table_should_contain($row, $column, $table, $value);
1408 // Throw exception if found.
641459a8
RT
1409 } catch (ElementNotFoundException $e) {
1410 // Table row/column doesn't contain this value. Nothing to do.
1411 continue;
1412 }
ca0ceacd
TH
1413 throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' .
1414 $row . '" row for table "' . $table . '"', $this->getSession()
1415 );
641459a8
RT
1416 }
1417 }
1418 }
cb7db63b
TH
1419
1420 /**
1421 * Given the text of a link, download the linked file and return the contents.
1422 *
1423 * This is a helper method used by {@link following_should_download_bytes()}
1424 * and {@link following_should_download_between_and_bytes()}
1425 *
1426 * @param string $link the text of the link.
1427 * @return string the content of the downloaded file.
1428 */
df003a85 1429 public function download_file_from_link($link) {
cb7db63b
TH
1430 // Find the link.
1431 $linknode = $this->find_link($link);
1432 $this->ensure_node_is_visible($linknode);
1433
1434 // Get the href and check it.
1435 $url = $linknode->getAttribute('href');
1436 if (!$url) {
1437 throw new ExpectationException('Download link does not have href attribute',
1438 $this->getSession());
1439 }
1440 if (!preg_match('~^https?://~', $url)) {
1441 throw new ExpectationException('Download link not an absolute URL: ' . $url,
1442 $this->getSession());
1443 }
1444
1445 // Download the URL and check the size.
1446 $session = $this->getSession()->getCookie('MoodleSession');
1447 return download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
1448 }
1449
1450 /**
1451 * Downloads the file from a link on the page and checks the size.
1452 *
1453 * Only works if the link has an href attribute. Javascript downloads are
1454 * not supported. Currently, the href must be an absolute URL.
1455 *
1456 * @Then /^following "(?P<link_string>[^"]*)" should download "(?P<expected_bytes>\d+)" bytes$/
1457 * @throws ExpectationException
1458 * @param string $link the text of the link.
1459 * @param number $expectedsize the expected file size in bytes.
1460 */
1461 public function following_should_download_bytes($link, $expectedsize) {
c3b72e58
RT
1462 $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1463
1464 // It will stop spinning once file is downloaded or time out.
1465 $result = $this->spin(
1466 function($context, $args) {
1467 $link = $args['link'];
1468 return $this->download_file_from_link($link);
1469 },
1470 array('link' => $link),
05a5d547 1471 behat_base::get_extended_timeout(),
c3b72e58
RT
1472 $exception
1473 );
1474
1475 // Check download size.
cb7db63b
TH
1476 $actualsize = (int)strlen($result);
1477 if ($actualsize !== (int)$expectedsize) {
1478 throw new ExpectationException('Downloaded data was ' . $actualsize .
1479 ' bytes, expecting ' . $expectedsize, $this->getSession());
1480 }
1481 }
1482
1483 /**
1484 * Downloads the file from a link on the page and checks the size is in a given range.
1485 *
1486 * Only works if the link has an href attribute. Javascript downloads are
1487 * not supported. Currently, the href must be an absolute URL.
1488 *
1489 * The range includes the endpoints. That is, a 10 byte file in considered to
1490 * be between "5" and "10" bytes, and between "10" and "20" bytes.
1491 *
1492 * @Then /^following "(?P<link_string>[^"]*)" should download between "(?P<min_bytes>\d+)" and "(?P<max_bytes>\d+)" bytes$/
1493 * @throws ExpectationException
1494 * @param string $link the text of the link.
1495 * @param number $minexpectedsize the minimum expected file size in bytes.
1496 * @param number $maxexpectedsize the maximum expected file size in bytes.
1497 */
1498 public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
1499 // If the minimum is greater than the maximum then swap the values.
1500 if ((int)$minexpectedsize > (int)$maxexpectedsize) {
1501 list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
1502 }
1503
c3b72e58
RT
1504 $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
1505
1506 // It will stop spinning once file is downloaded or time out.
1507 $result = $this->spin(
1508 function($context, $args) {
1509 $link = $args['link'];
1510
1511 return $this->download_file_from_link($link);
1512 },
1513 array('link' => $link),
05a5d547 1514 behat_base::get_extended_timeout(),
c3b72e58
RT
1515 $exception
1516 );
1517
1518 // Check download size.
cb7db63b
TH
1519 $actualsize = (int)strlen($result);
1520 if ($actualsize < $minexpectedsize || $actualsize > $maxexpectedsize) {
1521 throw new ExpectationException('Downloaded data was ' . $actualsize .
1522 ' bytes, expecting between ' . $minexpectedsize . ' and ' .
1523 $maxexpectedsize, $this->getSession());
1524 }
1525 }
a109a3ca 1526
0e367a11
MG
1527 /**
1528 * Checks that the image on the page is the same as one of the fixture files
1529 *
1530 * @Then /^the image at "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should be identical to "(?P<filepath_string>(?:[^"]|\\")*)"$/
1531 * @throws ExpectationException
1532 * @param string $element The locator of the image
1533 * @param string $selectortype The selector type
1534 * @param string $filepath path to the fixture file
1535 */
1536 public function the_image_at_should_be_identical_to($element, $selectortype, $filepath) {
1537 global $CFG;
1538
1539 // Get the container node (exception if it doesn't exist).
1540 $containernode = $this->get_selected_node($selectortype, $element);
1541 $url = $containernode->getAttribute('src');
1542 if ($url == null) {
1543 throw new ExpectationException('Element does not have src attribute',
1544 $this->getSession());
1545 }
1546 $session = $this->getSession()->getCookie('MoodleSession');
1547 $content = download_file_content($url, array('Cookie' => 'MoodleSession=' . $session));
1548
1549 // Get the content of the fixture file.
1550 // Replace 'admin/' if it is in start of path with $CFG->admin .
1551 if (substr($filepath, 0, 6) === 'admin/') {
1552 $filepath = $CFG->admin . DIRECTORY_SEPARATOR . substr($filepath, 6);
1553 }
1554 $filepath = str_replace('/', DIRECTORY_SEPARATOR, $filepath);
1555 $filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $filepath;
1556 if (!is_readable($filepath)) {
1557 throw new ExpectationException('The file to compare to does not exist.', $this->getSession());
1558 }
1559 $expectedcontent = file_get_contents($filepath);
1560
1561 if ($content !== $expectedcontent) {
1562 throw new ExpectationException('Image is not identical to the fixture. Received ' .
1563 strlen($content) . ' bytes and expected ' . strlen($expectedcontent) . ' bytes');
1564 }
1565 }
1566
a109a3ca
TH
1567 /**
1568 * Prepare to detect whether or not a new page has loaded (or the same page reloaded) some time in the future.
a33fed21 1569 *
a109a3ca
TH
1570 * @Given /^I start watching to see if a new page loads$/
1571 */
1572 public function i_start_watching_to_see_if_a_new_page_loads() {
1573 if (!$this->running_javascript()) {
1574 throw new DriverException('Page load detection requires JavaScript.');
1575 }
1576
9f3a68fe
RT
1577 $session = $this->getSession();
1578
1579 if ($this->pageloaddetectionrunning || $session->getPage()->find('xpath', $this->get_page_load_xpath())) {
a33fed21
SH
1580 // If we find this node at this point we are already watching for a reload and the behat steps
1581 // are out of order. We will treat this as an error - really it needs to be fixed as it indicates a problem.
9f3a68fe
RT
1582 throw new ExpectationException(
1583 'Page load expectation error: page reloads are already been watched for.', $session);
a33fed21
SH
1584 }
1585
1b2c35af
AN
1586 $this->pageloaddetectionrunning = true;
1587
42ad096f 1588 $session->executeScript(
a109a3ca 1589 'var span = document.createElement("span");
a33fed21 1590 span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
a109a3ca
TH
1591 span.setAttribute("style", "display: none;");
1592 document.body.appendChild(span);');
1593 }
1594
1595 /**
a33fed21
SH
1596 * Verify that a new page has loaded (or the same page has reloaded) since the
1597 * last "I start watching to see if a new page loads" step.
1598 *
a109a3ca
TH
1599 * @Given /^a new page should have loaded since I started watching$/
1600 */
1601 public function a_new_page_should_have_loaded_since_i_started_watching() {
9f3a68fe
RT
1602 $session = $this->getSession();
1603
1604 // Make sure page load tracking was started.
1605 if (!$this->pageloaddetectionrunning) {
a33fed21 1606 throw new ExpectationException(
9f3a68fe 1607 'Page load expectation error: page load tracking was not started.', $session);
a33fed21 1608 }
1b2c35af 1609
9f3a68fe
RT
1610 // As the node is inserted by code above it is either there or not, and we do not need spin and it is safe
1611 // to use the native API here which is great as exception handling (the alternative is slow).
1612 if ($session->getPage()->find('xpath', $this->get_page_load_xpath())) {
1613 // We don't want to find this node, if we do we have an error.
1b2c35af 1614 throw new ExpectationException(
9f3a68fe 1615 'Page load expectation error: a new page has not been loaded when it should have been.', $session);
1b2c35af
AN
1616 }
1617
9f3a68fe 1618 // Cancel the tracking of pageloaddetectionrunning.
1b2c35af 1619 $this->pageloaddetectionrunning = false;
a109a3ca
TH
1620 }
1621
1622 /**
a33fed21
SH
1623 * Verify that a new page has not loaded (or the same page has reloaded) since the
1624 * last "I start watching to see if a new page loads" step.
1625 *
a109a3ca
TH
1626 * @Given /^a new page should not have loaded since I started watching$/
1627 */
1628 public function a_new_page_should_not_have_loaded_since_i_started_watching() {
a610c9ab
EL
1629 $session = $this->getSession();
1630
9f3a68fe
RT
1631 // Make sure page load tracking was started.
1632 if (!$this->pageloaddetectionrunning) {
1633 throw new ExpectationException(
1634 'Page load expectation error: page load tracking was not started.', $session);
1635 }
1636
a33fed21
SH
1637 // We use our API here as we can use the exception handling provided by it.
1638 $this->find(
1639 'xpath',
1640 $this->get_page_load_xpath(),
1641 new ExpectationException(
1642 'Page load expectation error: A new page has been loaded when it should not have been.',
1643 $this->getSession()
1644 )
1645 );
a109a3ca
TH
1646 }
1647
1648 /**
1649 * Helper used by {@link a_new_page_should_have_loaded_since_i_started_watching}
1650 * and {@link a_new_page_should_not_have_loaded_since_i_started_watching}
1651 * @return string xpath expression.
1652 */
1653 protected function get_page_load_xpath() {
a33fed21 1654 return "//span[@data-rel = '" . self::PAGE_LOAD_DETECTION_STRING . "']";
a109a3ca 1655 }
d58b0ad6
RT
1656
1657 /**
1658 * Wait unit user press Enter/Return key. Useful when debugging a scenario.
1659 *
1660 * @Then /^(?:|I )pause(?:| scenario execution)$/
1661 */
1162e2c9 1662 public function i_pause_scenario_execution() {
0b07ea42
AN
1663 $message = "<colour:lightYellow>Paused. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.";
1664 behat_util::pause($this->getSession(), $message);
d58b0ad6 1665 }
051b45f8
MN
1666
1667 /**
1668 * Presses a given button in the browser.
cd3a6a78 1669 * NOTE: Phantomjs and goutte driver reloads page while navigating back and forward.
051b45f8
MN
1670 *
1671 * @Then /^I press the "(back|forward|reload)" button in the browser$/
1672 * @param string $button the button to press.
1673 * @throws ExpectationException
1674 */
1675 public function i_press_in_the_browser($button) {
1676 $session = $this->getSession();
1677
1678 if ($button == 'back') {
1679 $session->back();
1680 } else if ($button == 'forward') {
1681 $session->forward();
1682 } else if ($button == 'reload') {
1683 $session->reload();
1684 } else {
1685 throw new ExpectationException('Unknown browser button.', $session);
1686 }
1687 }
77b08110
RW
1688
1689 /**
1690 * Trigger a keydown event for a key on a specific element.
1691 *
1692 * @When /^I press key "(?P<key_string>(?:[^"]|\\")*)" in "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
1693 * @param string $key either char-code or character itself,
1694 * may optionally be prefixed with ctrl-, alt-, shift- or meta-
1695 * @param string $element Element we look for
1696 * @param string $selectortype The type of what we look for
1697 * @throws DriverException
1698 * @throws ExpectationException
1699 */
1700 public function i_press_key_in_element($key, $element, $selectortype) {
1701 if (!$this->running_javascript()) {
1702 throw new DriverException('Key down step is not available with Javascript disabled');
1703 }
1704 // Gets the node based on the requested selector type and locator.
1705 $node = $this->get_selected_node($selectortype, $element);
1706 $modifier = null;
1707 $validmodifiers = array('ctrl', 'alt', 'shift', 'meta');
1708 $char = $key;
1709 if (strpos($key, '-')) {
1710 list($modifier, $char) = preg_split('/-/', $key, 2);
1711 $modifier = strtolower($modifier);
1712 if (!in_array($modifier, $validmodifiers)) {
1713 throw new ExpectationException(sprintf('Unknown key modifier: %s.', $modifier));
1714 }
1715 }
1716 if (is_numeric($char)) {
1717 $char = (int)$char;
1718 }
1719
1720 $node->keyDown($char, $modifier);
1721 $node->keyPress($char, $modifier);
1722 $node->keyUp($char, $modifier);
1723 }
fba0ac63
RT
1724
1725 /**
1726 * Press tab key on a specific element.
1727 *
1728 * @When /^I press tab key in "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
1729 * @param string $element Element we look for
1730 * @param string $selectortype The type of what we look for
1731 * @throws DriverException
1732 * @throws ExpectationException
1733 */
1734 public function i_post_tab_key_in_element($element, $selectortype) {
1735 if (!$this->running_javascript()) {
1736 throw new DriverException('Tab press step is not available with Javascript disabled');
1737 }
1738 // Gets the node based on the requested selector type and locator.
1739 $node = $this->get_selected_node($selectortype, $element);
b5be3875
MN
1740 $driver = $this->getSession()->getDriver();
1741 if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
1742 $driver->post_key("\xEE\x80\x84", $node->getXpath());
1743 } else {
1744 $driver->keyDown($node->getXpath(), "\t");
1745 }
fba0ac63 1746 }
99ad3223
RT
1747
1748 /**
1749 * Checks if database family used is using one of the specified, else skip. (mysql, postgres, mssql, oracle, etc.)
1750 *
1751 * @Given /^database family used is one of the following:$/
1752 * @param TableNode $databasefamilies list of database.
1753 * @return void.
1754 * @throws \Moodle\BehatExtension\Exception\SkippedException
1755 */
1756 public function database_family_used_is_one_of_the_following(TableNode $databasefamilies) {
1757 global $DB;
1758
1759 $dbfamily = $DB->get_dbfamily();
1760
1761 // Check if used db family is one of the specified ones. If yes then return.
1762 foreach ($databasefamilies->getRows() as $dbfamilytocheck) {
1763 if ($dbfamilytocheck[0] == $dbfamily) {
1764 return;
1765 }
1766 }
1767
1768 throw new \Moodle\BehatExtension\Exception\SkippedException();
1769 }
26872a80
K
1770
1771 /**
1772 * Checks focus is with the given element.
1773 *
1774 * @Then /^the focused element is( not)? "(?P<node_string>(?:[^"]|\\")*)" "(?P<node_selector_string>[^"]*)"$/
1775 * @param string $not optional step verifier
1776 * @param string $nodeelement Element identifier
1777 * @param string $nodeselectortype Element type
0cc0f72c 1778 * @throws DriverException If not using JavaScript
26872a80
K
1779 * @throws ExpectationException
1780 */
1781 public function the_focused_element_is($not, $nodeelement, $nodeselectortype) {
1782 if (!$this->running_javascript()) {
0cc0f72c 1783 throw new DriverException('Checking focus on an element requires JavaScript');
26872a80 1784 }
d10ed4d2
AN
1785
1786 $element = $this->find($nodeselectortype, $nodeelement);
26872a80
K
1787 $xpath = addslashes_js($element->getXpath());
1788 $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
1789 document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
1790 $targetisfocused = $this->getSession()->evaluateScript($script);
1791 if ($not == ' not') {
1792 if ($targetisfocused) {
1793 throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
1794 }
1795 } else {
1796 if (!$targetisfocused) {
1797 throw new ExpectationException("$nodeelement $nodeselectortype is not focused", $this->getSession());
1798 }
1799 }
1800 }
1801
1802 /**
1803 * Checks focus is with the given element.
1804 *
1805 * @Then /^the focused element is( not)? "(?P<n>(?:[^"]|\\")*)" "(?P<ns>[^"]*)" in the "(?P<c>(?:[^"]|\\")*)" "(?P<cs>[^"]*)"$/
1806 * @param string $not string optional step verifier
1807 * @param string $element Element identifier
1808 * @param string $selectortype Element type
1809 * @param string $nodeelement Element we look in
1810 * @param string $nodeselectortype The type of selector where we look in
0cc0f72c 1811 * @throws DriverException If not using JavaScript
26872a80
K
1812 * @throws ExpectationException
1813 */
1814 public function the_focused_element_is_in_the($not, $element, $selectortype, $nodeelement, $nodeselectortype) {
1815 if (!$this->running_javascript()) {
0cc0f72c 1816 throw new DriverException('Checking focus on an element requires JavaScript');
26872a80
K
1817 }
1818 $element = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
1819 $xpath = addslashes_js($element->getXpath());
1820 $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
1821 document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
1822 $targetisfocused = $this->getSession()->evaluateScript($script);
1823 if ($not == ' not') {
1824 if ($targetisfocused) {
1825 throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
1826 }
1827 } else {
1828 if (!$targetisfocused) {
1829 throw new ExpectationException("$nodeelement $nodeselectortype is not focused", $this->getSession());
1830 }
1831 }
1832 }
1833
1834 /**
1835 * Manually press tab key.
1836 *
1837 * @When /^I press( shift)? tab$/
1838 * @param string $shift string optional step verifier
1839 * @throws DriverException
1840 */
1841 public function i_manually_press_tab($shift = '') {
1842 if (!$this->running_javascript()) {
1843 throw new DriverException($shift . ' Tab press step is not available with Javascript disabled');
1844 }
1845
1846 $value = ($shift == ' shift') ? [\WebDriver\Key::SHIFT . \WebDriver\Key::TAB] : [\WebDriver\Key::TAB];
1847 $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
1848 }
30e1f5a0
TQ
1849
1850 /**
1851 * Trigger click on node via javascript instead of actually clicking on it via pointer.
1852 * This function resolves the issue of nested elements.
1853 *
1854 * @When /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" skipping visibility check$/
1855 * @param string $element
1856 * @param string $selectortype
1857 */
1858 public function i_click_on_skipping_visibility_check($element, $selectortype) {
1859
1860 // Gets the node based on the requested selector type and locator.
1861 $node = $this->get_selected_node($selectortype, $element);
1862 $this->js_trigger_click($node);
1863 }
71fd7b11
MM
1864
1865 /**
1866 * Checks, that the specified element contains the specified text a certain amount of times.
1867 * When running Javascript tests it also considers that texts may be hidden.
1868 *
1869 * @Then /^I should see "(?P<elementscount_number>\d+)" occurrences of "(?P<text_string>(?:[^"]|\\")*)" in the "(?P<element_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)"$/
1870 * @throws ElementNotFoundException
1871 * @throws ExpectationException
1872 * @param int $elementscount How many occurrences of the element we look for.
1873 * @param string $text
1874 * @param string $element Element we look in.
1875 * @param string $selectortype The type of element where we are looking in.
1876 */
1877 public function i_should_see_occurrences_of_in_element($elementscount, $text, $element, $selectortype) {
1878
1879 // Getting the container where the text should be found.
1880 $container = $this->get_selected_node($selectortype, $element);
1881
1882 // Looking for all the matching nodes without any other descendant matching the
1883 // same xpath (we are using contains(., ....).
1884 $xpathliteral = behat_context_helper::escape($text);
1885 $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
1886 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
1887
1888 $nodes = $this->find_all('xpath', $xpath, false, $container);
1889
1890 if ($this->running_javascript()) {
1891 $nodes = array_filter($nodes, function($node) {
1892 return $node->isVisible();
1893 });
1894 }
1895
1896 if ($elementscount != count($nodes)) {
1897 throw new ExpectationException('Found '.count($nodes).' elements in column. Expected '.$elementscount,
1898 $this->getSession());
1899 }
1900 }
7a82ee7e
VDF
1901
1902 /**
1903 * Manually press enter key.
1904 *
1905 * @When /^I press enter/
1906 * @throws DriverException
1907 */
1908 public function i_manually_press_enter() {
1909 if (!$this->running_javascript()) {
1910 throw new DriverException('Enter press step is not available with Javascript disabled');
1911 }
1912
1913 $value = [\WebDriver\Key::ENTER];
1914 $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
1915 }
786ea937 1916}