This is so that phantomjs (which runs faster than selenium) can pass the tests.
});
};
+/**
+ * @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
*
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;
$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;
// 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) {
$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.
*
$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);
}
}
}
/**
- * 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.
}
/**