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