[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 * Base class of all steps definitions. 19 * 20 * This script is only called from Behat as part of it's integration 21 * in Moodle. 22 * 23 * @package core 24 * @category test 25 * @copyright 2012 David Monllaó 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 29 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. 30 31 use Behat\Mink\Exception\ExpectationException as ExpectationException, 32 Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, 33 Behat\Mink\Element\NodeElement as NodeElement; 34 35 /** 36 * Steps definitions base class. 37 * 38 * To extend by the steps definitions of the different Moodle components. 39 * 40 * It can not contain steps definitions to avoid duplicates, only utility 41 * methods shared between steps. 42 * 43 * @method NodeElement find_field(string $locator) Finds a form element 44 * @method NodeElement find_button(string $locator) Finds a form input submit element or a button 45 * @method NodeElement find_link(string $locator) Finds a link on a page 46 * @method NodeElement find_file(string $locator) Finds a forum input file element 47 * 48 * @package core 49 * @category test 50 * @copyright 2012 David Monllaó 51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 52 */ 53 class behat_base extends Behat\MinkExtension\Context\RawMinkContext { 54 55 /** 56 * Small timeout. 57 * 58 * A reduced timeout for cases where self::TIMEOUT is too much 59 * and a simple $this->getSession()->getPage()->find() could not 60 * be enough. 61 */ 62 const REDUCED_TIMEOUT = 2; 63 64 /** 65 * The timeout for each Behat step (load page, wait for an element to load...). 66 */ 67 const TIMEOUT = 6; 68 69 /** 70 * And extended timeout for specific cases. 71 */ 72 const EXTENDED_TIMEOUT = 10; 73 74 /** 75 * The JS code to check that the page is ready. 76 */ 77 const PAGE_READY_JS = '(typeof M !== "undefined" && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")'; 78 79 /** 80 * Locates url, based on provided path. 81 * Override to provide custom routing mechanism. 82 * 83 * @see Behat\MinkExtension\Context\MinkContext 84 * @param string $path 85 * @return string 86 */ 87 protected function locate_path($path) { 88 $starturl = rtrim($this->getMinkParameter('base_url'), '/') . '/'; 89 return 0 !== strpos($path, 'http') ? $starturl . ltrim($path, '/') : $path; 90 } 91 92 /** 93 * Returns the first matching element. 94 * 95 * @link http://mink.behat.org/#traverse-the-page-selectors 96 * @param string $selector The selector type (css, xpath, named...) 97 * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... 98 * @param Exception $exception Otherwise we throw exception with generic info 99 * @param NodeElement $node Spins around certain DOM node instead of the whole page 100 * @param int $timeout Forces a specific time out (in seconds). 101 * @return NodeElement 102 */ 103 protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) { 104 105 // Throw exception, so dev knows it is not supported. 106 if ($selector === 'named') { 107 $exception = 'Using the "named" selector is deprecated as of 3.1. ' 108 .' Use the "named_partial" or use the "named_exact" selector instead.'; 109 throw new ExpectationException($exception, $this->getSession()); 110 } 111 112 // Returns the first match. 113 $items = $this->find_all($selector, $locator, $exception, $node, $timeout); 114 return count($items) ? reset($items) : null; 115 } 116 117 /** 118 * Returns all matching elements. 119 * 120 * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method. 121 * 122 * @link http://mink.behat.org/#traverse-the-page-selectors 123 * @param string $selector The selector type (css, xpath, named...) 124 * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... 125 * @param Exception $exception Otherwise we throw expcetion with generic info 126 * @param NodeElement $node Spins around certain DOM node instead of the whole page 127 * @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied. 128 * @return array NodeElements list 129 */ 130 protected function find_all($selector, $locator, $exception = false, $node = false, $timeout = false) { 131 132 // Throw exception, so dev knows it is not supported. 133 if ($selector === 'named') { 134 $exception = 'Using the "named" selector is deprecated as of 3.1. ' 135 .' Use the "named_partial" or use the "named_exact" selector instead.'; 136 throw new ExpectationException($exception, $this->getSession()); 137 } 138 139 // Generic info. 140 if (!$exception) { 141 142 // With named selectors we can be more specific. 143 if (($selector == 'named_exact') || ($selector == 'named_partial')) { 144 $exceptiontype = $locator[0]; 145 $exceptionlocator = $locator[1]; 146 147 // If we are in a @javascript session all contents would be displayed as HTML characters. 148 if ($this->running_javascript()) { 149 $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES); 150 } 151 152 } else { 153 $exceptiontype = $selector; 154 $exceptionlocator = $locator; 155 } 156 157 $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator); 158 } 159 160 $params = array('selector' => $selector, 'locator' => $locator); 161 // Pushing $node if required. 162 if ($node) { 163 $params['node'] = $node; 164 } 165 166 // How much we will be waiting for the element to appear. 167 if (!$timeout) { 168 $timeout = self::TIMEOUT; 169 $microsleep = false; 170 } else { 171 // Spinning each 0.1 seconds if the timeout was forced as we understand 172 // that is a special case and is good to refine the performance as much 173 // as possible. 174 $microsleep = true; 175 } 176 177 // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception. 178 return $this->spin( 179 function($context, $args) { 180 181 // If no DOM node provided look in all the page. 182 if (empty($args['node'])) { 183 return $context->getSession()->getPage()->findAll($args['selector'], $args['locator']); 184 } 185 186 // For nodes contained in other nodes we can not use the basic named selectors 187 // as they include unions and they would look for matches in the DOM root. 188 $elementxpath = $context->getSession()->getSelectorsHandler()->selectorToXpath($args['selector'], $args['locator']); 189 190 // Split the xpath in unions and prefix them with the container xpath. 191 $unions = explode('|', $elementxpath); 192 foreach ($unions as $key => $union) { 193 $union = trim($union); 194 195 // We are in the container node. 196 if (strpos($union, '.') === 0) { 197 $union = substr($union, 1); 198 } else if (strpos($union, '/') !== 0) { 199 // Adding the path separator in case it is not there. 200 $union = '/' . $union; 201 } 202 $unions[$key] = $args['node']->getXpath() . $union; 203 } 204 205 // We can not use usual Element::find() as it prefixes with DOM root. 206 return $context->getSession()->getDriver()->find(implode('|', $unions)); 207 }, 208 $params, 209 $timeout, 210 $exception, 211 $microsleep 212 ); 213 } 214 215 /** 216 * Finds DOM nodes in the page using named selectors. 217 * 218 * The point of using this method instead of Mink ones is the spin 219 * method of behat_base::find() that looks for the element until it 220 * is available or it timeouts, this avoids the false failures received 221 * when selenium tries to execute commands on elements that are not 222 * ready to be used. 223 * 224 * All steps that requires elements to be available before interact with 225 * them should use one of the find* methods. 226 * 227 * The methods calls requires a {'find_' . $elementtype}($locator) 228 * format, like find_link($locator), find_select($locator), 229 * find_button($locator)... 230 * 231 * @link http://mink.behat.org/#named-selectors 232 * @throws coding_exception 233 * @param string $name The name of the called method 234 * @param mixed $arguments 235 * @return NodeElement 236 */ 237 public function __call($name, $arguments) { 238 239 if (substr($name, 0, 5) !== 'find_') { 240 throw new coding_exception('The "' . $name . '" method does not exist'); 241 } 242 243 // Only the named selector identifier. 244 $cleanname = substr($name, 5); 245 246 // All named selectors shares the interface. 247 if (count($arguments) !== 1) { 248 throw new coding_exception('The "' . $cleanname . '" named selector needs the locator as it\'s single argument'); 249 } 250 251 // Redirecting execution to the find method with the specified selector. 252 // It will detect if it's pointing to an unexisting named selector. 253 return $this->find('named_partial', 254 array( 255 $cleanname, 256 behat_context_helper::escape($arguments[0]) 257 ) 258 ); 259 } 260 261 /** 262 * Escapes the double quote character. 263 * 264 * Double quote is the argument delimiter, it can be escaped 265 * with a backslash, but we auto-remove this backslashes 266 * before the step execution, this method is useful when using 267 * arguments as arguments for other steps. 268 * 269 * @param string $string 270 * @return string 271 */ 272 public function escape($string) { 273 return str_replace('"', '\"', $string); 274 } 275 276 /** 277 * Executes the passed closure until returns true or time outs. 278 * 279 * In most cases the document.readyState === 'complete' will be enough, but sometimes JS 280 * requires more time to be completely loaded or an element to be visible or whatever is required to 281 * perform some action on an element; this method receives a closure which should contain the 282 * required statements to ensure the step definition actions and assertions have all their needs 283 * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the 284 * closure to the caller. 285 * 286 * The closures requirements to work well with this spin method are: 287 * - Must return false, null or '' if something goes wrong 288 * - Must return something != false if finishes as expected, this will be the (mixed) value 289 * returned by spin() 290 * 291 * The arguments of the closure are mixed, use $args depending on your needs. 292 * 293 * You can provide an exception to give more accurate feedback to tests writers, otherwise the 294 * closure exception will be used, but you must provide an exception if the closure does not throw 295 * an exception. 296 * 297 * @throws Exception If it timeouts without receiving something != false from the closure 298 * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method) 299 * @param mixed $args Arguments to pass to the closure 300 * @param int $timeout Timeout in seconds 301 * @param Exception $exception The exception to throw in case it time outs. 302 * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds. 303 * @return mixed The value returned by the closure 304 */ 305 protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) { 306 307 // Using default timeout which is pretty high. 308 if (!$timeout) { 309 $timeout = self::TIMEOUT; 310 } 311 if ($microsleep) { 312 // Will sleep 1/10th of a second by default for self::TIMEOUT seconds. 313 $loops = $timeout * 10; 314 } else { 315 // Will sleep for self::TIMEOUT seconds. 316 $loops = $timeout; 317 } 318 319 // DOM will never change on non-javascript case; do not wait or try again. 320 if (!$this->running_javascript()) { 321 $loops = 1; 322 } 323 324 for ($i = 0; $i < $loops; $i++) { 325 // We catch the exception thrown by the step definition to execute it again. 326 try { 327 // We don't check with !== because most of the time closures will return 328 // direct Behat methods returns and we are not sure it will be always (bool)false 329 // if it just runs the behat method without returning anything $return == null. 330 if ($return = call_user_func($lambda, $this, $args)) { 331 return $return; 332 } 333 } catch (Exception $e) { 334 // We would use the first closure exception if no exception has been provided. 335 if (!$exception) { 336 $exception = $e; 337 } 338 // We wait until no exception is thrown or timeout expires. 339 continue; 340 } 341 342 if ($this->running_javascript()) { 343 if ($microsleep) { 344 usleep(100000); 345 } else { 346 sleep(1); 347 } 348 } 349 } 350 351 // Using coding_exception as is a development issue if no exception has been provided. 352 if (!$exception) { 353 $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception'); 354 } 355 356 // Throwing exception to the user. 357 throw $exception; 358 } 359 360 /** 361 * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. 362 * 363 * Use behat_base::get_text_selector_node() for text-based selectors. 364 * 365 * @throws ElementNotFoundException Thrown by behat_base::find 366 * @param string $selectortype 367 * @param string $element 368 * @return NodeElement 369 */ 370 protected function get_selected_node($selectortype, $element) { 371 372 // Getting Mink selector and locator. 373 list($selector, $locator) = $this->transform_selector($selectortype, $element); 374 375 // Returns the NodeElement. 376 return $this->find($selector, $locator); 377 } 378 379 /** 380 * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. 381 * 382 * @throws ElementNotFoundException Thrown by behat_base::find 383 * @param string $selectortype 384 * @param string $element 385 * @return NodeElement 386 */ 387 protected function get_text_selector_node($selectortype, $element) { 388 389 // Getting Mink selector and locator. 390 list($selector, $locator) = $this->transform_text_selector($selectortype, $element); 391 392 // Returns the NodeElement. 393 return $this->find($selector, $locator); 394 } 395 396 /** 397 * Gets the requested element inside the specified container. 398 * 399 * @throws ElementNotFoundException Thrown by behat_base::find 400 * @param mixed $selectortype The element selector type. 401 * @param mixed $element The element locator. 402 * @param mixed $containerselectortype The container selector type. 403 * @param mixed $containerelement The container locator. 404 * @return NodeElement 405 */ 406 protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) { 407 408 // Gets the container, it will always be text based. 409 $containernode = $this->get_text_selector_node($containerselectortype, $containerelement); 410 411 list($selector, $locator) = $this->transform_selector($selectortype, $element); 412 413 // Specific exception giving info about where can't we find the element. 414 $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"'; 415 $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg); 416 417 // Looks for the requested node inside the container node. 418 return $this->find($selector, $locator, $exception, $containernode); 419 } 420 421 /** 422 * Transforms from step definition's argument style to Mink format. 423 * 424 * Mink has 3 different selectors css, xpath and named, where named 425 * selectors includes link, button, field... to simplify and group multiple 426 * steps in one we use the same interface, considering all link, buttons... 427 * at the same level as css selectors and xpath; this method makes the 428 * conversion from the arguments received by the steps to the selectors and locators 429 * required to interact with Mink. 430 * 431 * @throws ExpectationException 432 * @param string $selectortype It can be css, xpath or any of the named selectors. 433 * @param string $element The locator (or string) we are looking for. 434 * @return array Contains the selector and the locator expected by Mink. 435 */ 436 protected function transform_selector($selectortype, $element) { 437 438 // Here we don't know if an allowed text selector is being used. 439 $selectors = behat_selectors::get_allowed_selectors(); 440 if (!isset($selectors[$selectortype])) { 441 throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession()); 442 } 443 444 return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession()); 445 } 446 447 /** 448 * Transforms from step definition's argument style to Mink format. 449 * 450 * Delegates all the process to behat_base::transform_selector() checking 451 * the provided $selectortype. 452 * 453 * @throws ExpectationException 454 * @param string $selectortype It can be css, xpath or any of the named selectors. 455 * @param string $element The locator (or string) we are looking for. 456 * @return array Contains the selector and the locator expected by Mink. 457 */ 458 protected function transform_text_selector($selectortype, $element) { 459 460 $selectors = behat_selectors::get_allowed_text_selectors(); 461 if (empty($selectors[$selectortype])) { 462 throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession()); 463 } 464 465 return $this->transform_selector($selectortype, $element); 466 } 467 468 /** 469 * Returns whether the scenario is running in a browser that can run Javascript or not. 470 * 471 * @return boolean 472 */ 473 protected function running_javascript() { 474 return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; 475 } 476 477 /** 478 * Spins around an element until it exists 479 * 480 * @throws ExpectationException 481 * @param string $element 482 * @param string $selectortype 483 * @return void 484 */ 485 protected function ensure_element_exists($element, $selectortype) { 486 487 // Getting the behat selector & locator. 488 list($selector, $locator) = $this->transform_selector($selectortype, $element); 489 490 // Exception if it timesout and the element is still there. 491 $msg = 'The "' . $element . '" element does not exist and should exist'; 492 $exception = new ExpectationException($msg, $this->getSession()); 493 494 // It will stop spinning once the find() method returns true. 495 $this->spin( 496 function($context, $args) { 497 // We don't use behat_base::find as it is already spinning. 498 if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) { 499 return true; 500 } 501 return false; 502 }, 503 array('selector' => $selector, 'locator' => $locator), 504 self::EXTENDED_TIMEOUT, 505 $exception, 506 true 507 ); 508 509 } 510 511 /** 512 * Spins until the element does not exist 513 * 514 * @throws ExpectationException 515 * @param string $element 516 * @param string $selectortype 517 * @return void 518 */ 519 protected function ensure_element_does_not_exist($element, $selectortype) { 520 521 // Getting the behat selector & locator. 522 list($selector, $locator) = $this->transform_selector($selectortype, $element); 523 524 // Exception if it timesout and the element is still there. 525 $msg = 'The "' . $element . '" element exists and should not exist'; 526 $exception = new ExpectationException($msg, $this->getSession()); 527 528 // It will stop spinning once the find() method returns false. 529 $this->spin( 530 function($context, $args) { 531 // We don't use behat_base::find() as we are already spinning. 532 if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) { 533 return true; 534 } 535 return false; 536 }, 537 array('selector' => $selector, 'locator' => $locator), 538 self::EXTENDED_TIMEOUT, 539 $exception, 540 true 541 ); 542 } 543 544 /** 545 * Ensures that the provided node is visible and we can interact with it. 546 * 547 * @throws ExpectationException 548 * @param NodeElement $node 549 * @return void Throws an exception if it times out without the element being visible 550 */ 551 protected function ensure_node_is_visible($node) { 552 553 if (!$this->running_javascript()) { 554 return; 555 } 556 557 // Exception if it timesout and the element is still there. 558 $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible'; 559 $exception = new ExpectationException($msg, $this->getSession()); 560 561 // It will stop spinning once the isVisible() method returns true. 562 $this->spin( 563 function($context, $args) { 564 if ($args->isVisible()) { 565 return true; 566 } 567 return false; 568 }, 569 $node, 570 self::EXTENDED_TIMEOUT, 571 $exception, 572 true 573 ); 574 } 575 576 /** 577 * Ensures that the provided element is visible and we can interact with it. 578 * 579 * Returns the node in case other actions are interested in using it. 580 * 581 * @throws ExpectationException 582 * @param string $element 583 * @param string $selectortype 584 * @return NodeElement Throws an exception if it times out without being visible 585 */ 586 protected function ensure_element_is_visible($element, $selectortype) { 587 588 if (!$this->running_javascript()) { 589 return; 590 } 591 592 $node = $this->get_selected_node($selectortype, $element); 593 $this->ensure_node_is_visible($node); 594 595 return $node; 596 } 597 598 /** 599 * Ensures that all the page's editors are loaded. 600 * 601 * @deprecated since Moodle 2.7 MDL-44084 - please do not use this function any more. 602 * @throws ElementNotFoundException 603 * @throws ExpectationException 604 * @return void 605 */ 606 protected function ensure_editors_are_loaded() { 607 global $CFG; 608 609 if (empty($CFG->behat_usedeprecated)) { 610 debugging('Function behat_base::ensure_editors_are_loaded() is deprecated. It is no longer required.'); 611 } 612 return; 613 } 614 615 /** 616 * Change browser window size. 617 * - small: 640x480 618 * - medium: 1024x768 619 * - large: 2560x1600 620 * 621 * @param string $windowsize size of window. 622 * @param bool $viewport If true, changes viewport rather than window size 623 * @throws ExpectationException 624 */ 625 protected function resize_window($windowsize, $viewport = false) { 626 // Non JS don't support resize window. 627 if (!$this->running_javascript()) { 628 return; 629 } 630 631 switch ($windowsize) { 632 case "small": 633 $width = 640; 634 $height = 480; 635 break; 636 case "medium": 637 $width = 1024; 638 $height = 768; 639 break; 640 case "large": 641 $width = 2560; 642 $height = 1600; 643 break; 644 default: 645 preg_match('/^(\d+x\d+)$/', $windowsize, $matches); 646 if (empty($matches) || (count($matches) != 2)) { 647 throw new ExpectationException("Invalid screen size, can't resize", $this->getSession()); 648 } 649 $size = explode('x', $windowsize); 650 $width = (int) $size[0]; 651 $height = (int) $size[1]; 652 } 653 if ($viewport) { 654 // When setting viewport size, we set it so that the document width will be exactly 655 // as specified, assuming that there is a vertical scrollbar. (In cases where there is 656 // no scrollbar it will be slightly wider. We presume this is rare and predictable.) 657 // The window inner height will be as specified, which means the available viewport will 658 // actually be smaller if there is a horizontal scrollbar. We assume that horizontal 659 // scrollbars are rare so this doesn't matter. 660 $offset = $this->getSession()->getDriver()->evaluateScript( 661 'return (function() { var before = document.body.style.overflowY;' . 662 'document.body.style.overflowY = "scroll";' . 663 'var result = {};' . 664 'result.x = window.outerWidth - document.body.offsetWidth;' . 665 'result.y = window.outerHeight - window.innerHeight;' . 666 'document.body.style.overflowY = before;' . 667 'return result; })();'); 668 $width += $offset['x']; 669 $height += $offset['y']; 670 } 671 672 $this->getSession()->getDriver()->resizeWindow($width, $height); 673 } 674 675 /** 676 * Waits for all the JS to be loaded. 677 * 678 * @throws \Exception 679 * @throws NoSuchWindow 680 * @throws UnknownError 681 * @return bool True or false depending whether all the JS is loaded or not. 682 */ 683 public function wait_for_pending_js() { 684 // Waiting for JS is only valid for JS scenarios. 685 if (!$this->running_javascript()) { 686 return; 687 } 688 689 // We don't use behat_base::spin() here as we don't want to end up with an exception 690 // if the page & JSs don't finish loading properly. 691 for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) { 692 $pending = ''; 693 try { 694 $jscode = ' 695 return function() { 696 if (typeof M === "undefined") { 697 if (document.readyState === "complete") { 698 return ""; 699 } else { 700 return "incomplete"; 701 } 702 } else if (' . self::PAGE_READY_JS . ') { 703 return ""; 704 } else if (typeof M.util !== "undefined") { 705 return M.util.pending_js.join(":"); 706 } else { 707 return "incomplete" 708 } 709 }();'; 710 $pending = $this->getSession()->evaluateScript($jscode); 711 } catch (NoSuchWindow $nsw) { 712 // We catch an exception here, in case we just closed the window we were interacting with. 713 // No javascript is running if there is no window right? 714 $pending = ''; 715 } catch (UnknownError $e) { 716 // M is not defined when the window or the frame don't exist anymore. 717 if (strstr($e->getMessage(), 'M is not defined') != false) { 718 $pending = ''; 719 } 720 } 721 722 // If there are no pending JS we stop waiting. 723 if ($pending === '') { 724 return true; 725 } 726 727 // 0.1 seconds. 728 usleep(100000); 729 } 730 731 // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions(). 732 // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds 733 // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the 734 // number of JS pending code and JS completed code will not match and we will reach this point. 735 throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT . 736 ' seconds. There is a Javascript error or the code is extremely slow.'); 737 } 738 739 /** 740 * Internal step definition to find exceptions, debugging() messages and PHP debug messages. 741 * 742 * Part of behat_hooks class as is part of the testing framework, is auto-executed 743 * after each step so no features will splicitly use it. 744 * 745 * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception. 746 * @see Moodle\BehatExtension\Tester\MoodleStepTester 747 */ 748 public function look_for_exceptions() { 749 // Wrap in try in case we were interacting with a closed window. 750 try { 751 752 // Exceptions. 753 $exceptionsxpath = "//div[@data-rel='fatalerror']"; 754 // Debugging messages. 755 $debuggingxpath = "//div[@data-rel='debugging']"; 756 // PHP debug messages. 757 $phperrorxpath = "//div[@data-rel='phpdebugmessage']"; 758 // Any other backtrace. 759 $othersxpath = "(//*[contains(., ': call to ')])[1]"; 760 761 $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); 762 $joinedxpath = implode(' | ', $xpaths); 763 764 // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check 765 // is faster than to send the 4 xpath queries for each step. 766 if (!$this->getSession()->getDriver()->find($joinedxpath)) { 767 return; 768 } 769 770 // Exceptions. 771 if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { 772 773 // Getting the debugging info and the backtrace. 774 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); 775 // If errorinfoboxes is empty, try find notifytiny (original) class. 776 if (empty($errorinfoboxes)) { 777 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); 778 } 779 $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . 780 $this->get_debug_text($errorinfoboxes[1]->getHtml()); 781 782 $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; 783 throw new \Exception(html_entity_decode($msg)); 784 } 785 786 // Debugging messages. 787 if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { 788 $msgs = array(); 789 foreach ($debuggingmessages as $debuggingmessage) { 790 $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); 791 } 792 $msg = "debugging() message/s found:\n" . implode("\n", $msgs); 793 throw new \Exception(html_entity_decode($msg)); 794 } 795 796 // PHP debug messages. 797 if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { 798 799 $msgs = array(); 800 foreach ($phpmessages as $phpmessage) { 801 $msgs[] = $this->get_debug_text($phpmessage->getHtml()); 802 } 803 $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); 804 throw new \Exception(html_entity_decode($msg)); 805 } 806 807 // Any other backtrace. 808 // First looking through xpath as it is faster than get and parse the whole page contents, 809 // we get the contents and look for matches once we found something to suspect that there is a backtrace. 810 if ($this->getSession()->getDriver()->find($othersxpath)) { 811 $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/'; 812 if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { 813 $msgs = array(); 814 foreach ($backtraces[0] as $backtrace) { 815 $msgs[] = $backtrace . '()'; 816 } 817 $msg = "Other backtraces found:\n" . implode("\n", $msgs); 818 throw new \Exception(htmlentities($msg)); 819 } 820 } 821 822 } catch (NoSuchWindow $e) { 823 // If we were interacting with a popup window it will not exists after closing it. 824 } 825 } 826 827 /** 828 * Converts HTML tags to line breaks to display the info in CLI 829 * 830 * @param string $html 831 * @return string 832 */ 833 protected function get_debug_text($html) { 834 835 // Replacing HTML tags for new lines and keeping only the text. 836 $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html); 837 return preg_replace("/(\n)+/s", "\n", $notags); 838 } 839 840 /** 841 * Helper function to execute api in a given context. 842 * 843 * @param string $contextapi context in which api is defined. 844 * @param array $params list of params to pass. 845 * @throws Exception 846 */ 847 protected function execute($contextapi, $params = array()) { 848 if (!is_array($params)) { 849 $params = array($params); 850 } 851 852 // Get required context and execute the api. 853 $contextapi = explode("::", $contextapi); 854 $context = behat_context_helper::get($contextapi[0]); 855 call_user_func_array(array($context, $contextapi[1]), $params); 856 857 // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results. 858 // Don't make them optional for performance reasons. 859 860 // Wait for pending js. 861 $this->wait_for_pending_js(); 862 863 // Look for exceptions. 864 $this->look_for_exceptions(); 865 } 866 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |