Merge branch 'MDL-42625_master' of git://github.com/dmonllao/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2013-12-10 23:29:06 +01:00
commit ebc77165a4
51 changed files with 991 additions and 335 deletions

View file

@ -55,7 +55,17 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
/**
* The timeout for each Behat step (load page, wait for an element to load...).
*/
const TIMEOUT = 6;
const TIMEOUT = 3;
/**
* And extended timeout for specific cases.
*/
const EXTENDED_TIMEOUT = 10;
/**
* The JS code to check that the page is ready.
*/
const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
/**
* Locates url, based on provided path.
@ -420,4 +430,194 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
}
/**
* Spins around an element until it exists
*
* @throws ExpectationException
* @param string $element
* @param string $selectortype
* @return void
*/
protected function ensure_element_exists($element, $selectortype) {
// Getting the behat selector & locator.
list($selector, $locator) = $this->transform_selector($selectortype, $element);
// Exception if it timesout and the element is still there.
$msg = 'The "' . $element . '" element does not exist and should exist';
$exception = new ExpectationException($msg, $this->getSession());
// It will stop spinning once the find() method returns true.
$this->spin(
function($context, $args) {
// We don't use behat_base::find as it is already spinning.
if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
return true;
}
return false;
},
array('selector' => $selector, 'locator' => $locator),
self::EXTENDED_TIMEOUT,
$exception,
true
);
}
/**
* Spins until the element does not exist
*
* @throws ExpectationException
* @param string $element
* @param string $selectortype
* @return void
*/
protected function ensure_element_does_not_exist($element, $selectortype) {
// Getting the behat selector & locator.
list($selector, $locator) = $this->transform_selector($selectortype, $element);
// Exception if it timesout and the element is still there.
$msg = 'The "' . $element . '" element exists and should not exist';
$exception = new ExpectationException($msg, $this->getSession());
// It will stop spinning once the find() method returns false.
$this->spin(
function($context, $args) {
// We don't use behat_base::find() as we are already spinning.
if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
return true;
}
return false;
},
array('selector' => $selector, 'locator' => $locator),
self::EXTENDED_TIMEOUT,
$exception,
true
);
}
/**
* Ensures that the provided node is visible and we can interact with it.
*
* @throws ExpectationException
* @param NodeElement $node
* @return void Throws an exception if it times out without the element being visible
*/
protected function ensure_node_is_visible($node) {
if (!$this->running_javascript()) {
return;
}
// Exception if it timesout and the element is still there.
$msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible';
$exception = new ExpectationException($msg, $this->getSession());
// It will stop spinning once the isVisible() method returns true.
$this->spin(
function($context, $args) {
if ($args->isVisible()) {
return true;
}
return false;
},
$node,
self::EXTENDED_TIMEOUT,
$exception,
true
);
}
/**
* Ensures that the provided element is visible and we can interact with it.
*
* Returns the node in case other actions are interested in using it.
*
* @throws ExpectationException
* @param string $element
* @param string $selectortype
* @return NodeElement Throws an exception if it times out without being visible
*/
protected function ensure_element_is_visible($element, $selectortype) {
if (!$this->running_javascript()) {
return;
}
$node = $this->get_selected_node($selectortype, $element);
$this->ensure_node_is_visible($node);
return $node;
}
/**
* Ensures that all the page's editors are loaded.
*
* This method is expensive as it waits for .mceEditor CSS
* so use with caution and only where there will be editors.
*
* @throws ElementNotFoundException
* @throws ExpectationException
* @return void
*/
protected function ensure_editors_are_loaded() {
if (!$this->running_javascript()) {
return;
}
// If there are no editors we don't need to wait.
try {
$this->find('css', '.mceEditor');
} catch (ElementNotFoundException $e) {
return;
}
// Exception if it timesout and the element is not appearing.
$msg = 'The editors are not completely loaded';
$exception = new ExpectationException($msg, $this->getSession());
// Here we know that there are .mceEditor editors in the page and we will
// probably need to interact with them, if we use tinyMCE JS var before
// it exists it will throw an exception and we want to catch it until all
// the page's editors are ready to interact with them.
$this->spin(
function($context) {
// It may return 0 if tinyMCE is loaded but not the instances, so we just loop again.
$neditors = $context->getSession()->evaluateScript('return tinyMCE.editors.length;');
if ($neditors == 0) {
return false;
}
// It may be there but not ready.
$iframeready = $context->getSession()->evaluateScript('
var readyeditors = new Array;
for (editorid in tinyMCE.editors) {
if (tinyMCE.editors[editorid].getDoc().readyState === "complete") {
readyeditors[editorid] = editorid;
}
}
if (tinyMCE.editors.length === readyeditors.length) {
return "complete";
}
return "";
');
// Now we know that the editors are there.
if ($iframeready) {
return true;
}
// Loop again if it is not ready.
return false;
},
false,
self::EXTENDED_TIMEOUT,
$exception,
true
);
}
}

View file

@ -93,6 +93,7 @@ class behat_files extends behat_base {
$classname = 'fp-file-' . $action;
$button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
$this->ensure_node_is_visible($button);
$button->click();
}
@ -148,13 +149,14 @@ class behat_files extends behat_base {
$locatorprefix .
"//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
"[normalize-space(.)=$name]" .
"//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]",
"//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-field ')]",
false,
$containernode
);
}
// Click opens the contextual menu when clicking on files.
$this->ensure_node_is_visible($node);
$node->click();
}
@ -179,6 +181,7 @@ class behat_files extends behat_base {
// Otherwise should be a single-file filepicker form element.
$add = $this->find('css', 'input.fp-btn-choose', $exception, $filemanagernode);
}
$this->ensure_node_is_visible($add);
$add->click();
// Getting the repository link and opening it.
@ -197,12 +200,16 @@ class behat_files extends behat_base {
);
// Selecting the repo.
$this->ensure_node_is_visible($repositorylink);
$repositorylink->click();
}
/**
* Waits until the file manager modal windows are closed.
*
* This method is not used by any of our step definitions,
* keeping it here for users already using it.
*
* @throws ExpectationException
* @return void
*/
@ -220,6 +227,9 @@ class behat_files extends behat_base {
/**
* Checks that the file manager contents are not being updated.
*
* This method is not used by any of our step definitions,
* keeping it here for users already using it.
*
* @throws ExpectationException
* @param NodeElement $filepickernode The file manager DOM node
* @return void
@ -243,9 +253,6 @@ class behat_files extends behat_base {
$exception,
$filepickernode
);
// After removing the class FileManagerHelper.view_files() performs other actions.
$this->getSession()->wait(4 * 1000, false);
}
}

View file

@ -48,19 +48,38 @@ class behat_form_editor extends behat_form_field {
*/
public function set_value($value) {
// Get tinyMCE editor id if it exists.
if ($editorid = $this->get_editor_id()) {
$lastexception = null;
// Set the value to the iframe and save it to the textarea.
$this->session->executeScript('
tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
tinyMCE.get("'.$editorid.'").save();
');
// We want the editor to be ready, otherwise the value can not
// be set and an exception is thrown.
for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
try {
// Get tinyMCE editor id if it exists.
if ($editorid = $this->get_editor_id()) {
} else {
// Set the value to a textarea otherwise.
parent::set_value($value);
// Set the value to the iframe and save it to the textarea.
$this->session->executeScript('
tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
tinyMCE.get("'.$editorid.'").save();
');
} else {
// Set the value to a textarea otherwise.
parent::set_value($value);
}
return;
} catch (Exception $e) {
// Catching any kind of exception and ignoring it until times out.
$lastexception = $e;
// Waiting 0.1 seconds.
usleep(100000);
}
}
// If it is not available we throw the last exception.
throw $lastexception;
}
/**
@ -70,14 +89,45 @@ class behat_form_editor extends behat_form_field {
*/
public function get_value() {
// Get tinyMCE editor id if it exists.
if ($editorid = $this->get_editor_id()) {
// Can be be a string value or an exception depending whether the editor loads or not.
$lastoutcome = '';
// Save the current iframe value in case default value has been edited.
$this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
// We want the editor to be ready to return the correct value, sometimes the
// page loads too fast and the returned value may be '' if the editor didn't
// have enough time to load completely despite having a different value.
for ($i = 0; $i < behat_base::EXTENDED_TIMEOUT; $i++) {
try {
// Get tinyMCE editor id if it exists.
if ($editorid = $this->get_editor_id()) {
// Save the current iframe value in case default value has been edited.
$this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
}
$lastoutcome = $this->field->getValue();
// We only want to wait until it times out if the value is empty.
if ($lastoutcome != '') {
return $lastoutcome;
}
} catch (Exception $e) {
// Catching any kind of exception and ignoring it until times out.
$lastoutcome = $e;
// Waiting 0.1 seconds.
usleep(100000);
}
}
return $this->field->getValue();
// If it is not available we throw the last exception.
if (is_a($lastoutcome, 'Exception')) {
throw $lastoutcome;
}
// Return the value if there are no exceptions it will be '' at this point
return $lastoutcome;
}
/**
@ -87,7 +137,7 @@ class behat_form_editor extends behat_form_field {
* can not execute Javascript, also some Moodle settings disables the HTML
* editor.
*
* @return mixed The id of the editor of false if is not available
* @return mixed The id of the editor of false if it is not available
*/
protected function get_editor_id() {
@ -95,7 +145,7 @@ class behat_form_editor extends behat_form_field {
try {
$available = $this->session->evaluateScript('return (typeof tinyMCE != "undefined")');
// Also checking that it exist a tinyMCE editor for the requested field.
// Also checking that it exists a tinyMCE editor for the requested field.
$editorid = $this->field->getAttribute('id');
$available = $this->session->evaluateScript('return (typeof tinyMCE.get("'.$editorid.'") != "undefined")');

View file

@ -137,6 +137,7 @@ class behat_form_field {
$classname = 'behat_form_select';
} else {
// We can not provide a closer field type.
return false;
}
@ -154,4 +155,24 @@ class behat_form_field {
return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
}
/**
* Gets the field internal id used by selenium wire protocol.
*
* Only available when running_javascript().
*
* @throws coding_exception
* @return int
*/
protected function get_internal_field_id() {
if (!$this->running_javascript()) {
throw new coding_exception('You can only get an internal ID using the selenium driver.');
}
return $this->session->
getDriver()->
getWebDriverSession()->
element('xpath', $this->field->getXPath())->
getID();
}
}

View file

@ -40,40 +40,82 @@ class behat_form_select extends behat_form_field {
/**
* Sets the value of a single select.
*
* Seems an easy select, but there are lots of combinations
* of browsers and operative systems and each one manages the
* autosubmits and the multiple option selects in a diferent way.
*
* @param string $value
* @return void
*/
public function set_value($value) {
// In some browsers we select an option and it triggers all the
// autosubmits and works as expected but not in all of them, so we
// try to catch all the possibilities to make this function work as
// expected.
// Get the internal id of the element we are going to click.
// This kind of internal IDs are only available in the selenium wire
// protocol, so only available using selenium drivers, phantomjs and family.
if ($this->running_javascript()) {
$currentelementid = $this->get_internal_field_id();
}
// Here we select an option.
$this->field->selectOption($value);
// Adding a click as Selenium requires it to fire some JS events.
if ($this->running_javascript()) {
// With JS disabled this is enough and we finish here.
if (!$this->running_javascript()) {
return;
}
// In some browsers the selectOption actions can perform a page reload
// so we need to ensure the element is still available to continue interacting
// with it. We don't wait here.
if (!$this->session->getDriver()->find($this->field->getXpath())) {
// With JS enabled we add more clicks as some selenium
// drivers requires it to fire JS events.
// In some browsers the selectOption actions can perform a form submit or reload page
// so we need to ensure the element is still available to continue interacting
// with it. We don't wait here.
$selectxpath = $this->field->getXpath();
if (!$this->session->getDriver()->find($selectxpath)) {
return;
}
// We also check the selenium internal element id, if it have changed
// we are dealing with an autosubmit that was already executed, and we don't to
// execute anything else as the action we wanted was already performed.
if ($currentelementid != $this->get_internal_field_id()) {
return;
}
// We also check that the option is still there. We neither wait.
$valueliteral = $this->session->getSelectorsHandler()->xpathLiteral($value);
$optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
if (!$this->session->getDriver()->find($optionxpath)) {
return;
}
// Single select sometimes needs an extra click in the option.
if (!$this->field->hasAttribute('multiple')) {
// Using the driver direcly because Element methods are messy when dealing
// with elements inside containers.
$optionnodes = $this->session->getDriver()->find($optionxpath);
if ($optionnodes) {
current($optionnodes)->click();
}
} else {
// Multiple ones needs the click in the select.
$this->field->click();
// We ensure that the option is still there.
if (!$this->session->getDriver()->find($optionxpath)) {
return;
}
// Single select needs an extra click in the option.
if (!$this->field->hasAttribute('multiple')) {
$value = $this->session->getSelectorsHandler()->xpathLiteral($value);
// Using the driver direcly because Element methods are messy when dealing
// with elements inside containers.
$optionxpath = $this->field->getXpath() .
"/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
$optionnodes = $this->session->getDriver()->find($optionxpath);
if ($optionnodes) {
current($optionnodes)->click();
}
} else {
// Multiple ones needs the click in the select.
$this->field->click();
}
// Repeating the select as some drivers (chrome that I know) are moving
// to another option after the general select field click above.
$this->field->selectOption($value);
}
}

View file

@ -43,6 +43,8 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
};
M.editor_tinymce.initialised = true;
M.util.js_pending('editors');
options.oninit = "M.editor_tinymce.init_callback";
}
M.editor_tinymce.editor_options[editorid] = options;
@ -86,6 +88,10 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
}
};
M.editor_tinymce.init_callback = function() {
M.util.js_complete('editors');
}
M.editor_tinymce.init_filepicker = function(Y, editorid, options) {
M.editor_tinymce.filepicker_options[editorid] = options;
};

View file

@ -45,6 +45,7 @@ Feature: Add or remove items from the TinyMCE editor toolbar
Given I follow "Course 1"
And I turn editing mode on
When I add a "Database" to section "1"
And I wait until "#id_introeditor_tbl" "css_element" exists
Then "#id_introeditor_tbl .mce_bold" "css_element" should exists
And "#id_introeditor_tbl .mce_anchor" "css_element" should not exists
And I press "Cancel"

View 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
*

View 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;
@ -1453,6 +1457,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) {

View file

@ -68,6 +68,7 @@ class behat_deprecated extends behat_base {
// Looking for the element DOM node inside the specified row.
list($selector, $locator) = $this->transform_selector($selectortype, $element);
$elementnode = $this->find($selector, $locator, false, $rownode);
$this->ensure_element_is_visible($elementnode);
$elementnode->click();
}

View file

@ -69,6 +69,9 @@ class behat_forms extends behat_base {
*/
public function i_fill_the_moodle_form_with(TableNode $data) {
// We ensure that all the editors are loaded and we can interact with them.
$this->ensure_editors_are_loaded();
// Expand all fields in case we have.
$this->expand_all_fields();
@ -171,31 +174,11 @@ class behat_forms extends behat_base {
public function select_option($option, $select) {
$selectnode = $this->find_field($select);
$selectnode->selectOption($option);
// Adding a click as Selenium requires it to fire some JS events.
if ($this->running_javascript()) {
// In some browsers the selectOption actions can perform a page reload
// so we need to ensure the element is still available to continue interacting
// with it. We don't wait here.
if (!$this->getSession()->getDriver()->find($selectnode->getXpath())) {
return;
}
// Single select needs an extra click in the option.
if (!$selectnode->hasAttribute('multiple')) {
// Avoid quotes problems.
$option = $this->getSession()->getSelectorsHandler()->xpathLiteral($option);
$xpath = "//option[(./@value=$option or normalize-space(.)=$option)]";
$optionnode = $this->find('xpath', $xpath, false, $selectnode);
$optionnode->click();
} else {
// Multiple ones needs the click in the select.
$selectnode->click();
}
}
// We delegate to behat_form_field class, it will
// guess the type properly as it is a select tag.
$selectformfield = behat_field_manager::get_form_field($selectnode, $this->getSession());
$selectformfield->set_value($option);
}
/**
@ -225,6 +208,8 @@ class behat_forms extends behat_base {
*/
public function check_option($option) {
// We don't delegate to behat_form_checkbox as the
// step is explicitly saying I check.
$checkboxnode = $this->find_field($option);
$checkboxnode->check();
}
@ -238,6 +223,8 @@ class behat_forms extends behat_base {
*/
public function uncheck_option($option) {
// We don't delegate to behat_form_checkbox as the
// step is explicitly saying I uncheck.
$checkboxnode = $this->find_field($option);
$checkboxnode->uncheck();
}

View file

@ -124,7 +124,20 @@ class behat_general extends behat_base {
* @param string $iframename
*/
public function switch_to_iframe($iframename) {
$this->getSession()->switchToIFrame($iframename);
// We spin to give time to the iframe to be loaded.
// Using extended timeout as we don't know about which
// kind of iframe will be loaded.
$this->spin(
function($context, $iframename) {
$context->getSession()->switchToIFrame($iframename);
// If no exception we are done.
return true;
},
$iframename,
self::EXTENDED_TIMEOUT
);
}
/**
@ -173,6 +186,7 @@ class behat_general extends behat_base {
public function click_link($link) {
$linknode = $this->find_link($link);
$this->ensure_node_is_visible($linknode);
$linknode->click();
}
@ -202,7 +216,56 @@ class behat_general extends behat_base {
throw new DriverException('Waits are disabled in scenarios without Javascript support');
}
$this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
$this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
}
/**
* Waits until the editors are all completely loaded.
*
* @Given /^I wait until the editors are loaded$/
* @throws DriverException
*/
public function wait_until_editors_are_loaded() {
if (!$this->running_javascript()) {
throw new DriverException('Editors are not loaded when running without Javascript support');
}
$this->ensure_editors_are_loaded();
}
/**
* Waits until the provided element selector exists in the DOM
*
* Using the protected method as this method will be usually
* called by other methods which are not returning a set of
* steps and performs the actions directly, so it would not
* be executed if it returns another step.
* @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
* @param string $element
* @param string $selector
* @return void
*/
public function wait_until_exists($element, $selectortype) {
$this->ensure_element_exists($element, $selectortype);
}
/**
* Waits until the provided element does not exist in the DOM
*
* Using the protected method as this method will be usually
* called by other methods which are not returning a set of
* steps and performs the actions directly, so it would not
* be executed if it returns another step.
* @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
* @param string $element
* @param string $selector
* @return void
*/
public function wait_until_does_not_exists($element, $selectortype) {
$this->ensure_element_does_not_exist($element, $selectortype);
}
/**
@ -230,6 +293,7 @@ class behat_general extends behat_base {
// Gets the node based on the requested selector type and locator.
$node = $this->get_selected_node($selectortype, $element);
$this->ensure_node_is_visible($node);
$node->click();
}
@ -245,6 +309,7 @@ class behat_general extends behat_base {
public function i_click_on_in_the($element, $selectortype, $nodeelement, $nodeselectortype) {
$node = $this->get_node_in_container($selectortype, $element, $nodeselectortype, $nodeelement);
$this->ensure_node_is_visible($node);
$node->click();
}
@ -298,6 +363,9 @@ class behat_general extends behat_base {
/**
* Checks, that the specified element is not visible. Only available in tests using Javascript.
*
* As a "not" method, it's performance is not specially good as we should ensure that the element
* have time to appear.
*
* @Then /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" should not be visible$/
* @throws ElementNotFoundException
* @throws ExpectationException
@ -381,24 +449,35 @@ class behat_general extends behat_base {
$xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
"[count(descendant::*[contains(., $xpathliteral)]) = 0]";
// Wait until it finds the text, otherwise custom exception.
try {
$nodes = $this->find_all('xpath', $xpath);
// We also check for the element visibility when running JS tests.
if ($this->running_javascript()) {
foreach ($nodes as $node) {
if ($node->isVisible()) {
return;
}
}
throw new ExpectationException("'{$text}' text was found but was not visible", $this->getSession());
}
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $text . '" text was not found in the page', $this->getSession());
}
// If we are not running javascript we have enough with the
// element existing as we can't check if it is visible.
if (!$this->running_javascript()) {
return;
}
// We spin as we don't have enough checking that the element is there, we
// should also ensure that the element is visible.
$this->spin(
function($context, $args) {
foreach ($args['nodes'] as $node) {
if ($node->isVisible()) {
return true;
}
}
// If non of the nodes is visible we loop again.
throw new ExpectationException('"' . $args['text'] . '" text was found but was not visible', $context->getSession());
},
array('nodes' => $nodes, 'text' => $text)
);
}
/**
@ -410,16 +489,43 @@ class behat_general extends behat_base {
*/
public function assert_page_not_contains_text($text) {
// Delegating the process to assert_page_contains_text.
// Looking for all the matching nodes without any other descendant matching the
// same xpath (we are using contains(., ....).
$xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
$xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
"[count(descendant::*[contains(., $xpathliteral)]) = 0]";
// We should wait a while to ensure that the page is not still loading elements.
// Giving preference to the reliability of the results rather than to the performance.
try {
$this->assert_page_contains_text($text);
} catch (ExpectationException $e) {
// It should not appear, so this is good.
$nodes = $this->find_all('xpath', $xpath);
} catch (ElementNotFoundException $e) {
// All ok.
return;
}
// If the page contains the text this is failing.
throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
// If we are not running javascript we have enough with the
// element existing as we can't check if it is hidden.
if (!$this->running_javascript()) {
throw new ExpectationException('"' . $text . '" text was found in the page', $this->getSession());
}
// If the element is there we should be sure that it is not visible.
$this->spin(
function($context, $args) {
foreach ($args['nodes'] as $node) {
if ($node->isVisible()) {
throw new ExpectationException('"' . $args['text'] . '" text was found in the page', $context->getSession());
}
}
// If non of the found nodes is visible we consider that the text is not visible.
return true;
},
array('nodes' => $nodes, 'text' => $text)
);
}
/**
@ -446,22 +552,30 @@ class behat_general extends behat_base {
// Wait until it finds the text inside the container, otherwise custom exception.
try {
$nodes = $this->find_all('xpath', $xpath, false, $container);
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $text . '" text was not found in the "' . $element . '" element', $this->getSession());
}
// We also check for the element visibility when running JS tests.
if ($this->running_javascript()) {
foreach ($nodes as $node) {
// If we are not running javascript we have enough with the
// element existing as we can't check if it is visible.
if (!$this->running_javascript()) {
return;
}
// We also check the element visibility when running JS tests.
$this->spin(
function($context, $args) {
foreach ($args['nodes'] as $node) {
if ($node->isVisible()) {
return;
return true;
}
}
throw new ExpectationException("'{$text}' text was found in the {$element} element but was not visible", $this->getSession());
}
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $text . '" text was not found in the ' . $element . ' element', $this->getSession());
}
throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element but was not visible', $context->getSession());
},
array('nodes' => $nodes, 'text' => $text, 'element' => $element)
);
}
/**
@ -476,18 +590,45 @@ class behat_general extends behat_base {
*/
public function assert_element_not_contains_text($text, $element, $selectortype) {
// Delegating the process to assert_element_contains_text.
// Getting the container where the text should be found.
$container = $this->get_selected_node($selectortype, $element);
// Looking for all the matching nodes without any other descendant matching the
// same xpath (we are using contains(., ....).
$xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text);
$xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
"[count(descendant::*[contains(., $xpathliteral)]) = 0]";
// We should wait a while to ensure that the page is not still loading elements.
// Giving preference to the reliability of the results rather than to the performance.
try {
$this->assert_element_contains_text($text, $element, $selectortype);
} catch (ExpectationException $e) {
// It should not appear, so this is good.
// We only catch ExpectationException as ElementNotFoundException
// will be thrown if the container does not exist.
$nodes = $this->find_all('xpath', $xpath, false, $container);
} catch (ElementNotFoundException $e) {
// All ok.
return;
}
// If the element contains the text this is failing.
throw new ExpectationException('"' . $text . '" text was found in the ' . $element . ' element', $this->getSession());
// If we are not running javascript we have enough with the
// element not being found as we can't check if it is visible.
if (!$this->running_javascript()) {
throw new ExpectationException('"' . $text . '" text was found in the "' . $element . '" element', $this->getSession());
}
// We need to ensure all the found nodes are hidden.
$this->spin(
function($context, $args) {
foreach ($args['nodes'] as $node) {
if ($node->isVisible()) {
throw new ExpectationException('"' . $args['text'] . '" text was found in the "' . $args['element'] . '" element', $context->getSession());
}
}
// If all the found nodes are hidden we are happy.
return true;
},
array('nodes' => $nodes, 'text' => $text, 'element' => $element)
);
}
/**

View file

@ -222,42 +222,68 @@ class behat_hooks extends behat_base {
}
/**
* Checks that all DOM is ready.
* Wait for JS to complete before beginning interacting with the DOM.
*
* Executed only when running against a real browser.
*
* @BeforeStep @javascript
*/
public function before_step_javascript($event) {
$this->wait_for_pending_js();
}
/**
* Wait for JS to complete after finishing the step.
*
* With this we ensure that there are not AJAX calls
* still in progress.
*
* Executed only when running against a real browser.
*
* @AfterStep @javascript
*/
public function after_step_javascript($event) {
$this->wait_for_pending_js();
}
// If it doesn't have definition or it fails there is no need to check it.
if ($event->getResult() != StepEvent::PASSED ||
!$event->hasDefinition()) {
return;
}
/**
* Waits for all the JS to be loaded.
*
* @throws NoSuchWindow
* @throws UnknownError
* @return bool True or false depending whether all the JS is loaded or not.
*/
protected function wait_for_pending_js() {
// 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")');
// 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 ' . self::PAGE_READY_JS . ' ? "" : 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) {
// Same exception as before, but some combinations of browser + OS reports it as an unknown error
// exception.
$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 there are no pending JS we stop waiting.
if ($pending === '') {
return true;
}
// 0.1 seconds.
usleep(100000);
}
// Timeout waiting for JS to complete.
// TODO MDL-43173 We should fail the scenarios if JS loading times out.
return false;
}
/**

View file

@ -76,6 +76,7 @@ class behat_navigation extends behat_base {
$exception = new ExpectationException('The "' . $nodetext . '" node can not be expanded', $this->getSession());
$node = $this->find('xpath', $xpath, $exception);
$this->ensure_node_is_visible($node);
$node->click();
}

View file

@ -95,7 +95,16 @@ class behat_permissions extends behat_base {
try {
$advancedtoggle = $this->find_button(get_string('showadvanced', 'form'));
if ($advancedtoggle) {
$this->getSession()->getPage()->pressButton(get_string('showadvanced', 'form'));
// As we are interacting with a moodle form we wait for the editor to be ready
// otherwise we may have problems when setting values on it or clicking on elements
// as the position of the elements will change once the editor is loaded.
$this->ensure_editors_are_loaded();
$advancedtoggle->click();
// Wait for the page to load.
$this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
}
} catch (Exception $e) {
// We already are in advanced mode.