From ec0a000564a06d6bfdf7baff9956c3be81a7fb54 Mon Sep 17 00:00:00 2001 From: Rajesh Taneja Date: Mon, 4 Apr 2016 14:55:24 +0800 Subject: [PATCH] MDL-53381 behat: Add support to execute step api With chain step deprecation, execute api will help user to execute step api and backport it --- auth/tests/behat/behat_auth.php | 84 +++-------------- lib/behat/behat_base.php | 162 ++++++++++++++++++++++++++++++++ lib/tests/behat/behat_hooks.php | 133 +------------------------- 3 files changed, 178 insertions(+), 201 deletions(-) diff --git a/auth/tests/behat/behat_auth.php b/auth/tests/behat/behat_auth.php index 959c2fb043d..234b13cb173 100644 --- a/auth/tests/behat/behat_auth.php +++ b/auth/tests/behat/behat_auth.php @@ -47,34 +47,15 @@ class behat_auth extends behat_base { * @Given /^I log in as "(?P(?:[^"]|\\")*)"$/ */ public function i_log_in_as($username) { + // Visit login page. + $this->getSession()->visit($this->locate_path('login/index.php')); - // Running this step using the API rather than a chained step because - // we need to see if the 'Log in' link is available or we need to click - // the dropdown to expand the navigation bar before. - $this->getSession()->visit($this->locate_path('/')); + // Enter username and password. + $this->execute('behat_forms::i_set_the_field_to', array('Username', $this->escape($username))); + $this->execute('behat_forms::i_set_the_field_to', array('Password', $this->escape($username))); - // Generic steps (we will prefix them later expanding the navigation dropdown if necessary). - $steps = array( - new Given('I click on "' . get_string('login') . '" "link" in the ".logininfo" "css_element"'), - new Given('I set the field "' . get_string('username') . '" to "' . $this->escape($username) . '"'), - new Given('I set the field "' . get_string('password') . '" to "'. $this->escape($username) . '"'), - new Given('I press "' . get_string('login') . '"') - ); - - // If Javascript is disabled we have enough with these steps. - if (!$this->running_javascript()) { - return $steps; - } - - // Wait for the homepage to be ready. - $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS); - - // If it is needed, it expands the navigation bar with the 'Log in' link. - if ($clicknavbar = $this->get_expand_navbar_step()) { - array_unshift($steps, $clicknavbar); - } - - return $steps; + // Press log in button, no need to check for exceptions as it will checked after this step execution. + $this->execute('behat_forms::press_button', get_string('login')); } /** @@ -83,53 +64,16 @@ class behat_auth extends behat_base { * @Given /^I log out$/ */ public function i_log_out() { - - $steps = array(new When('I follow "' . get_string('logout') . '"')); - - // No need to check anything else if we run without JS. - if (!$this->running_javascript()) { - return $steps; - } - // There is no longer any need to worry about whether the navigation // bar needs to be expanded; user_menu now lives outside the // hamburger. - - // However, the user menu *always* needs to be expanded. - $xpath = "//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]"; - array_unshift($steps, new When('I click on "'.$xpath.'" "xpath_element"')); - - return $steps; - } - - /** - * Returns a step to open the navigation bar if it is needed. - * - * The top log in and log out links are hidden when middle or small - * size windows (or devices) are used. This step returns a step definition - * clicking to expand the navbar if it is hidden. - * - * @return Given|bool A step definition or false if there is no need to show the navbar. - */ - protected function get_expand_navbar_step() { - - // Checking if we need to click the navbar button to show the navigation menu, it - // is hidden by default when using clean theme and a medium or small screen size. - - // The DOM and the JS should be all ready and loaded. Running without spinning - // as this is a widely used step and we can not spend time here trying to see - // a DOM node that is not always there (at the moment clean is not even the - // default theme...). - $navbuttonjs = "return ( - Y.one('.btn-navbar') && - Y.one('.btn-navbar').getComputedStyle('display') !== 'none' - )"; - - // Adding an extra click we need to show the 'Log in' link. - if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) { - return false; + // However, the user menu *always* needs to be expanded. if running JS. + if ($this->running_javascript()) { + $xpath = "//div[@class='usermenu']//a[contains(concat(' ', @class, ' '), ' toggle-display ')]"; + $this->execute('behat_general::i_click_on', array($xpath, "xpath_element")); } - - return new Given('I click on ".btn-navbar" "css_element"'); + // No need to check for exceptions as it will checked after this step execution. + $this->execute('behat_general::click_link', get_string('logout')); } + } diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index efe954b3893..9bfead49f90 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -637,4 +637,166 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { } $this->getSession()->getDriver()->resizeWindow($width, $height); } + + /** + * Waits for all the JS to be loaded. + * + * @throws \Exception + * @throws NoSuchWindow + * @throws UnknownError + * @return bool True or false depending whether all the JS is loaded or not. + */ + public function wait_for_pending_js() { + // Waiting for JS is only valid for JS scenarios. + if (!$this->running_javascript()) { + return; + } + // We don't use behat_base::spin() here as we don't want to end up with an exception + // if the page & JSs don't finish loading properly. + for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) { + $pending = ''; + try { + $jscode = ' + return function() { + if (typeof M === "undefined") { + if (document.readyState === "complete") { + return ""; + } else { + return "incomplete"; + } + } else if (' . self::PAGE_READY_JS . ') { + return ""; + } else if (typeof M.util !== "undefined") { + return M.util.pending_js.join(":"); + } else { + return "incomplete"; + } + }();'; + $pending = $this->getSession()->evaluateScript($jscode); + } catch (NoSuchWindow $nsw) { + // We catch an exception here, in case we just closed the window we were interacting with. + // No javascript is running if there is no window right? + $pending = ''; + } catch (UnknownError $e) { + // M is not defined when the window or the frame don't exist anymore. + if (strstr($e->getMessage(), 'M is not defined') != false) { + $pending = ''; + } + } + // If there are no pending JS we stop waiting. + if ($pending === '') { + return true; + } + // 0.1 seconds. + usleep(100000); + } + // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions(). + // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds + // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the + // number of JS pending code and JS completed code will not match and we will reach this point. + throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT . + ' seconds. There is a Javascript error or the code is extremely slow.'); + } + /** + * Internal step definition to find exceptions, debugging() messages and PHP debug messages. + * + * Part of behat_hooks class as is part of the testing framework, is auto-executed + * after each step so no features will splicitly use it. + * + * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception. + * @see Moodle\BehatExtension\Tester\MoodleStepTester + */ + public function look_for_exceptions() { + // Wrap in try in case we were interacting with a closed window. + try { + // Exceptions. + $exceptionsxpath = "//div[@data-rel='fatalerror']"; + // Debugging messages. + $debuggingxpath = "//div[@data-rel='debugging']"; + // PHP debug messages. + $phperrorxpath = "//div[@data-rel='phpdebugmessage']"; + // Any other backtrace. + $othersxpath = "(//*[contains(., ': call to ')])[1]"; + $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); + $joinedxpath = implode(' | ', $xpaths); + // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check + // is faster than to send the 4 xpath queries for each step. + if (!$this->getSession()->getDriver()->find($joinedxpath)) { + return; + } + // Exceptions. + if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { + // Getting the debugging info and the backtrace. + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); + // If errorinfoboxes is empty, try find notifytiny (original) class. + if (empty($errorinfoboxes)) { + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); + } + $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . + $this->get_debug_text($errorinfoboxes[1]->getHtml()); + $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; + throw new \Exception(html_entity_decode($msg)); + } + // Debugging messages. + if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { + $msgs = array(); + foreach ($debuggingmessages as $debuggingmessage) { + $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); + } + $msg = "debugging() message/s found:\n" . implode("\n", $msgs); + throw new \Exception(html_entity_decode($msg)); + } + // PHP debug messages. + if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { + $msgs = array(); + foreach ($phpmessages as $phpmessage) { + $msgs[] = $this->get_debug_text($phpmessage->getHtml()); + } + $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); + throw new \Exception(html_entity_decode($msg)); + } + // Any other backtrace. + // First looking through xpath as it is faster than get and parse the whole page contents, + // we get the contents and look for matches once we found something to suspect that there is a backtrace. + if ($this->getSession()->getDriver()->find($othersxpath)) { + $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/'; + if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { + $msgs = array(); + foreach ($backtraces[0] as $backtrace) { + $msgs[] = $backtrace . '()'; + } + $msg = "Other backtraces found:\n" . implode("\n", $msgs); + throw new \Exception(htmlentities($msg)); + } + } + } catch (NoSuchWindow $e) { + // If we were interacting with a popup window it will not exists after closing it. + } + } + /** + * Helper function to execute api in a given context. + * + * @param string $contextapi context in which api is defined. + * @param array $params list of params to pass. + * @throws Exception + */ + protected function execute($contextapi, $params = array()) { + if (!is_array($params)) { + $params = array($params); + } + + // Get required context and execute the api. + $contextapi = explode("::", $contextapi); + $context = behat_context_helper::get($contextapi[0]); + call_user_func_array(array($context, $contextapi[1]), $params); + + // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results. + // So don't make them optional for performance reasons. + + // Wait for pending js. + $this->wait_for_pending_js(); + + // Look for exceptions. + $this->look_for_exceptions(); + } } diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index cddbe93c96f..7cfae68cda0 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -492,62 +492,6 @@ class behat_hooks extends behat_base { return array($dir, $filename); } - /** - * Waits for all the JS to be loaded. - * - * @throws \Exception - * @throws NoSuchWindow - * @throws UnknownError - * @return bool True or false depending whether all the JS is loaded or not. - */ - protected function wait_for_pending_js() { - - // We don't use behat_base::spin() here as we don't want to end up with an exception - // if the page & JSs don't finish loading properly. - for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) { - $pending = ''; - try { - $jscode = - 'if (typeof M === "undefined") { - if (document.readyState === "complete") { - return ""; - } else { - return "incomplete"; - } - } else if (' . self::PAGE_READY_JS . ') { - return ""; - } else { - return M.util.pending_js.join(":"); - }'; - $pending = $this->getSession()->evaluateScript($jscode); - } catch (NoSuchWindow $nsw) { - // We catch an exception here, in case we just closed the window we were interacting with. - // No javascript is running if there is no window right? - $pending = ''; - } catch (UnknownError $e) { - // M is not defined when the window or the frame don't exist anymore. - if (strstr($e->getMessage(), 'M is not defined') != false) { - $pending = ''; - } - } - - // If there are no pending JS we stop waiting. - if ($pending === '') { - return true; - } - - // 0.1 seconds. - usleep(100000); - } - - // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions(). - // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds - // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the - // number of JS pending code and JS completed code will not match and we will reach this point. - throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT . - ' seconds. There is a Javascript error or the code is extremely slow.'); - } - /** * Internal step definition to find exceptions, debugging() messages and PHP debug messages. * @@ -565,82 +509,9 @@ class behat_hooks extends behat_base { throw self::$currentstepexception; } - // Wrap in try in case we were interacting with a closed window. - try { + // Look for exceptions displayed on page. + $this->look_for_exceptions(); - // Exceptions. - $exceptionsxpath = "//div[@data-rel='fatalerror']"; - // Debugging messages. - $debuggingxpath = "//div[@data-rel='debugging']"; - // PHP debug messages. - $phperrorxpath = "//div[@data-rel='phpdebugmessage']"; - // Any other backtrace. - $othersxpath = "(//*[contains(., ': call to ')])[1]"; - - $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); - $joinedxpath = implode(' | ', $xpaths); - - // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check - // is faster than to send the 4 xpath queries for each step. - if (!$this->getSession()->getDriver()->find($joinedxpath)) { - return; - } - - // Exceptions. - if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { - - // Getting the debugging info and the backtrace. - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); - // If errorinfoboxes is empty, try find notifytiny (original) class. - if (empty($errorinfoboxes)) { - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); - } - $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . - $this->get_debug_text($errorinfoboxes[1]->getHtml()); - - $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; - throw new \Exception(html_entity_decode($msg)); - } - - // Debugging messages. - if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { - $msgs = array(); - foreach ($debuggingmessages as $debuggingmessage) { - $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); - } - $msg = "debugging() message/s found:\n" . implode("\n", $msgs); - throw new \Exception(html_entity_decode($msg)); - } - - // PHP debug messages. - if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { - - $msgs = array(); - foreach ($phpmessages as $phpmessage) { - $msgs[] = $this->get_debug_text($phpmessage->getHtml()); - } - $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); - throw new \Exception(html_entity_decode($msg)); - } - - // Any other backtrace. - // First looking through xpath as it is faster than get and parse the whole page contents, - // we get the contents and look for matches once we found something to suspect that there is a backtrace. - if ($this->getSession()->getDriver()->find($othersxpath)) { - $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/'; - if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { - $msgs = array(); - foreach ($backtraces[0] as $backtrace) { - $msgs[] = $backtrace . '()'; - } - $msg = "Other backtraces found:\n" . implode("\n", $msgs); - throw new \Exception(htmlentities($msg)); - } - } - - } catch (NoSuchWindow $e) { - // If we were interacting with a popup window it will not exists after closing it. - } } /**