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