mirror of
https://github.com/moodle/moodle.git
synced 2025-08-08 02:16:41 +02:00
Merge branch 'MDL-42625_master' of git://github.com/dmonllao/moodle
This commit is contained in:
commit
ebc77165a4
51 changed files with 991 additions and 335 deletions
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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")');
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue