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