// We expect admin block to be visible, otherwise go to homepage.
if (!$this->getSession()->getPage()->find('css', '.block_settings')) {
$this->getSession()->visit($this->locate_path('/'));
- $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
}
// Search by label.
$submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
$submitsearch->press();
- $this->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Admin settings does not use the same DOM structure than other moodle forms
// but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
$this->find_button(get_string('backupstage4action', 'backup'))->press();
// Waiting for it to finish.
- $this->wait(10);
+ $this->wait();
// Last backup continue button.
$this->find_button(get_string('backupstage16action', 'backup'))->press();
// Review, no options here.
$this->find_button(get_string('restorestage16action', 'backup'))->press();
- $this->wait(10);
+ $this->wait();
// Last restore continue button, redirected to restore course after this.
$this->find_button(get_string('restorestage32action', 'backup'))->press();
/**
* Waits until the DOM is ready.
*
- * @param int To override the default timeout
* @return void
*/
- protected function wait($timeout = false) {
+ protected function wait() {
if (!$this->running_javascript()) {
return;
}
- if (!$timeout) {
- $timeout = self::TIMEOUT;
- }
- $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
}
}
And I add a "Database" to section "1" and I fill the form with:
| Name | Test database name |
| Description | Test database description |
- And I open "Test database name" actions menu
- When I click on "Duplicate" "link" in the "Test database name" activity
+ And I duplicate "Test database name" activity
+ And I wait until section "1" is available
And I open "Test database name" actions menu
And I click on "Edit settings" "link" in the "Test database name" activity
And I fill the moodle form with:
As an admin
I need to add criteria to badges in the system
- Background:
- Given I am on homepage
- And I log in as "admin"
-
@javascript
Scenario: Award profile badge
- Given I expand "Site administration" node
+ Given I log in as "admin"
+ And I expand "Site administration" node
And I expand "Badges" node
And I follow "Add a new badge"
And I fill the moodle form with:
| username | firstname | lastname | email |
| teacher | teacher | 1 | teacher1@asd.com |
| student | student | 1 | student1@asd.com |
+ And I log in as "admin"
And I expand "Site administration" node
And I expand "Badges" node
And I follow "Add a new badge"
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
- And I log out
And I log in as "teacher1"
And I follow "Course 1"
And I click on "//span[text()='Badges']" "xpath_element" in the "Administration" "block"
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- And I log out
And I log in as "admin"
And I set the following administration settings values:
| Enable completion tracking | 1 |
And I follow "Home"
And I follow "Course 1"
And I press "Mark as complete: Test assignment name"
- And I wait "2" seconds
And I expand "My profile" node
And I follow "My badges"
Then I should see "Course Badge"
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
- And I log out
And I log in as "admin"
And I set the following administration settings values:
| Enable completion tracking | 1 |
$this->find_link(get_string('savecomment'))->click();
- // Wait for the AJAX request.
- $this->getSession()->wait(4 * 1000, false);
-
} else {
$commentstextarea = $this->find('css', '.block_comments form textarea', $exception);
$deleteicon = $this->find('css', '.comment-delete a img', $deleteexception, $commentnode);
$deleteicon->click();
- // Wait for the AJAX request.
+ // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
$this->getSession()->wait(4 * 1000, false);
}
And I follow "Save comment"
When I click on ".comment-delete a" "css_element"
# Waiting for the animation to finish.
- And I wait "2" seconds
+ And I wait "4" seconds
Then I should not see "$My own >nasty< \"string\"!"
And I follow "Blog post from user 1"
And I click on ".comment-link" "css_element"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
-
- @javascript
- Scenario: Show activity greyed-out to students when available from date is in future
- Given I log in as "teacher1"
+ And I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
+ # Adding the page like this because id_available*_enabled needs to be clicked to trigger the action.
And I add a "Assignment" to section "1"
+ And I expand all fieldsets
+
+ @javascript
+ Scenario: Show activity greyed-out to students when available from date is in future
+ Given I click on "id_availablefrom_enabled" "checkbox"
And I fill the moodle form with:
| Assignment name | Test assignment 1 |
| Description | This assignment is restricted by date |
| id_availablefrom_month | 12 |
| id_availablefrom_year | 2050 |
| id_showavailability | 1 |
- And I click on "id_availablefrom_enabled" "checkbox"
And I press "Save and return to course"
And I log out
When I log in as "student1"
@javascript
Scenario: Show activity hidden to students when available until date is in past
- Given I log in as "teacher1"
- And I follow "Course 1"
- And I turn editing mode on
- And I add a "Assignment" to section "2"
+ Given I click on "id_availableuntil_enabled" "checkbox"
And I fill the moodle form with:
| Assignment name | Test assignment 2 |
| Description | This assignment is restricted by date |
| id_availableuntil_month | 2 |
| id_availableuntil_year | 2013 |
| id_showavailability | 0 |
- And I click on "id_availableuntil_enabled" "checkbox"
And I press "Save and return to course"
And I log out
When I log in as "student1"
| Description | Grade this assignment to revoke restriction on restricted assignment |
| assignsubmission_onlinetext_enabled | 1 |
| assignsubmission_file_enabled | 0 |
- And I add a "Page" to section "2" and I fill the form with:
+ # Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action.
+ And I add a "Page" to section "2"
+ And I expand all fieldsets
+ And I click on "id_availablefrom_enabled" "checkbox"
+ And I fill the moodle form with:
| Name | Test page name |
| Description | Restricted page, till grades in Grade assignment is at least 20% |
| Page content | Test page contents |
| id_conditiongradegroup_0_conditiongradeitemid | 2 |
| id_conditiongradegroup_0_conditiongrademin | 20 |
| id_showavailability | 1 |
+ And I press "Save and return to course"
And I log out
When I log in as "student1"
And I follow "Course 1"
Scenario: Add an activity without the required fields
When I add a "Database" to section "3" and I fill the form with:
| Name | Test name |
+ And I press "Save and return to course"
Then I should see "Adding a new"
And I should see "Required"
+ And I press "Cancel"
Scenario: Add an activity to a course with Javascript disabled
Then I should see "Add a resource to section 'Topic 1'"
* @return Given[]
*/
public function i_create_a_course_with(TableNode $table) {
- return array(
+
+ $steps = array(
new Given('I go to the courses management page'),
new Given('I should see the "'.get_string('categories').'" management page'),
new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
- new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"'),
- new Given('I fill the moodle form with:', $table),
- new Given('I press "' . get_string('savechanges') . '"')
+ new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"')
);
+
+ // If the course format is one of the fields we change how we
+ // fill the form as we need to wait for the form to be set.
+ $rowshash = $table->getRowsHash();
+ $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
+ foreach ($formatfieldrefs as $fieldref) {
+ if (!empty($rowshash[$fieldref])) {
+ $formatfield = $fieldref;
+ }
+ }
+
+ // Setting the format separately.
+ if (!empty($formatfield)) {
+
+ // Removing the format field from the TableNode.
+ $rows = $table->getRows();
+ $formatvalue = $rowshash[$formatfield];
+ foreach ($rows as $key => $row) {
+ if ($row[0] == $formatfield) {
+ unset($rows[$key]);
+ }
+ }
+ $table->setRows($rows);
+
+ $steps[] = new Given('I fill the moodle form with:', $table);
+ $steps[] = new Given('I select "' . $formatvalue . '" from "' . $formatfield . '"');
+ $steps[] = new Given('I wait until the page is ready');
+ } else {
+ $steps[] = new Given('I fill the moodle form with:', $table);
+ }
+
+ $steps[] = new Given('I press "' . get_string('savechanges') . '"');
+
+ return $steps;
}
/**
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
- return array(
- new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
- new Given('I wait "2" seconds')
- );
+ return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
/**
// Ensures the section exists.
$xpath = $this->section_exists($sectionnumber);
- return array(
- new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
- new Given('I wait "2" seconds')
- );
+ return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
}
/**
$showlink = $this->show_section_icon_exists($sectionnumber);
$showlink->click();
- // It requires time.
if ($this->running_javascript()) {
- $this->getSession()->wait(5000, false);
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+ $this->i_wait_until_section_is_available($sectionnumber);
}
}
$hidelink = $this->hide_section_icon_exists($sectionnumber);
$hidelink->click();
- // It requires time.
if ($this->running_javascript()) {
- $this->getSession()->wait(5000, false);
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+ $this->i_wait_until_section_is_available($sectionnumber);
}
}
$sectionxpath = $this->section_exists($sectionnumber);
+ // Preventive in case there is any action in progress.
+ // Adding it here because we are interacting (click) with
+ // the elements, not necessary when we just find().
+ $this->i_wait_until_section_is_available($sectionnumber);
+
// Section should be hidden.
$exception = new ExpectationException('The section is not hidden', $this->getSession());
$this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
// Non-JS browsers can not click on img elements.
if ($this->running_javascript()) {
- // Expanding the actions menu.
- $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
- $actionsmenu->click();
+ // Expanding the actions menu if it is not shown.
+ $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+ if (empty($classes['action-menu-shown'])) {
+ $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+ $actionsmenu->click();
+ }
// To check that the visibility is not clickable we check the funcionality rather than the applied style.
$visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
// We ensure that we still see the show icon.
$visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
+
+ // It is there only when running JS scenarios.
+ if ($this->running_javascript()) {
+
+ // Collapse the actions menu if it is displayed.
+ $classes = array_flip(explode(' ', $activity->getAttribute('class')));
+ if (!empty($classes['action-menu-shown'])) {
+ $actionsmenu = $this->find('css', "a[role='menuitem']", false, $activity);
+ $actionsmenu->click();
+ }
+ }
}
}
$activity = $this->escape($activityname);
return array(
new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
- new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
- new Given('I wait "2" seconds')
+ new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"')
);
}
return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
}
+ /**
+ * Closes an activity actions menu if it is not already closed.
+ *
+ * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
+ * @throws DriverException The step is not available when Javascript is disabled
+ * @param string $activityname
+ * @return Given
+ */
+ public function i_close_actions_menu($activityname) {
+
+ if (!$this->running_javascript()) {
+ throw new DriverException('Activities actions menu not available when Javascript is disabled');
+ }
+
+ // If it is already closed we do nothing.
+ $activitynode = $this->get_activity_node($activityname);
+ $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
+ if (empty($classes['action-menu-shown'])) {
+ return;
+ }
+
+ return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
+ }
+
/**
* Indents to the right the activity or resource specified by it's name. Editing mode should be on.
*
}
$steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- $steps[] = new Given('I wait "2" seconds');
- }
-
return $steps;
}
}
$steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- $steps[] = new Given('I wait "2" seconds');
- }
-
return $steps;
}
$this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
- $this->getSession()->wait(2 * 1000, false);
-
} else {
// With JS disabled.
$steps[] = new Given('I open "' . $activity . '" actions menu');
}
$steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
- if ($this->running_javascript()) {
- // Temporary wait until MDL-41030 lands.
- $steps[] = new Given('I wait "4" seconds');
- } else {
+ if (!$this->running_javascript()) {
$steps[] = new Given('I press "' . get_string('continue') .'"');
$steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
}
$steps = array();
$activity = $this->escape($activityname);
+ $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
if ($this->running_javascript()) {
$steps[] = new Given('I duplicate "' . $activity . '" activity');
+ // We wait until the AJAX request finishes and the section is visible again.
+ $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
+ "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
+ "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
+ $steps[] = new Given('I wait until the page is ready');
+ $steps[] = new Given('I wait until "' . $this->escape($hiddenlightboxxpath) .'" "xpath_element" exists');
+
+ // Close the original activity actions menu.
+ $steps[] = new Given('I close "' . $activity . '" actions menu');
+
// Determine the future new activity xpath from the former one.
- $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
$duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityliteral)]" .
"/following-sibling::li";
$duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@role='menuitem']";
return $steps;
}
+ /**
+ * Waits until the section is available to interact with it. Useful when the section is performing an action and the section is overlayed with a loading layout.
+ *
+ * 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.
+ *
+ * Hopefully we would not require test writers to use this step
+ * and we will manage it from other step definitions.
+ *
+ * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
+ * @param int $sectionnumber
+ * @return void
+ */
+ public function i_wait_until_section_is_available($sectionnumber) {
+
+ // Looks for a hidden lightbox or a non-existent lightbox in that section.
+ $sectionxpath = $this->section_exists($sectionnumber);
+ $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
+ " | " .
+ $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
+
+ $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
+ }
+
/**
* Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
*
return $this->find('xpath', $xpath);
}
+ /**
+ * Gets the activity instance name from the activity node.
+ *
+ * @throws ElementNotFoundException
+ * @param NodeElement $activitynode
+ * @return string
+ */
+ protected function get_activity_name($activitynode) {
+ $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
+ return $instancenamenode->getText();
+ }
+
/**
* Returns whether the user can edit the course contents or not.
*
-@core @core_course @test
+@core @core_course
Feature: Course category management interface performs as expected
In order to test JS enhanced display of categories and subcategories.
As a moodle admin
-@core @core_course
+@core @core_course @_alerts
Feature: Course activity controls works as expected
In order to manage my course's activities
As a teacher
And I click on "Edit settings" "link" in the "Test forum name 1" activity
And I should see "Updating Forum"
And I should see "Display description on course page"
- And I press "Save and return to course"
+ And I fill the moodle form with:
+ | Forum name | Just to check that I can edit the name |
+ | Description | Just to check that I can edit the description |
+ | Display description on course page | 1 |
+ And I click on "Cancel" "button"
And "#section-2" "css_element" <should_see_other_sections> exists
And I open "Test forum name 1" actions menu
And I click on "Hide" "link" in the "Test forum name 1" activity
And "#section-2" "css_element" <should_see_other_sections> exists
+ And I close "Test forum name 1" actions menu
And I duplicate "Test forum name 2" activity editing the new copy with:
| Forum name | Edited test forum name 2 |
And "#section-2" "css_element" <should_see_other_sections> exists
$this->find_button(get_string('adduserstogroup', 'group'))->click();
// Wait for add/remove members page to be loaded.
- $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Getting the option and selecting it.
$select = $this->find_field('addselect');
$this->find_button(get_string('add'))->click();
// Wait for the page to load.
- $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")');
+ $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
// Returning to the main groups page.
$this->find_button(get_string('backtogroups', 'group'))->click();
-@core @core_group
+@core @core_group @_only_local
Feature: Importing of groups and groupings
In order to import groups and grouping
As a teacher
/**
* 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;
+
+ /**
+ * Number of retries to wait for the editor to be ready.
+ */
+ const WAIT_FOR_EDITOR_RETRIES = 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.
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 ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the element "' . $element . '" is not there';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // Will stop spinning the find() return false.
+ $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;
+ }
+ },
+ 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 ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the "' . $element . '" element is still there';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // Will stop spinning the find() return false.
+ $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;
+ }
+ },
+ 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 ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the "' . $node->getXPath() . '" element is not visible';
+ $exception = new ExpectationException($msg, $this->getSession());
+
+ // Will stop spinning the find() return false.
+ $this->spin(
+ function($context, $args) {
+ if ($args->isVisible()) {
+ return true;
+ }
+ },
+ $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
+ * @return void
+ */
+ protected function ensure_editors_are_loaded() {
+
+ if (!$this->running_javascript()) {
+ return;
+ }
+
+ $lastexception = new Exception('The editors are not properly loaded');
+
+ // If there are no editors we don't need to wait.
+ //$this->getSession()->getPage()->find('css', '.mceEditor');
+ try {
+ $this->find('css', '.mceEditor');
+ } catch (ElementNotFoundException $e) {
+ return;
+ }
+
+ // We loop until we can interact with all the page editors.
+ for ($i = 0; $i < self::WAIT_FOR_EDITOR_RETRIES; $i++) {
+
+ // 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.
+ try {
+
+ // It may return 0 if tinyMCE is loaded but not the instances, so we just loop again.
+ if ($this->getSession()->evaluateScript('return tinyMCE.editors.length;') > 0) {
+
+ // It may be there but not ready.
+ $iframeready = $this->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;
+ }
+ }
+
+ } 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;
+
+ }
}
$classname = 'fp-file-' . $action;
$button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
+ $this->ensure_node_is_visible($button);
$button->click();
}
$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();
}
// 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.
);
// 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
*/
/**
* 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
$exception,
$filepickernode
);
-
- // After removing the class FileManagerHelper.view_files() performs other actions.
- $this->getSession()->wait(4 * 1000, false);
}
}
*/
public function set_value($value) {
- // Get tinyMCE editor id if it exists.
- if ($editorid = $this->get_editor_id()) {
-
- // 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);
+ $lastexception = null;
+
+ // 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::WAIT_FOR_EDITOR_RETRIES; $i++) {
+ try {
+ // Get tinyMCE editor id if it exists.
+ if ($editorid = $this->get_editor_id()) {
+
+ // 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;
}
/**
*/
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 = '';
+
+ // 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::WAIT_FOR_EDITOR_RETRIES; $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);
+ }
+ }
- // Save the current iframe value in case default value has been edited.
- $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+ // If it is not available we throw the last exception.
+ if (is_a($lastoutcome, 'Exception')) {
+ throw $lastoutcome;
}
- return $this->field->getValue();
+ // Return the value if there are no exceptions it will be '' at this point
+ return $lastoutcome;
}
/**
$classname = 'behat_form_select';
} else {
+ // We can not provide a closer field type.
return false;
}
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();
+ }
}
/**
* 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) {
- $this->field->selectOption($value);
- // Adding a click as Selenium requires it to fire some JS events.
+ // 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();
+ }
- // 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())) {
- return;
- }
+ // Here we select an option.
+ $this->field->selectOption($value);
- // Single select needs an extra click in the option.
- if (!$this->field->hasAttribute('multiple')) {
+ // With JS disabled this is enough and we finish here.
+ if (!$this->running_javascript()) {
+ return;
+ }
- $value = $this->session->getSelectorsHandler()->xpathLiteral($value);
+ // With JS enabled we add more clicks as some selenium
+ // drivers requires it to fire JS events.
- // 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();
- }
+ // 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;
+ }
- } else {
- // Multiple ones needs the click in the select.
- $this->field->click();
+ // 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;
+ }
+
+ // 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);
}
}
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"
$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.
*
*/
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();
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
// Delegates to the field class.
- $this->set_field_value($field, $value);
+ $field->set_value($value);
}
}
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);
}
/**
*/
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();
}
*/
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();
}
* @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
+ );
}
/**
public function click_link($link) {
$linknode = $this->find_link($link);
+ $this->ensure_node_is_visible($linknode);
$linknode->click();
}
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 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);
}
/**
// 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();
}
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();
}
}
/**
- * Wait for JS to comlete.
+ * 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) {
- $lastpending = '';
+ $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();
+ }
+
+ /**
+ * 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 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";'));
+ $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 = '';
}
+
+ // If there are no pending JS we stop waiting.
if ($pending === '') {
- return;
+ return true;
}
- $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.
+ echo ' Slow JS, pending requests:' . $pending . ' ';
+ return false;
}
/**
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.
-@mod @mod_scorm
+@mod @mod_scorm @_only_local @_switch_frame
Feature: Add scorm activity
In order to let students access a scorm package
As a teacher
And I follow "Course 1"
And I turn editing mode on
And I add a "SCORM package" to section "1"
- And I upload "mod/scorm/tests/packages/singlescobasic.zip" file to "Package file" filemanager
And I fill the moodle form with:
| Name | Awesome SCORM package |
| Description | Description |
+ And I upload "mod/scorm/tests/packages/singlescobasic.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
Then I should see "Awesome SCORM package"
And I should see "Normal"
And I follow "Awesome SCORM package"
And I should see "Normal"
And I press "Enter"
- And I wait "5" seconds
And I switch to "scorm_object" iframe
And I switch to "contentFrame" iframe
And I should see "Play of the game"
And I fill the moodle form with:
| HTML format | Student page contents to be previewed |
And I press "Preview"
- Then I should see "This is a preview. Changes have not been saved yet"
+ Then I expand all fieldsets
+ And I should see "This is a preview. Changes have not been saved yet"
And I should see "Student page contents to be previewed"
And I press "Save"
And I should see "Student page contents to be previewed"
new Given('I follow "' . get_string('questionbank', 'question') . '"'),
new Given('I press "' . get_string('createnewquestion', 'question') . '"'),
new Given('I click on "' . $this->escape($questiontypexpath) . '" "xpath_element"'),
- new Given('I click on "Next" "button" in the "#qtypechoicecontainer" "css_element"'),
+ new Given('I click on "#chooseqtype_submit" "css_element"'),
new Given('I fill the moodle form with:', $questiondata),
new Given('I press "' . get_string('savechanges') . '"')
);
As a teacher
I need to edit questions
- @javascript
+ @javascript @_switch_window
Scenario: Edit a previously created question
Given the following "users" exists:
| username | firstname | lastname | email |
-@core @core_question
+@core @core_question @_switch_window
Feature: A teacher can preview questions in the question bank
In order to ensure the questions are properly created
As a teacher
$dialognode = $this->find('css', '.moodle-dialogue-focused');
$buttonnode = $this->find('css', '.fp-dlg-butcreate', $exception, $dialognode);
$buttonnode->click();
-
- // Wait until the process finished and modal windows are hidden.
- $this->wait_until_return_to_form();
-
- // Wait until the current folder contents are updated
- $this->wait_until_contents_are_updated($fieldnode);
}
/**
$this->getSession()
);
- // Just in case there is any contents refresh in progress.
- $this->wait_until_contents_are_updated($fieldnode);
-
$folderliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($foldername);
// We look both in the pathbar and in the contents.
// It should be a NodeElement, otherwise an exception would have been thrown.
$folder->click();
-
- // Wait until the current folder contents are updated
- $this->wait_until_contents_are_updated($fieldnode);
}
/**
// Execute the action.
$exception = new ExpectationException($filename.' element can not be unzipped', $this->getSession());
$this->perform_on_element('unzip', $exception);
-
- // Wait until the process finished and modal windows are hidden.
- $this->wait_until_return_to_form();
-
- // Wait until the current folder contents are updated
- $containernode = $this->get_filepicker_node($filemanagerelement);
- $this->wait_until_contents_are_updated($containernode);
}
/**
// Execute the action.
$exception = new ExpectationException($foldername.' element can not be zipped', $this->getSession());
$this->perform_on_element('zip', $exception);
-
- // Wait until the process finished and modal windows are hidden.
- $this->wait_until_return_to_form();
-
- // Wait until the current folder contents are updated
- $containernode = $this->get_filepicker_node($filemanagerelement);
- $this->wait_until_contents_are_updated($containernode);
}
/**
// Using xpath + click instead of pressButton as 'Ok' it is a common string.
$okbutton = $this->find('css', 'div.fp-dlg button.fp-dlg-butconfirm');
$okbutton->click();
-
- // Wait until the process finished and modal windows are hidden.
- $this->wait_until_return_to_form();
-
- // Wait until file manager contents are updated.
- $containernode = $this->get_filepicker_node($filemanagerelement);
- $this->wait_until_contents_are_updated($containernode);
}
*/
public function i_should_see_elements_in_filemanager($elementscount, $filemanagerelement) {
$filemanagernode = $this->get_filepicker_node($filemanagerelement);
- $this->wait_until_contents_are_updated($filemanagernode);
$elements = $this->find_all('css', '.fp-content .fp-file', false, $filemanagernode);
if (count($elements) != $elementscount) {
throw new ExpectationException('Found '.count($elements).' elements in filemanager instead of expected '.$elementscount);
$overwriteaction = false) {
$filemanagernode = $this->get_filepicker_node($filemanagerelement);
- // Wait until file manager is completely loaded.
- $this->wait_until_contents_are_updated($filemanagernode);
-
// Opening the select repository window and selecting the upload repository.
$this->open_add_file_window($filemanagernode, $repository);
$this->find_button(get_string('getfile', 'repository'))->click();
if ($overwriteaction !== false) {
- $this->getSession()->wait(1 * 1000, false);
$this->find_button($overwriteaction)->click();
}
-
- // Ensure the file has been uploaded and all ajax processes finished.
- $this->wait_until_return_to_form();
-
- // Wait until file manager contents are updated.
- $this->wait_until_contents_are_updated($filemanagernode);
}
}
$filemanagernode = $this->get_filepicker_node($filemanagerelement);
- // Wait until file manager is completely loaded.
- $this->wait_until_contents_are_updated($filemanagernode);
-
// Opening the select repository window and selecting the upload repository.
$this->open_add_file_window($filemanagernode, get_string('pluginname', 'repository_upload'));
// Ensure all the form is ready.
- $this->getSession()->wait(2 * 1000, false);
$noformexception = new ExpectationException('The upload file form is not ready', $this->getSession());
$this->find(
'xpath',
$submit->press();
if ($overwriteaction !== false) {
- $this->getSession()->wait(1 * 1000, false);
$this->find_button($overwriteaction)->click();
}
- // Ensure the file has been uploaded and all ajax processes finished.
- $this->wait_until_return_to_form();
-
- // Wait until file manager contents are updated.
- $this->wait_until_contents_are_updated($filemanagernode);
}
}