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