diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php index 13d5f2f248a..58b77754d55 100644 --- a/admin/tests/behat/behat_admin.php +++ b/admin/tests/behat/behat_admin.php @@ -59,7 +59,7 @@ class behat_admin extends behat_base { // 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. @@ -68,7 +68,7 @@ class behat_admin extends behat_base { $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. diff --git a/admin/tests/behat/upload_users.feature b/admin/tests/behat/upload_users.feature index 16aef6dc396..b276f44fd53 100644 --- a/admin/tests/behat/upload_users.feature +++ b/admin/tests/behat/upload_users.feature @@ -42,5 +42,4 @@ Feature: Upload users And I expand "Users" node And I follow "Groups" And I select "Section 1 (1)" from "groups" - And I wait "4" seconds And the "members" select box should contain "Tom Jones" diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature index 5b1bd502260..83f03207a50 100644 --- a/admin/tool/behat/tests/behat/basic_actions.feature +++ b/admin/tool/behat/tests/behat/basic_actions.feature @@ -39,7 +39,7 @@ Feature: Page contents assertions | Course 1 | C1 | 0 | And I log in as "admin" And I follow "Course 1" - When I click on "Move this to the dock" "button" in the "Administration" "block" + When I dock "Administration" block Then I should not see "Question bank" in the "region-pre" "region" And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element" @@ -49,5 +49,5 @@ Feature: Page contents assertions | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" - When I click on "Move this to the dock" "button" in the "Administration" "block" + When I dock "Administration" block Then I should not see "Turn editing on" in the "region-pre" "region" diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature index 063e1983ac5..8f21a72f8a7 100644 --- a/admin/tool/behat/tests/behat/data_generators.feature +++ b/admin/tool/behat/tests/behat/data_generators.feature @@ -230,8 +230,6 @@ Feature: Set up contextual data for tests Then the "groups" select box should contain "Group 1 (1)" And the "groups" select box should contain "Group 2 (1)" And I select "Group 1 (1)" from "groups" - And I wait "5" seconds And the "members" select box should contain "Student 1" And I select "Group 2 (1)" from "groups" - And I wait "5" seconds And the "members" select box should contain "Student 2" diff --git a/backup/util/ui/tests/behat/backup_courses.feature b/backup/util/ui/tests/behat/backup_courses.feature index ddc406ed345..60b187117fa 100644 --- a/backup/util/ui/tests/behat/backup_courses.feature +++ b/backup/util/ui/tests/behat/backup_courses.feature @@ -38,7 +38,7 @@ Feature: Backup Moodle courses And I press "Continue" And I click on "Continue" "button" in the ".bcs-current-course" "css_element" And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists - And I check "Include course logs" + And "Include course logs" "checkbox" should exists And I press "Next" @javascript diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php index d06185ed304..84fb672bc78 100644 --- a/backup/util/ui/tests/behat/behat_backup.php +++ b/backup/util/ui/tests/behat/behat_backup.php @@ -56,27 +56,32 @@ class behat_backup extends behat_base { // Go to homepage. $this->getSession()->visit($this->locate_path('/')); + $this->wait(); // Click the course link. $this->find_link($backupcourse)->click(); + $this->wait(); // Click the backup link. $this->find_link(get_string('backup'))->click(); + $this->wait(); // Initial settings. $this->fill_backup_restore_form($options); $this->find_button(get_string('backupstage1action', 'backup'))->press(); + $this->wait(); // Schema settings. $this->fill_backup_restore_form($options); $this->find_button(get_string('backupstage2action', 'backup'))->press(); + $this->wait(); // Confirmation and review, backup filename can also be specified. $this->fill_backup_restore_form($options); $this->find_button(get_string('backupstage4action', 'backup'))->press(); // Waiting for it to finish. - $this->wait(10); + $this->wait(self::EXTENDED_TIMEOUT); // Last backup continue button. $this->find_button(get_string('backupstage16action', 'backup'))->press(); @@ -101,12 +106,15 @@ class behat_backup extends behat_base { // Go to homepage. $this->getSession()->visit($this->locate_path('/')); + $this->wait(); // Click the course link. $this->find_link($tocourse)->click(); + $this->wait(); // Click the import link. $this->find_link(get_string('import'))->click(); + $this->wait(); // Select the course. $exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession()); @@ -121,18 +129,21 @@ class behat_backup extends behat_base { $radionode->click(); $this->find_button(get_string('continue'))->press(); + $this->wait(); // Initial settings. $this->fill_backup_restore_form($options); $this->find_button(get_string('importbackupstage1action', 'backup'))->press(); + $this->wait(); // Schema settings. $this->fill_backup_restore_form($options); $this->find_button(get_string('importbackupstage2action', 'backup'))->press(); + $this->wait(); // Run it. $this->find_button(get_string('importbackupstage4action', 'backup'))->press(); - $this->wait(); + $this->wait(self::EXTENDED_TIMEOUT); // Continue and redirect to 'to' course. $this->find_button(get_string('continue'))->press(); @@ -294,17 +305,22 @@ class behat_backup extends behat_base { // Settings. $this->fill_backup_restore_form($options); $this->find_button(get_string('restorestage4action', 'backup'))->press(); + $this->wait(); // Schema. $this->fill_backup_restore_form($options); $this->find_button(get_string('restorestage8action', 'backup'))->press(); + $this->wait(); // 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(); + + // Long wait when waiting for the restore to finish. + $this->wait(self::EXTENDED_TIMEOUT); } /** @@ -325,11 +341,15 @@ class behat_backup extends behat_base { return; } + // Wait for the page to be loaded and the JS ready. + $this->wait(); + // If we find any of the provided options in the current form we should set the value. $datahash = $options->getRowsHash(); foreach ($datahash as $locator => $value) { try { + // Using $this->find* to enforce stability over speed. $fieldnode = $this->find_field($locator); $field = behat_field_manager::get_form_field($fieldnode, $this->getSession()); $field->set_value($value); @@ -341,9 +361,9 @@ class behat_backup extends behat_base { } /** - * Waits until the DOM is ready. + * Waits until the DOM and the page Javascript code is ready. * - * @param int To override the default timeout + * @param int $timeout The number of seconds that we wait. * @return void */ protected function wait($timeout = false) { @@ -355,7 +375,8 @@ class behat_backup extends behat_base { if (!$timeout) { $timeout = self::TIMEOUT; } - $this->getSession()->wait($timeout, '(document.readyState === "complete")'); + + $this->getSession()->wait($timeout * 1000, self::PAGE_READY_JS); } } diff --git a/backup/util/ui/tests/behat/duplicate_activities.feature b/backup/util/ui/tests/behat/duplicate_activities.feature index 9675a620e24..3314372b482 100644 --- a/backup/util/ui/tests/behat/duplicate_activities.feature +++ b/backup/util/ui/tests/behat/duplicate_activities.feature @@ -21,8 +21,8 @@ Feature: Duplicate activities 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: diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index c738acf92b8..458df34eb59 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -4,13 +4,10 @@ Feature: Award badges 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: @@ -46,6 +43,7 @@ Feature: Award badges | 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" @@ -89,7 +87,6 @@ Feature: Award badges | 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" @@ -133,7 +130,6 @@ Feature: Award badges | 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 | @@ -172,7 +168,6 @@ Feature: Award badges 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" @@ -190,7 +185,6 @@ Feature: Award badges | 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 | diff --git a/blocks/comments/tests/behat/behat_block_comments.php b/blocks/comments/tests/behat/behat_block_comments.php index e7eaf887d4d..059fe1d5f40 100644 --- a/blocks/comments/tests/behat/behat_block_comments.php +++ b/blocks/comments/tests/behat/behat_block_comments.php @@ -65,9 +65,6 @@ class behat_block_comments extends behat_base { $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); @@ -103,7 +100,7 @@ class behat_block_comments extends behat_base { $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); } diff --git a/blocks/navigation/tests/behat/view_my_courses.feature b/blocks/navigation/tests/behat/view_my_courses.feature index 6287bad3d15..d1670b40747 100644 --- a/blocks/navigation/tests/behat/view_my_courses.feature +++ b/blocks/navigation/tests/behat/view_my_courses.feature @@ -58,14 +58,11 @@ Feature: View my courses in navigation block And I should see "cat3" in the "Navigation" "block" And I should not see "cat2" in the "Navigation" "block" And I expand "cat3" node - And I wait "2" seconds And I should see "cat31" in the "Navigation" "block" And I should see "cat33" in the "Navigation" "block" And I should not see "cat32" in the "Navigation" "block" And I expand "cat31" node - And I wait "2" seconds And I should see "c31" in the "Navigation" "block" And I expand "cat33" node - And I wait "2" seconds And I should see "c331" in the "Navigation" "block" And I should not see "c332" in the "Navigation" "block" diff --git a/blocks/tests/behat/behat_blocks.php b/blocks/tests/behat/behat_blocks.php index 36d8be2468d..28d59caffb0 100644 --- a/blocks/tests/behat/behat_blocks.php +++ b/blocks/tests/behat/behat_blocks.php @@ -58,6 +58,20 @@ class behat_blocks extends behat_base { return $steps; } + /** + * Docks a block. Editing mode should be previously enabled. + * + * @Given /^I dock "(?P(?:[^"]|\\")*)" block$/ + * @param string $blockname + * @return Given + */ + public function i_dock_block($blockname) { + + // Looking for both title and alt. + $xpath = "//input[@type='image'][@title='" . get_string('dockblock', 'block', $blockname) . "' or @alt='" . get_string('addtodock', 'block') . "']"; + return new Given('I click on " ' . $xpath . '" "xpath_element" in the "' . $this->escape($blockname) . '" "block"'); + } + /** * Opens a block's actions menu if it is not already opened. * diff --git a/blog/tests/behat/comment.feature b/blog/tests/behat/comment.feature index 1527a3ca256..84501bd2045 100644 --- a/blog/tests/behat/comment.feature +++ b/blog/tests/behat/comment.feature @@ -33,11 +33,9 @@ Feature: Comment on a blog entry And I follow "Comments (0)" When I fill in "content" with "$My own >nasty< \"string\"!" And I follow "Save comment" - And I wait "4" seconds Then I should see "$My own >nasty< \"string\"!" And I fill in "content" with "Another $Nasty " And I follow "Save comment" - And I wait "4" seconds And I should see "Comments (2)" in the ".comment-link" "css_element" @javascript @@ -53,8 +51,8 @@ Feature: Comment on a blog entry And I follow "Comments (0)" And I fill in "content" with "$My own >nasty< \"string\"!" And I follow "Save comment" - And I wait "4" seconds When I click on ".comment-delete a" "css_element" + # Waiting for the animation to finish. And I wait "4" seconds Then I should not see "$My own >nasty< \"string\"!" And I follow "Blog post from user 1" @@ -73,5 +71,4 @@ Feature: Comment on a blog entry When I follow "Comments (0)" And I fill in "content" with "$My own >nasty< \"string\"!" And I follow "Save comment" - And I wait "4" seconds Then I should see "$My own >nasty< \"string\"!" diff --git a/completion/tests/behat/enable_manual_complete_mark.feature b/completion/tests/behat/enable_manual_complete_mark.feature index a14b9b9a1ce..98d29827e5c 100644 --- a/completion/tests/behat/enable_manual_complete_mark.feature +++ b/completion/tests/behat/enable_manual_complete_mark.feature @@ -37,7 +37,6 @@ Feature: Allow students to manually mark an activity as complete And I log in as "student1" And I follow "Course 1" And I press "Mark as complete: Test forum name" - And I wait "3" seconds And I log out And I log in as "teacher1" And I follow "Course 1" diff --git a/completion/tests/behat/restrict_activity_by_date.feature b/completion/tests/behat/restrict_activity_by_date.feature index fe1a0f27bf5..8e700b13144 100644 --- a/completion/tests/behat/restrict_activity_by_date.feature +++ b/completion/tests/behat/restrict_activity_by_date.feature @@ -20,13 +20,16 @@ Feature: Restrict activity availability through date conditions And I set the following administration settings values: | Enable conditional access | 1 | And I log out + 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 log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on - And I add a "Assignment" to section "1" + 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 | @@ -36,7 +39,6 @@ Feature: Restrict activity availability through date conditions | 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" @@ -47,10 +49,7 @@ Feature: Restrict activity availability through date conditions @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 | @@ -60,7 +59,6 @@ Feature: Restrict activity availability through date conditions | 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" diff --git a/completion/tests/behat/restrict_activity_by_grade.feature b/completion/tests/behat/restrict_activity_by_grade.feature index e3de103157e..72154be52e1 100644 --- a/completion/tests/behat/restrict_activity_by_grade.feature +++ b/completion/tests/behat/restrict_activity_by_grade.feature @@ -29,13 +29,18 @@ Feature: Restrict activity availability through grade conditions | 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" diff --git a/course/tests/behat/activities_group_icons.feature b/course/tests/behat/activities_group_icons.feature index 055cb1a0cf9..2fde9985cd6 100644 --- a/course/tests/behat/activities_group_icons.feature +++ b/course/tests/behat/activities_group_icons.feature @@ -29,14 +29,12 @@ Feature: Toggle activities groups mode from the course page Then "No groups (Click to change)" "link" should exists And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists And I click on "No groups (Click to change)" "link" in the "Test forum name" activity - And I wait "3" seconds And "Separate groups (Click to change)" "link" should exists And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists And I reload the page And "Separate groups (Click to change)" "link" should exists And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity - And I wait "3" seconds And "Visible groups (Click to change)" "link" should exists And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists And I reload the page diff --git a/course/tests/behat/add_activities.feature b/course/tests/behat/add_activities.feature index db8879cb06f..de34b8483c0 100644 --- a/course/tests/behat/add_activities.feature +++ b/course/tests/behat/add_activities.feature @@ -40,8 +40,10 @@ Feature: Add activities to courses 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'" diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index 87f84954084..fcceee3e115 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -67,15 +67,50 @@ class behat_course extends behat_base { * @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); + + // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the + // format field when the editor is being rendered and the click misses the field coordinates. + $steps[] = new Given('I wait until the editors are loaded'); + $steps[] = new Given('I select "' . $formatvalue . '" from "' . $formatfield . '"'); + $steps[] = new Given('I fill the moodle form with:', $table); + } else { + $steps[] = new Given('I fill the moodle form with:', $table); + } + + $steps[] = new Given('I press "' . get_string('savechanges') . '"'); + + return $steps; } /** @@ -181,10 +216,7 @@ class behat_course extends behat_base { // 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"'); } /** @@ -199,10 +231,7 @@ class behat_course extends behat_base { // 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"'); } /** @@ -215,9 +244,9 @@ class behat_course extends behat_base { $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); } } @@ -231,9 +260,9 @@ class behat_course extends behat_base { $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); } } @@ -314,6 +343,11 @@ class behat_course extends behat_base { $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); @@ -338,9 +372,12 @@ class behat_course extends behat_base { // 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); @@ -349,6 +386,17 @@ class behat_course extends behat_base { // 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(); + } + } } } @@ -543,8 +591,7 @@ class behat_course extends behat_base { $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) . '"') ); } @@ -572,6 +619,30 @@ class behat_course extends behat_base { 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(?:[^"]|\\")*)" 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. * @@ -588,10 +659,6 @@ class behat_course extends behat_base { } $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; } @@ -611,10 +678,6 @@ class behat_course extends behat_base { } $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; } @@ -640,8 +703,6 @@ class behat_course extends behat_base { $this->getSession()->getDriver()->getWebDriverSession()->accept_alert(); - $this->getSession()->wait(2 * 1000, false); - } else { // With JS disabled. @@ -668,10 +729,7 @@ class behat_course extends behat_base { $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') .'"'); } @@ -691,12 +749,22 @@ class behat_course extends behat_base { $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']"; @@ -717,6 +785,32 @@ class behat_course extends behat_base { 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\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. * @@ -882,6 +976,18 @@ class behat_course extends behat_base { 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. * diff --git a/course/tests/behat/course_category_management_listing.feature b/course/tests/behat/course_category_management_listing.feature index 14a682083d1..d7a96a1f18d 100644 --- a/course/tests/behat/course_category_management_listing.feature +++ b/course/tests/behat/course_category_management_listing.feature @@ -1,4 +1,4 @@ -@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 diff --git a/course/tests/behat/course_controls.feature b/course/tests/behat/course_controls.feature index 87adb974ed9..0e5823916dc 100644 --- a/course/tests/behat/course_controls.feature +++ b/course/tests/behat/course_controls.feature @@ -1,4 +1,4 @@ -@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 @@ -59,11 +59,16 @@ Feature: Course activity controls works as expected 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" 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" 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" exists diff --git a/group/clientlib.js b/group/clientlib.js index 83c2c542f6d..fef76b1edf8 100644 --- a/group/clientlib.js +++ b/group/clientlib.js @@ -91,7 +91,7 @@ function UpdatableMembersCombo(wwwRoot, courseId) { this.courseId = courseId; this.connectCallback = { - success: function(o) { + success: function(t, o) { if (o.responseText !== undefined) { var selectEl = document.getElementById("members"); @@ -124,7 +124,7 @@ function UpdatableMembersCombo(wwwRoot, courseId) { removeLoaderImgs("membersloader", "memberslabel"); }, - failure: function(o) { + failure: function() { removeLoaderImgs("membersloader", "memberslabel"); } @@ -185,9 +185,13 @@ UpdatableMembersCombo.prototype.refreshMembers = function () { if(singleSelection) { var sUrl = this.wwwRoot+"/group/index.php?id="+this.courseId+"&group="+groupId+"&act_ajax_getmembersingroup"; - var callback = this.connectCallback; - YUI().use('yui2-connection', function (Y) { - Y.YUI2.util.Connect.asyncRequest("GET", sUrl, callback, null); + var self = this; + YUI().use('io', function (Y) { + Y.io(sUrl, { + method: 'GET', + context: this, + on: self.connectCallback + }); }); } }; @@ -271,4 +275,4 @@ function init_add_remove_members_page(Y) { addselect = document.getElementById('addselect'); addselect.onchange = updateUserSummary; -} \ No newline at end of file +} diff --git a/group/tests/behat/behat_groups.php b/group/tests/behat/behat_groups.php index 2afb6f383b8..f32ce79957a 100644 --- a/group/tests/behat/behat_groups.php +++ b/group/tests/behat/behat_groups.php @@ -67,7 +67,7 @@ class behat_groups extends behat_base { $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'); @@ -80,7 +80,7 @@ class behat_groups extends behat_base { $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(); diff --git a/group/tests/behat/create_groups.feature b/group/tests/behat/create_groups.feature index c6e3f84e3e3..7ec76223217 100644 --- a/group/tests/behat/create_groups.feature +++ b/group/tests/behat/create_groups.feature @@ -40,12 +40,10 @@ Feature: Organize students into groups And I add "student2" user to "Group 2" group And I add "student3" user to "Group 2" group Then I select "Group 1 (2)" from "groups" - And I wait "5" seconds And the "members" select box should contain "Student 0" And the "members" select box should contain "Student 1" And the "members" select box should not contain "Student 2" And I select "Group 2 (2)" from "groups" - And I wait "5" seconds And the "members" select box should contain "Student 2" And the "members" select box should contain "Student 3" And the "members" select box should not contain "Student 0" diff --git a/group/tests/behat/groups_import.feature b/group/tests/behat/groups_import.feature index 83322aa558d..ed021a69ce7 100644 --- a/group/tests/behat/groups_import.feature +++ b/group/tests/behat/groups_import.feature @@ -1,4 +1,4 @@ -@core @core_group +@core @core_group @_only_local Feature: Importing of groups and groupings In order to import groups and grouping As a teacher diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 241a773d236..12904931dfd 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -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 + ); + } + } diff --git a/lib/behat/behat_files.php b/lib/behat/behat_files.php index 06358f3f9ea..a1865741f13 100644 --- a/lib/behat/behat_files.php +++ b/lib/behat/behat_files.php @@ -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); } } diff --git a/lib/behat/form_field/behat_form_editor.php b/lib/behat/form_field/behat_form_editor.php index ed3cd27d4b2..5773d127368 100644 --- a/lib/behat/form_field/behat_form_editor.php +++ b/lib/behat/form_field/behat_form_editor.php @@ -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")'); diff --git a/lib/behat/form_field/behat_form_field.php b/lib/behat/form_field/behat_form_field.php index d5d43e1b218..8db45793c39 100644 --- a/lib/behat/form_field/behat_form_field.php +++ b/lib/behat/form_field/behat_form_field.php @@ -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(); + } } diff --git a/lib/behat/form_field/behat_form_select.php b/lib/behat/form_field/behat_form_select.php index 5be8135633e..2805a97101c 100644 --- a/lib/behat/form_field/behat_form_select.php +++ b/lib/behat/form_field/behat_form_select.php @@ -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); } } diff --git a/lib/editor/tinymce/module.js b/lib/editor/tinymce/module.js index b86da9d8406..d3118ef0f33 100644 --- a/lib/editor/tinymce/module.js +++ b/lib/editor/tinymce/module.js @@ -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; }; diff --git a/lib/editor/tinymce/tests/behat/edit_available_icons.feature b/lib/editor/tinymce/tests/behat/edit_available_icons.feature index dad83469992..582069080f3 100644 --- a/lib/editor/tinymce/tests/behat/edit_available_icons.feature +++ b/lib/editor/tinymce/tests/behat/edit_available_icons.feature @@ -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" diff --git a/lib/javascript-static.js b/lib/javascript-static.js index 1c1e19ccec7..df36acd83d5 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -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 * diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index e0648ffad8a..24a3f566d1f 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -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) { diff --git a/lib/tests/behat/behat_deprecated.php b/lib/tests/behat/behat_deprecated.php index 5532c815cb2..2203702d626 100644 --- a/lib/tests/behat/behat_deprecated.php +++ b/lib/tests/behat/behat_deprecated.php @@ -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(); } diff --git a/lib/tests/behat/behat_forms.php b/lib/tests/behat/behat_forms.php index b33931fcc98..e16fda35bf0 100644 --- a/lib/tests/behat/behat_forms.php +++ b/lib/tests/behat/behat_forms.php @@ -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(); } diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 8785612b213..8d11e55d5d4 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -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(?:[^"]|\\")*)" "(?P[^"]*)" 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(?:[^"]|\\")*)" "(?P[^"]*)" 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(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" 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) + ); } /** diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index 57891341d28..81285c0717a 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -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; } /** diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php index 541731050e1..d7de467345f 100644 --- a/lib/tests/behat/behat_navigation.php +++ b/lib/tests/behat/behat_navigation.php @@ -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(); } diff --git a/lib/tests/behat/behat_permissions.php b/lib/tests/behat/behat_permissions.php index 2d5f2f04fa6..562be1a709f 100644 --- a/lib/tests/behat/behat_permissions.php +++ b/lib/tests/behat/behat_permissions.php @@ -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. diff --git a/mod/assign/tests/behat/file_submission.feature b/mod/assign/tests/behat/file_submission.feature index 4e33ee93589..97d25023711 100644 --- a/mod/assign/tests/behat/file_submission.feature +++ b/mod/assign/tests/behat/file_submission.feature @@ -38,10 +38,10 @@ Feature: In an assignment, students can upload files for assessment And I should see "Not graded" And I press "Edit submission" And I upload "lib/tests/fixtures/upload_users.csv" file to "File submissions" filemanager - And ".ffilemanager .fm-maxfiles .fp-btn-add" "css_element" should exists + And ".ffilemanager .fm-maxfiles .fp-btn-add" "css_element" should not be visible And I press "Save changes" And I should see "Submitted for grading" And I should see "empty.txt" And I should see "upload_users.csv" And I press "Edit submission" - And ".ffilemanager .fm-maxfiles .fp-btn-add" "css_element" should exists + And ".ffilemanager .fm-maxfiles .fp-btn-add" "css_element" should not be visible diff --git a/mod/forum/tests/behat/add_forum.feature b/mod/forum/tests/behat/add_forum.feature index 90edd7473d2..135a3e53ce6 100644 --- a/mod/forum/tests/behat/add_forum.feature +++ b/mod/forum/tests/behat/add_forum.feature @@ -25,5 +25,4 @@ Feature: Add forum activities and discussions When I add a new discussion to "Test forum name" forum with: | Subject | Forum post 1 | | Message | This is the body | - And I wait "6" seconds Then I should see "Test forum name" diff --git a/mod/forum/tests/behat/edit_post_teacher.feature b/mod/forum/tests/behat/edit_post_teacher.feature index 5dafb8fb5aa..c9e1d57d897 100644 --- a/mod/forum/tests/behat/edit_post_teacher.feature +++ b/mod/forum/tests/behat/edit_post_teacher.feature @@ -41,7 +41,6 @@ Feature: Teachers can edit or delete any forum post And I follow "Teacher post subject" And I click on "Delete" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Student post subject')]" "xpath_element" And I press "Continue" - And I wait "4" seconds Then I should not see "Student post subject" And I should not see "Student post message" @@ -56,7 +55,7 @@ Feature: Teachers can edit or delete any forum post And I fill the moodle form with: | Subject | Edited student subject | And I press "Save changes" - And I wait "4" seconds + And I wait to be redirected Then I should see "Edited student subject" And I should see "Edited by Teacher 1 - original submission" diff --git a/mod/forum/tests/behat/track_read_posts.feature b/mod/forum/tests/behat/track_read_posts.feature index d08ce1d7567..da92f8605e3 100644 --- a/mod/forum/tests/behat/track_read_posts.feature +++ b/mod/forum/tests/behat/track_read_posts.feature @@ -30,7 +30,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I log out When I log in as "student1" And I follow "Course 1" @@ -48,19 +47,18 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I log out When I log in as "student1" And I follow "Course 1" Then I should see "1 unread post" And I follow "Test forum name" And I follow "Don't track unread posts" - And I wait "4" seconds + And I wait to be redirected And I follow "Course 1" And I should not see "1 unread post" And I follow "Test forum name" And I follow "Track unread posts" - And I wait "4" seconds + And I wait to be redirected And I follow "1" And I follow "Course 1" And I should not see "1 unread post" @@ -75,7 +73,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I log out When I log in as "student2" And I follow "Course 1" @@ -85,7 +82,7 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos @javascript Scenario: Tracking forum posts forced with user tracking on - And I set the following administration settings values: + Given I set the following administration settings values: | Allow forced read tracking | 1 | And I follow "Home" And I follow "Course 1" @@ -97,7 +94,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I log out When I log in as "student1" And I follow "Course 1" @@ -110,7 +106,7 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos @javascript Scenario: Tracking forum posts forced with user tracking off - And I set the following administration settings values: + Given I set the following administration settings values: | Allow forced read tracking | 1 | And I follow "Home" And I follow "Course 1" @@ -122,7 +118,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I log out When I log in as "student2" And I follow "Course 1" @@ -135,7 +130,7 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos @javascript Scenario: Tracking forum posts forced (with force disabled) with user tracking on - And I set the following administration settings values: + Given I set the following administration settings values: | Allow forced read tracking | 1 | And I follow "Home" And I follow "Course 1" @@ -147,7 +142,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I set the following administration settings values: | Allow forced read tracking | 0 | And I log out @@ -156,19 +150,19 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos Then I should see "1 unread post" And I follow "Test forum name" And I follow "Don't track unread posts" - And I wait "4" seconds + And I wait to be redirected And I follow "Course 1" And I should not see "1 unread post" And I follow "Test forum name" And I follow "Track unread posts" - And I wait "4" seconds + And I wait to be redirected And I follow "1" And I follow "Course 1" And I should not see "1 unread post" @javascript Scenario: Tracking forum posts forced (with force disabled) with user tracking off - And I set the following administration settings values: + Given I set the following administration settings values: | Allow forced read tracking | 1 | And I follow "Home" And I follow "Course 1" @@ -180,7 +174,6 @@ Feature: A teacher can set one of 3 possible options for tracking read forum pos And I add a new discussion to "Test forum name" forum with: | Subject | Test post subject | | Message | Test post message | - And I wait "6" seconds And I set the following administration settings values: | Allow forced read tracking | 0 | And I log out diff --git a/mod/scorm/tests/behat/add_scorm.feature b/mod/scorm/tests/behat/add_scorm.feature index feb613df53c..e9480c94480 100644 --- a/mod/scorm/tests/behat/add_scorm.feature +++ b/mod/scorm/tests/behat/add_scorm.feature @@ -1,4 +1,4 @@ -@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 @@ -21,10 +21,10 @@ Feature: Add scorm activity 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" @@ -35,7 +35,6 @@ Feature: Add scorm activity 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" diff --git a/mod/wiki/module.js b/mod/wiki/module.js index 52773d81e8b..cf59ef8cbf7 100644 --- a/mod/wiki/module.js +++ b/mod/wiki/module.js @@ -38,21 +38,23 @@ M.mod_wiki.init = function(Y, args) { }); new WikiHelper(args); }; -M.mod_wiki.renew_lock = function(Y, args) { - function renewLock() { - var args = {}; - args['sesskey'] = M.cfg.sesskey; - args['pageid'] = wiki.pageid; - if (wiki.section) { - args['section'] = wiki.section; - } - var callback = {}; - Y.use('yui2-connection', function(Y) { - Y.YUI2.util.Connect.asyncRequest('GET', 'lock.php?' + build_querystring(args), callback); - }); +M.mod_wiki.renew_lock = function() { + var args = { + sesskey: M.cfg.sesskey, + pageid: wiki.pageid + }; + if (wiki.section) { + args.section = wiki.section; } - setInterval(renewLock, wiki.renew_lock_timeout * 1000); -} + YUI().use('io', function(Y) { + function renewLock() { + Y.io('lock.php?' + build_querystring(args), { + method: 'POST' + }); + } + setInterval(renewLock, wiki.renew_lock_timeout * 1000); + }); +}; M.mod_wiki.history = function(Y, args) { var compare = false; var comparewith = false; diff --git a/mod/wiki/tests/behat/preview_page.feature b/mod/wiki/tests/behat/preview_page.feature index 524bf705401..ce26e8d658f 100644 --- a/mod/wiki/tests/behat/preview_page.feature +++ b/mod/wiki/tests/behat/preview_page.feature @@ -32,7 +32,8 @@ Feature: Edited wiki pages may be previewed before saving 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" diff --git a/question/tests/behat/behat_question.php b/question/tests/behat/behat_question.php index 7f6435e1f57..540cfc4943f 100644 --- a/question/tests/behat/behat_question.php +++ b/question/tests/behat/behat_question.php @@ -60,7 +60,7 @@ class behat_question extends behat_base { 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') . '"') ); diff --git a/question/tests/behat/edit_questions.feature b/question/tests/behat/edit_questions.feature index 94316413f92..45d38820a35 100644 --- a/question/tests/behat/edit_questions.feature +++ b/question/tests/behat/edit_questions.feature @@ -4,7 +4,7 @@ Feature: A teacher can edit questions in the question bank 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 | diff --git a/question/tests/behat/preview_question.feature b/question/tests/behat/preview_question.feature index 7b177957837..63669e646a0 100644 --- a/question/tests/behat/preview_question.feature +++ b/question/tests/behat/preview_question.feature @@ -1,4 +1,4 @@ -@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 diff --git a/repository/tests/behat/behat_filepicker.php b/repository/tests/behat/behat_filepicker.php index 4dac65fe829..8b2ed2d1ecd 100644 --- a/repository/tests/behat/behat_filepicker.php +++ b/repository/tests/behat/behat_filepicker.php @@ -68,12 +68,6 @@ class behat_filepicker extends behat_files { $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); } /** @@ -93,9 +87,6 @@ class behat_filepicker extends behat_files { $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. @@ -124,9 +115,6 @@ class behat_filepicker extends behat_files { // 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); } /** @@ -145,13 +133,6 @@ class behat_filepicker extends behat_files { // 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); } /** @@ -170,13 +151,6 @@ class behat_filepicker extends behat_files { // 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); } /** @@ -200,13 +174,6 @@ class behat_filepicker extends behat_files { // 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); } @@ -220,7 +187,6 @@ class behat_filepicker extends behat_files { */ 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); @@ -298,9 +264,6 @@ class behat_filepicker extends behat_files { $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); @@ -323,16 +286,18 @@ class behat_filepicker extends behat_files { $this->find_button(get_string('getfile', 'repository'))->click(); + // We wait for all the JS to finish as it is performing an action. + $this->getSession()->wait(self::TIMEOUT, self::PAGE_READY_JS); + if ($overwriteaction !== false) { - $this->getSession()->wait(1 * 1000, false); - $this->find_button($overwriteaction)->click(); + $overwritebutton = $this->find_button($overwriteaction); + $this->ensure_node_is_visible($overwritebutton); + $overwritebutton->click(); + + // We wait for all the JS to finish. + $this->getSession()->wait(self::TIMEOUT, self::PAGE_READY_JS); } - // 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); } } diff --git a/repository/upload/tests/behat/behat_repository_upload.php b/repository/upload/tests/behat/behat_repository_upload.php index 9fa490e287e..48bc15545a0 100644 --- a/repository/upload/tests/behat/behat_repository_upload.php +++ b/repository/upload/tests/behat/behat_repository_upload.php @@ -109,14 +109,10 @@ class behat_repository_upload extends behat_files { $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', @@ -161,16 +157,18 @@ class behat_repository_upload extends behat_files { $submit = $this->find_button(get_string('upload', 'repository')); $submit->press(); + // We wait for all the JS to finish as it is performing an action. + $this->getSession()->wait(self::TIMEOUT, self::PAGE_READY_JS); + if ($overwriteaction !== false) { - $this->getSession()->wait(1 * 1000, false); - $this->find_button($overwriteaction)->click(); + $overwritebutton = $this->find_button($overwriteaction); + $this->ensure_node_is_visible($overwritebutton); + $overwritebutton->click(); + + // We wait for all the JS to finish. + $this->getSession()->wait(self::TIMEOUT, self::PAGE_READY_JS); } - // 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); } }