MDL-41030 Behat: Add support for waiting for javascript
This is so that phantomjs (which runs faster than selenium) can pass the tests.
This commit is contained in:
committed by
David Monllao
parent
5d2b67228c
commit
6e2c417c1f
@@ -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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user