From 6e2c417c1f07c76622c1ff7f7c8f23596bc91dce Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 29 Oct 2013 10:36:30 +0800 Subject: [PATCH] MDL-41030 Behat: Add support for waiting for javascript This is so that phantomjs (which runs faster than selenium) can pass the tests. --- lib/javascript-static.js | 70 +++++++++++++++++++++++++++++++++ lib/outputrequirementslib.php | 12 ++++-- lib/tests/behat/behat_forms.php | 26 +++++++++++- lib/tests/behat/behat_hooks.php | 50 ++++++++++------------- 4 files changed, 124 insertions(+), 34 deletions(-) diff --git a/lib/javascript-static.js b/lib/javascript-static.js index 1c1e19ccec7..df36acd83d5 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -755,6 +755,76 @@ M.util.init_block_hider = function(Y, config) { }); }; +/** + * @var pending_js - The keys are the list of all pending js actions. + * @type Object + */ +M.util.pending_js = []; +M.util.complete_js = []; + +/** + * Register any long running javascript code with a unique identifier. + * Should be followed with a call to js_complete with a matching + * idenfitier when the code is complete. May also be called with no arguments + * to test if there is any js calls pending. This is relied on by behat so that + * it can wait for all pending updates before interacting with a page. + * @param String uniqid - optional, if provided, + * registers this identifier until js_complete is called. + * @return boolean - True if there is any pending js. + */ +M.util.js_pending = function(uniqid) { + if (uniqid !== false) { + M.util.pending_js.push(uniqid); + } + + return M.util.pending_js.length; +}; + +/** + * Register listeners for Y.io start/end so we can wait for them in behat. + */ +M.util.js_watch_io = function() { + YUI.add('moodle-core-io', function(Y) { + Y.on('io:start', function(id) { + M.util.js_pending('io:' + id); + }); + Y.on('io:end', function(id) { + M.util.js_complete('io:' + id); + }); + }); + YUI.applyConfig({ + modules: { + 'moodle-core-io': { + after: ['io-base'] + }, + 'io-base': { + requires: ['moodle-core-io'], + } + } + }); + +}; + +// Start this asap. +M.util.js_pending('init'); +M.util.js_watch_io(); + +/** + * Unregister any long running javascript code by unique identifier. + * This function should form a matching pair with js_pending + * + * @param String uniqid - required, unregisters this identifier + * @return boolean - True if there is any pending js. + */ +M.util.js_complete = function(uniqid) { + var index = M.util.pending_js.indexOf(uniqid); + if (index >= 0) { + M.util.complete_js.push(M.util.pending_js.splice(index, 1)); + } + + return M.util.pending_js.length; +}; + /** * Returns a string registered in advance for usage in JavaScript * diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index d530d797e2d..7519a3139b4 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -1043,14 +1043,18 @@ class page_requirements_manager { public function js_init_code($jscode, $ondomready = false, array $module = null) { $jscode = trim($jscode, " ;\n"). ';'; + $uniqid = html_writer::random_id(); + $startjs = " M.util.js_pending('" . $uniqid . "');"; + $endjs = " M.util.js_complete('" . $uniqid . "');"; + if ($module) { $this->js_module($module); $modulename = $module['name']; - $jscode = "Y.use('$modulename', function(Y) { $jscode });"; + $jscode = "$startjs Y.use('$modulename', function(Y) { $jscode $endjs });"; } if ($ondomready) { - $jscode = "Y.on('domready', function() { $jscode });"; + $jscode = "$startjs Y.on('domready', function() { $jscode $endjs });"; } $this->jsinitcode[] = $jscode; @@ -1216,7 +1220,7 @@ class page_requirements_manager { $output .= js_writer::function_call($data[0], $data[1], $data[2]); } if (!empty($ondomready)) { - $output = " Y.on('domready', function() {\n$output\n });"; + $output = " Y.on('domready', function() {\n$output\n});"; } } return $output; @@ -1463,6 +1467,8 @@ class page_requirements_manager { // Add other requested modules. $output = $this->get_extra_modules_code(); + $this->js_init_code('M.util.js_complete("init");', true); + // All the other linked scripts - there should be as few as possible. if ($this->jsincludes['footer']) { foreach ($this->jsincludes['footer'] as $url) { diff --git a/lib/tests/behat/behat_forms.php b/lib/tests/behat/behat_forms.php index b33931fcc98..e5eaa84fa93 100644 --- a/lib/tests/behat/behat_forms.php +++ b/lib/tests/behat/behat_forms.php @@ -60,6 +60,30 @@ class behat_forms extends behat_base { $buttonnode->press(); } + /** + * Try a few times to set a field value as it may not be visible yet (TinyMCE). + * + * @param string $field + * @param string $value + */ + public function set_field_value($field, $value) { + $lastexception = null; + // Spin on this - certain fields, e.g. text editors (I'm looking at you TinyMCE) load slowly and randomly. + $retries = 5; + while ($retries > 0) { + try { + $field->set_value($value); + return; + } catch (Exception $e) { + usleep(100000); + $retries--; + $lastexception = $e; + } + } + // If we timeout - throw the last exception. + throw $lastexception; + } + /** * Fills a moodle form with field/value data. * @@ -84,7 +108,7 @@ class behat_forms extends behat_base { $field = behat_field_manager::get_form_field($fieldnode, $this->getSession()); // Delegates to the field class. - $field->set_value($value); + $this->set_field_value($field, $value); } } diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index 57891341d28..e18e48d44c4 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -222,42 +222,32 @@ class behat_hooks extends behat_base { } /** - * Checks that all DOM is ready. + * Wait for JS to comlete. * * Executed only when running against a real browser. * - * @AfterStep @javascript + * @BeforeStep @javascript */ - public function after_step_javascript($event) { - - // If it doesn't have definition or it fails there is no need to check it. - if ($event->getResult() != StepEvent::PASSED || - !$event->hasDefinition()) { - return; - } - - // Wait until the page is ready. - // We are already checking that we use a JS browser, this could - // change in case we use another JS driver. - try { - - // Safari and Internet Explorer requires time between steps, - // otherwise Selenium tries to click in the previous page's DOM. - if ($this->getSession()->getDriver()->getBrowserName() == 'safari' || - $this->getSession()->getDriver()->getBrowserName() == 'internet explorer') { - $this->getSession()->wait(self::TIMEOUT * 1000, false); - - } else { - // With other browsers we just wait for the DOM ready. - $this->getSession()->wait(self::TIMEOUT * 1000, '(document.readyState === "complete")'); + public function before_step_javascript($event) { + $lastpending = ''; + // Wait for all pending JS to complete (max 10 seconds). + for ($i = 0; $i < 100; $i++) { + $pending = ''; + try { + $pending = ($this->getSession()->evaluateScript('return (M && M.util && M.util.pending_js) ? M.util.pending_js.join(":") : "not loaded";')); + } catch (NoSuchWindow $nsw) { + // No javascript is running if there is no window right? + $pending = ''; } - - } catch (NoSuchWindow $e) { - // If we were interacting with a popup window it will not exists after closing it. - } catch (UnknownError $e) { - // Custom exception to provide more feedback about possible solutions. - $this->throw_unknown_exception($e); + if ($pending === '') { + return; + } + $lastpending = $pending; + // 0.1 seconds. + usleep(100000); } + // Timeout waiting for JS to complete. + // We could throw an exception here - as this is a likely indicator of slow JS or JS errors. } /**