MDL-41030 Behat: Add support for waiting for javascript
authorDamyon Wiese <damyon@moodle.com>
Tue, 29 Oct 2013 02:36:30 +0000 (10:36 +0800)
committerDavid Monllao <davidm@moodle.com>
Fri, 6 Dec 2013 04:13:39 +0000 (12:13 +0800)
This is so that phantomjs (which runs faster than selenium) can pass the tests.

lib/javascript-static.js
lib/outputrequirementslib.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_hooks.php

index 1c1e19c..df36acd 100644 (file)
@@ -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
  *
index d530d79..7519a31 100644 (file)
@@ -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) {
index b33931f..e5eaa84 100644 (file)
@@ -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);
         }
     }
 
index 5789134..e18e48d 100644 (file)
@@ -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.
     }
 
     /**