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