MDL-42625 behat: Step definitions + related changes in features
authorDavid Monllao <davidm@moodle.com>
Thu, 31 Oct 2013 07:32:11 +0000 (15:32 +0800)
committerDavid Monllao <davidm@moodle.com>
Fri, 6 Dec 2013 05:10:24 +0000 (13:10 +0800)
In general aiming for compatibility with multiple browsers,
firefox, chrome and phantomjs to be more specific.

* Removing hardcoded waits
* Adding @_alert, @_switch_window and @_switch_frame tags,
  to label actions that different drivers have problems with.
* Adding missing @_files_upload and @_only_local tags to features that
  uploads files.
* Fixing a few wait for page ready what specified miliseconds.
* New methods to ensure elements (usual selectors), sections and editors
  are ready to interact with
* Changing the select an option implementation to deal with the different
  drivers implementations when listening to JS events.

31 files changed:
admin/tests/behat/behat_admin.php
backup/util/ui/tests/behat/behat_backup.php
backup/util/ui/tests/behat/duplicate_activities.feature
badges/tests/behat/award_badge.feature
blocks/comments/tests/behat/behat_block_comments.php
blog/tests/behat/comment.feature
completion/tests/behat/restrict_activity_by_date.feature
completion/tests/behat/restrict_activity_by_grade.feature
course/tests/behat/add_activities.feature
course/tests/behat/behat_course.php
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_controls.feature
group/tests/behat/behat_groups.php
group/tests/behat/groups_import.feature
lib/behat/behat_base.php
lib/behat/behat_files.php
lib/behat/form_field/behat_form_editor.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/editor/tinymce/tests/behat/edit_available_icons.feature
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_permissions.php
mod/scorm/tests/behat/add_scorm.feature
mod/wiki/tests/behat/preview_page.feature
question/tests/behat/behat_question.php
question/tests/behat/edit_questions.feature
question/tests/behat/preview_question.feature
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php

index 13d5f2f..58b7775 100644 (file)
@@ -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.
index d06185e..a9cdf6c 100644 (file)
@@ -76,7 +76,7 @@ class behat_backup extends behat_base {
         $this->find_button(get_string('backupstage4action', 'backup'))->press();
 
         // Waiting for it to finish.
-        $this->wait(10);
+        $this->wait();
 
         // Last backup continue button.
         $this->find_button(get_string('backupstage16action', 'backup'))->press();
@@ -301,7 +301,7 @@ class behat_backup extends behat_base {
 
         // 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();
@@ -343,19 +343,15 @@ class behat_backup extends behat_base {
     /**
      * Waits until the DOM is ready.
      *
-     * @param int To override the default timeout
      * @return void
      */
-    protected function wait($timeout = false) {
+    protected function wait() {
 
         if (!$this->running_javascript()) {
             return;
         }
 
-        if (!$timeout) {
-            $timeout = self::TIMEOUT;
-        }
-        $this->getSession()->wait($timeout, '(document.readyState === "complete")');
+        $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
     }
 
 }
index 9675a62..3314372 100644 (file)
@@ -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:
index c738acf..458df34 100644 (file)
@@ -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 |
index e7eaf88..059fe1d 100644 (file)
@@ -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);
     }
 
index f02bc19..84501bd 100644 (file)
@@ -53,7 +53,7 @@ Feature: Comment on a blog entry
     And I follow "Save comment"
     When I click on ".comment-delete a" "css_element"
     # Waiting for the animation to finish.
-    And I wait "2" seconds
+    And I wait "4" seconds
     Then I should not see "$My own >nasty< \"string\"!"
     And I follow "Blog post from user 1"
     And I click on ".comment-link" "css_element"
index fe1a0f2..8e700b1 100644 (file)
@@ -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
-
-  @javascript
-  Scenario: Show activity greyed-out to students when available from date is in future
-    Given I log in as "teacher1"
+    And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
+    # Adding the page like this because id_available*_enabled needs to be clicked to trigger the action.
     And I add a "Assignment" to section "1"
+    And I expand all fieldsets
+
+  @javascript
+  Scenario: Show activity greyed-out to students when available from date is in future
+    Given I click on "id_availablefrom_enabled" "checkbox"
     And I fill the moodle form with:
       | Assignment name | Test assignment 1 |
       | Description | This assignment is restricted by date |
@@ -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"
index e3de103..72154be 100644 (file)
@@ -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"
index db8879c..de34b84 100644 (file)
@@ -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'"
index 87f8495..87cc6e3 100644 (file)
@@ -67,15 +67,48 @@ 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);
+
+            $steps[] = new Given('I fill the moodle form with:', $table);
+            $steps[] = new Given('I select "' . $formatvalue . '" from "' . $formatfield . '"');
+            $steps[] = new Given('I wait until the page is ready');
+        } else {
+            $steps[] = new Given('I fill the moodle form with:', $table);
+        }
+
+        $steps[] = new Given('I press "' . get_string('savechanges') . '"');
+
+        return $steps;
     }
 
     /**
@@ -181,10 +214,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 +229,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 +242,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 +258,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 +341,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 +370,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 +384,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 +589,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 +617,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<activity_name_string>(?:[^"]|\\")*)" actions menu$/
+     * @throws DriverException The step is not available when Javascript is disabled
+     * @param string $activityname
+     * @return Given
+     */
+    public function i_close_actions_menu($activityname) {
+
+        if (!$this->running_javascript()) {
+            throw new DriverException('Activities actions menu not available when Javascript is disabled');
+        }
+
+        // If it is already closed we do nothing.
+        $activitynode = $this->get_activity_node($activityname);
+        $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
+        if (empty($classes['action-menu-shown'])) {
+            return;
+        }
+
+        return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
+    }
+
     /**
      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
      *
@@ -588,10 +657,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 +676,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 +701,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 +727,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 +747,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 +783,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<section_number>\d+)" is available$/
+     * @param int $sectionnumber
+     * @return void
+     */
+    public function i_wait_until_section_is_available($sectionnumber) {
+
+        // Looks for a hidden lightbox or a non-existent lightbox in that section.
+        $sectionxpath = $this->section_exists($sectionnumber);
+        $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
+            " | " .
+            $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
+
+        $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
+    }
+
     /**
      * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
      *
@@ -882,6 +974,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.
      *
index 14a6820..d7a96a1 100644 (file)
@@ -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
index 87adb97..0e58239 100644 (file)
@@ -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" <should_see_other_sections> exists
     And I open "Test forum name 1" actions menu
     And I click on "Hide" "link" in the "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I close "Test forum name 1" actions menu
     And I duplicate "Test forum name 2" activity editing the new copy with:
       | Forum name | Edited test forum name 2 |
     And "#section-2" "css_element" <should_see_other_sections> exists
index 2afb6f3..f32ce79 100644 (file)
@@ -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();
index 83322aa..ed021a6 100644 (file)
@@ -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
index 241a773..752e538 100644 (file)
@@ -55,7 +55,22 @@ 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;
+
+    /**
+     * Number of retries to wait for the editor to be ready.
+     */
+    const WAIT_FOR_EDITOR_RETRIES = 10;
+
+    /**
+     * The JS code to check that the page is ready.
+     */
+    const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
 
     /**
      * Locates url, based on provided path.
@@ -420,4 +435,192 @@ 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 ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the element "' . $element . '" is not there';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // Will stop spinning the find() return false.
+        $this->spin(
+            function($context, $args) {
+                // We don't use behat_base::find as it is already spinning.
+                if ($context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+                    return true;
+                }
+            },
+            array('selector' => $selector, 'locator' => $locator),
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+
+    }
+
+    /**
+     * Spins until the element does not exist
+     *
+     * @throws ExpectationException
+     * @param string $element
+     * @param string $selectortype
+     * @return void
+     */
+    protected function ensure_element_does_not_exist($element, $selectortype) {
+
+        // Getting the behat selector & locator.
+        list($selector, $locator) = $this->transform_selector($selectortype, $element);
+
+        // Exception if it timesout and the element is still there.
+        $msg = 'The ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the "' . $element . '" element is still there';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // Will stop spinning the find() return false.
+        $this->spin(
+            function($context, $args) {
+                // We don't use behat_base::find as it is already spinning.
+                if (!$context->getSession()->getPage()->find($args['selector'], $args['locator'])) {
+                    return true;
+                }
+            },
+            array('selector' => $selector, 'locator' => $locator),
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+    }
+
+    /**
+     * Ensures that the provided node is visible and we can interact with it.
+     *
+     * @throws ExpectationException
+     * @param NodeElement $node
+     * @return void Throws an exception if it times out without the element being visible
+     */
+    protected function ensure_node_is_visible($node) {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        // Exception if it timesout and the element is still there.
+        $msg = 'The ' . self::EXTENDED_TIMEOUT . ' seconds timeout expired and the "' . $node->getXPath() . '" element is not visible';
+        $exception = new ExpectationException($msg, $this->getSession());
+
+        // Will stop spinning the find() return false.
+        $this->spin(
+            function($context, $args) {
+                if ($args->isVisible()) {
+                    return true;
+                }
+            },
+            $node,
+            self::EXTENDED_TIMEOUT,
+            $exception,
+            true
+        );
+    }
+
+    /**
+     * Ensures that the provided element is visible and we can interact with it.
+     *
+     * Returns the node in case other actions are interested in using it.
+     *
+     * @throws ExpectationException
+     * @param string $element
+     * @param string $selectortype
+     * @return NodeElement Throws an exception if it times out without being visible
+     */
+    protected function ensure_element_is_visible($element, $selectortype) {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        $node = $this->get_selected_node($selectortype, $element);
+        $this->ensure_node_is_visible($node);
+
+        return $node;
+    }
+
+    /**
+     * Ensures that all the page's editors are loaded.
+     *
+     * This method is expensive as it waits for .mceEditor CSS
+     * so use with caution and only where there will be editors.
+     *
+     * @throws ElementNotFoundException
+     * @return void
+     */
+    protected function ensure_editors_are_loaded() {
+
+        if (!$this->running_javascript()) {
+            return;
+        }
+
+        $lastexception = new Exception('The editors are not properly loaded');
+
+        // If there are no editors we don't need to wait.
+        //$this->getSession()->getPage()->find('css', '.mceEditor');
+        try {
+            $this->find('css', '.mceEditor');
+        } catch (ElementNotFoundException $e) {
+            return;
+        }
+
+        // We loop until we can interact with all the page editors.
+        for ($i = 0; $i < self::WAIT_FOR_EDITOR_RETRIES; $i++) {
+
+            // Here we know that there are .mceEditor editors in the page and we will
+            // probably need to interact with them, if we use tinyMCE JS var before
+            // it exists it will throw an exception and we want to catch it until all
+            // the page's editors are ready to interact with them.
+            try {
+
+                // It may return 0 if tinyMCE is loaded but not the instances, so we just loop again.
+                if ($this->getSession()->evaluateScript('return tinyMCE.editors.length;') > 0) {
+
+                    // It may be there but not ready.
+                    $iframeready = $this->getSession()->evaluateScript('
+                        var readyeditors = new Array;
+                        for (editorid in tinyMCE.editors) {
+                            if (tinyMCE.editors[editorid].getDoc().readyState === "complete") {
+                                readyeditors[editorid] = editorid;
+                            }
+                        }
+                        if (tinyMCE.editors.length === readyeditors.length) {
+                            return "complete";
+                        }
+                        return "";
+                    ');
+
+                    // Now we know that the editors are there.
+                    if ($iframeready) {
+                        return;
+                    }
+                }
+
+            } catch (Exception $e) {
+                // Catching any kind of exception and ignoring it until times out.
+                $lastexception = $e;
+
+                // Waiting 0.1 seconds.
+                usleep(100000);
+            }
+        }
+
+        // If it is not available we throw the last exception.
+        throw $lastexception;
+
+    }
 }
index 06358f3..a186574 100644 (file)
@@ -93,6 +93,7 @@ class behat_files extends behat_base {
         $classname = 'fp-file-' . $action;
         $button = $this->find('css', '.moodle-dialogue-focused button.' . $classname, $exception);
 
+        $this->ensure_node_is_visible($button);
         $button->click();
     }
 
@@ -148,13 +149,14 @@ class behat_files extends behat_base {
                 $locatorprefix .
                 "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" .
                 "[normalize-space(.)=$name]" .
-                "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]",
+                "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-field ')]",
                 false,
                 $containernode
             );
         }
 
         // Click opens the contextual menu when clicking on files.
+        $this->ensure_node_is_visible($node);
         $node->click();
     }
 
@@ -179,6 +181,7 @@ class behat_files extends behat_base {
             // Otherwise should be a single-file filepicker form element.
             $add = $this->find('css', 'input.fp-btn-choose', $exception, $filemanagernode);
         }
+        $this->ensure_node_is_visible($add);
         $add->click();
 
         // Getting the repository link and opening it.
@@ -197,12 +200,16 @@ class behat_files extends behat_base {
         );
 
         // Selecting the repo.
+        $this->ensure_node_is_visible($repositorylink);
         $repositorylink->click();
     }
 
     /**
      * Waits until the file manager modal windows are closed.
      *
+     * This method is not used by any of our step definitions,
+     * keeping it here for users already using it.
+     *
      * @throws ExpectationException
      * @return void
      */
@@ -220,6 +227,9 @@ class behat_files extends behat_base {
     /**
      * Checks that the file manager contents are not being updated.
      *
+     * This method is not used by any of our step definitions,
+     * keeping it here for users already using it.
+     *
      * @throws ExpectationException
      * @param NodeElement $filepickernode The file manager DOM node
      * @return void
@@ -243,9 +253,6 @@ class behat_files extends behat_base {
             $exception,
             $filepickernode
         );
-
-        // After removing the class FileManagerHelper.view_files() performs other actions.
-        $this->getSession()->wait(4 * 1000, false);
     }
 
 }
index ed3cd27..d89157f 100644 (file)
@@ -48,19 +48,38 @@ class behat_form_editor extends behat_form_field {
      */
     public function set_value($value) {
 
-        // Get tinyMCE editor id if it exists.
-        if ($editorid = $this->get_editor_id()) {
-
-            // Set the value to the iframe and save it to the textarea.
-            $this->session->executeScript('
-                tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
-                tinyMCE.get("'.$editorid.'").save();
-            ');
-
-        } else {
-            // Set the value to a textarea otherwise.
-            parent::set_value($value);
+        $lastexception = null;
+
+        // We want the editor to be ready, otherwise the value can not
+        // be set and an exception is thrown.
+        for ($i = 0; $i < behat_base::WAIT_FOR_EDITOR_RETRIES; $i++) {
+            try {
+                // Get tinyMCE editor id if it exists.
+                if ($editorid = $this->get_editor_id()) {
+
+                    // Set the value to the iframe and save it to the textarea.
+                    $this->session->executeScript('
+                        tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
+                        tinyMCE.get("'.$editorid.'").save();
+                    ');
+
+                } else {
+                    // Set the value to a textarea otherwise.
+                    parent::set_value($value);
+                }
+                return;
+
+            } catch (Exception $e) {
+                // Catching any kind of exception and ignoring it until times out.
+                $lastexception = $e;
+
+                // Waiting 0.1 seconds.
+                usleep(100000);
+            }
         }
+
+        // If it is not available we throw the last exception.
+        throw $lastexception;
     }
 
     /**
@@ -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 = '';
+
+        // We want the editor to be ready to return the correct value, sometimes the
+        // page loads too fast and the returned value may be '' if the editor didn't
+        // have enough time to load completely despite having a different value.
+        for ($i = 0; $i < behat_base::WAIT_FOR_EDITOR_RETRIES; $i++) {
+            try {
+
+                // Get tinyMCE editor id if it exists.
+                if ($editorid = $this->get_editor_id()) {
+
+                    // Save the current iframe value in case default value has been edited.
+                    $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+                }
+
+                $lastoutcome = $this->field->getValue();
+
+                // We only want to wait until it times out if the value is empty.
+                if ($lastoutcome != '') {
+                    return $lastoutcome;
+                }
+
+            } catch (Exception $e) {
+                // Catching any kind of exception and ignoring it until times out.
+                $lastoutcome = $e;
+
+                // Waiting 0.1 seconds.
+                usleep(100000);
+            }
+        }
 
-            // Save the current iframe value in case default value has been edited.
-            $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If it is not available we throw the last exception.
+        if (is_a($lastoutcome, 'Exception')) {
+            throw $lastoutcome;
         }
 
-        return $this->field->getValue();
+        // Return the value if there are no exceptions it will be '' at this point
+        return $lastoutcome;
     }
 
     /**
index d5d43e1..8db4579 100644 (file)
@@ -137,6 +137,7 @@ class behat_form_field {
             $classname = 'behat_form_select';
 
         } else {
+            // We can not provide a closer field type.
             return false;
         }
 
@@ -154,4 +155,24 @@ class behat_form_field {
         return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
     }
 
+    /**
+     * Gets the field internal id used by selenium wire protocol.
+     *
+     * Only available when running_javascript().
+     *
+     * @throws coding_exception
+     * @return int
+     */
+    protected function get_internal_field_id() {
+
+        if (!$this->running_javascript()) {
+            throw new coding_exception('You can only get an internal ID using the selenium driver.');
+        }
+
+        return $this->session->
+            getDriver()->
+            getWebDriverSession()->
+            element('xpath', $this->field->getXPath())->
+            getID();
+    }
 }
index 5be8135..2805a97 100644 (file)
@@ -40,40 +40,82 @@ class behat_form_select extends behat_form_field {
     /**
      * Sets the value of a single select.
      *
+     * Seems an easy select, but there are lots of combinations
+     * of browsers and operative systems and each one manages the
+     * autosubmits and the multiple option selects in a diferent way.
+     *
      * @param string $value
      * @return void
      */
     public function set_value($value) {
-        $this->field->selectOption($value);
 
-        // Adding a click as Selenium requires it to fire some JS events.
+        // In some browsers we select an option and it triggers all the
+        // autosubmits and works as expected but not in all of them, so we
+        // try to catch all the possibilities to make this function work as
+        // expected.
+
+        // Get the internal id of the element we are going to click.
+        // This kind of internal IDs are only available in the selenium wire
+        // protocol, so only available using selenium drivers, phantomjs and family.
         if ($this->running_javascript()) {
+            $currentelementid = $this->get_internal_field_id();
+        }
 
-            // In some browsers the selectOption actions can perform a page reload
-            // so we need to ensure the element is still available to continue interacting
-            // with it. We don't wait here.
-            if (!$this->session->getDriver()->find($this->field->getXpath())) {
-                return;
-            }
+        // Here we select an option.
+        $this->field->selectOption($value);
 
-            // Single select needs an extra click in the option.
-            if (!$this->field->hasAttribute('multiple')) {
+        // With JS disabled this is enough and we finish here.
+        if (!$this->running_javascript()) {
+            return;
+        }
 
-                $value = $this->session->getSelectorsHandler()->xpathLiteral($value);
+        // With JS enabled we add more clicks as some selenium
+        // drivers requires it to fire JS events.
 
-                // Using the driver direcly because Element methods are messy when dealing
-                // with elements inside containers.
-                $optionxpath = $this->field->getXpath() .
-                    "/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
-                $optionnodes = $this->session->getDriver()->find($optionxpath);
-                if ($optionnodes) {
-                    current($optionnodes)->click();
-                }
+        // In some browsers the selectOption actions can perform a form submit or reload page
+        // so we need to ensure the element is still available to continue interacting
+        // with it. We don't wait here.
+        $selectxpath = $this->field->getXpath();
+        if (!$this->session->getDriver()->find($selectxpath)) {
+            return;
+        }
 
-            } else {
-                // Multiple ones needs the click in the select.
-                $this->field->click();
+        // We also check the selenium internal element id, if it have changed
+        // we are dealing with an autosubmit that was already executed, and we don't to
+        // execute anything else as the action we wanted was already performed.
+        if ($currentelementid != $this->get_internal_field_id()) {
+            return;
+        }
+
+        // We also check that the option is still there. We neither wait.
+        $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral($value);
+        $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
+        if (!$this->session->getDriver()->find($optionxpath)) {
+            return;
+        }
+
+        // Single select sometimes needs an extra click in the option.
+        if (!$this->field->hasAttribute('multiple')) {
+
+            // Using the driver direcly because Element methods are messy when dealing
+            // with elements inside containers.
+            $optionnodes = $this->session->getDriver()->find($optionxpath);
+            if ($optionnodes) {
+                current($optionnodes)->click();
             }
+
+        } else {
+            // Multiple ones needs the click in the select.
+            $this->field->click();
+
+            // We ensure that the option is still there.
+            if (!$this->session->getDriver()->find($optionxpath)) {
+                return;
+            }
+
+            // Repeating the select as some drivers (chrome that I know) are moving
+            // to another option after the general select field click above.
+            $this->field->selectOption($value);
         }
     }
 
index dad8346..5820690 100644 (file)
@@ -45,6 +45,7 @@ Feature: Add or remove items from the TinyMCE editor toolbar
     Given I follow "Course 1"
     And I turn editing mode on
     When I add a "Database" to section "1"
+    And I wait until "#id_introeditor_tbl" "css_element" exists
     Then "#id_introeditor_tbl .mce_bold" "css_element" should exists
     And "#id_introeditor_tbl .mce_anchor" "css_element" should not exists
     And I press "Cancel"
index e5eaa84..e16fda3 100644 (file)
@@ -60,30 +60,6 @@ class behat_forms extends behat_base {
         $buttonnode->press();
     }
 
-    /**
-     * Try a few times to set a field value as it may not be visible yet (TinyMCE).
-     *
-     * @param string $field
-     * @param string $value
-     */
-    public function set_field_value($field, $value) {
-        $lastexception = null;
-        // Spin on this - certain fields, e.g. text editors (I'm looking at you TinyMCE) load slowly and randomly.
-        $retries = 5;
-        while ($retries > 0) {
-            try {
-                $field->set_value($value);
-                return;
-            } catch (Exception $e) {
-                usleep(100000);
-                $retries--;
-                $lastexception = $e;
-            }
-        }
-        // If we timeout - throw the last exception.
-        throw $lastexception;
-    }
-
     /**
      * Fills a moodle form with field/value data.
      *
@@ -93,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();
 
@@ -108,7 +87,7 @@ class behat_forms extends behat_base {
             $field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
 
             // Delegates to the field class.
-            $this->set_field_value($field, $value);
+            $field->set_value($value);
         }
     }
 
@@ -195,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);
     }
 
     /**
@@ -249,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();
     }
@@ -262,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();
     }
index 8785612..ad48a2a 100644 (file)
@@ -124,7 +124,20 @@ class behat_general extends behat_base {
      * @param string $iframename
      */
     public function switch_to_iframe($iframename) {
-        $this->getSession()->switchToIFrame($iframename);
+
+        // We spin to give time to the iframe to be loaded.
+        // Using extended timeout as we don't know about which
+        // kind of iframe will be loaded.
+        $this->spin(
+            function($context, $iframename) {
+                $context->getSession()->switchToIFrame($iframename);
+
+                // If no exception we are done.
+                return true;
+            },
+            $iframename,
+            self::EXTENDED_TIMEOUT
+        );
     }
 
     /**
@@ -173,6 +186,7 @@ class behat_general extends behat_base {
     public function click_link($link) {
 
         $linknode = $this->find_link($link);
+        $this->ensure_node_is_visible($linknode);
         $linknode->click();
     }
 
@@ -202,7 +216,41 @@ 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 provided element selector exists in the DOM
+     *
+     * Using the protected method as this method will be usually
+     * called by other methods which are not returning a set of
+     * steps and performs the actions directly, so it would not
+     * be executed if it returns another step.
+
+     * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" exists$/
+     * @param string $element
+     * @param string $selector
+     * @return void
+     */
+    public function wait_until_exists($element, $selectortype) {
+        $this->ensure_element_exists($element, $selectortype);
+    }
+
+    /**
+     * Waits until the provided element does not exist in the DOM
+     *
+     * Using the protected method as this method will be usually
+     * called by other methods which are not returning a set of
+     * steps and performs the actions directly, so it would not
+     * be executed if it returns another step.
+
+     * @Given /^I wait until "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" does not exist$/
+     * @param string $element
+     * @param string $selector
+     * @return void
+     */
+    public function wait_until_does_not_exists($element, $selectortype) {
+        $this->ensure_element_does_not_exist($element, $selectortype);
     }
 
     /**
@@ -230,6 +278,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 +294,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();
     }
 
index e18e48d..1103060 100644 (file)
@@ -222,32 +222,68 @@ class behat_hooks extends behat_base {
     }
 
     /**
-     * Wait for JS to comlete.
+     * Wait for JS to complete before beginning interacting with the DOM.
      *
      * Executed only when running against a real browser.
      *
      * @BeforeStep @javascript
      */
     public function before_step_javascript($event) {
-        $lastpending = '';
+        $this->wait_for_pending_js();
+    }
+
+    /**
+     * Wait for JS to complete after finishing the step.
+     *
+     * With this we ensure that there are not AJAX calls
+     * still in progress.
+     *
+     * Executed only when running against a real browser.
+     *
+     * @AfterStep @javascript
+     */
+    public function after_step_javascript($event) {
+        $this->wait_for_pending_js();
+    }
+
+    /**
+     * Waits for all the JS to be loaded.
+     *
+     * @throws NoSuchWindow
+     * @throws UnknownError
+     * @return bool True or false depending whether all the JS is loaded or not.
+     */
+    protected function wait_for_pending_js() {
+
         // Wait for all pending JS to complete (max 10 seconds).
         for ($i = 0; $i < 100; $i++) {
             $pending = '';
             try {
-                $pending = ($this->getSession()->evaluateScript('return (M && M.util && M.util.pending_js) ? M.util.pending_js.join(":") : "not loaded";'));
+                $jscode = 'return ' . self::PAGE_READY_JS . ' ? "" : M.util.pending_js.join(":");';
+                $pending = $this->getSession()->evaluateScript($jscode);
             } catch (NoSuchWindow $nsw) {
+                // We catch an exception here, in case we just closed the window we were interacting with.
                 // No javascript is running if there is no window right?
                 $pending = '';
+            } catch (UnknownError $e) {
+                // Same exception as before, but some combinations of browser + OS reports it as an unknown error
+                // exception.
+                $pending = '';
             }
+
+            // If there are no pending JS we stop waiting.
             if ($pending === '') {
-                return;
+                return true;
             }
-            $lastpending = $pending;
+
             // 0.1 seconds.
             usleep(100000);
         }
+
         // Timeout waiting for JS to complete.
         // We could throw an exception here - as this is a likely indicator of slow JS or JS errors.
+        echo ' Slow JS, pending requests:' . $pending . ' ';
+        return false;
     }
 
     /**
index 2d5f2f0..562be1a 100644 (file)
@@ -95,7 +95,16 @@ class behat_permissions extends behat_base {
         try {
             $advancedtoggle = $this->find_button(get_string('showadvanced', 'form'));
             if ($advancedtoggle) {
-                $this->getSession()->getPage()->pressButton(get_string('showadvanced', 'form'));
+
+                // As we are interacting with a moodle form we wait for the editor to be ready
+                // otherwise we may have problems when setting values on it or clicking on elements
+                // as the position of the elements will change once the editor is loaded.
+                $this->ensure_editors_are_loaded();
+
+                $advancedtoggle->click();
+
+                // Wait for the page to load.
+                $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
             }
         } catch (Exception $e) {
             // We already are in advanced mode.
index feb613d..e9480c9 100644 (file)
@@ -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"
index 524bf70..ce26e8d 100644 (file)
@@ -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"
index 7f6435e..540cfc4 100644 (file)
@@ -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') . '"')
         );
index 9431641..45d3882 100644 (file)
@@ -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 |
index 7b17795..63669e6 100644 (file)
@@ -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
index 4dac65f..66ece1e 100644 (file)
@@ -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);
 
@@ -324,15 +287,8 @@ class behat_filepicker extends behat_files {
         $this->find_button(get_string('getfile', 'repository'))->click();
 
         if ($overwriteaction !== false) {
-            $this->getSession()->wait(1 * 1000, false);
             $this->find_button($overwriteaction)->click();
         }
-
-        // Ensure the file has been uploaded and all ajax processes finished.
-        $this->wait_until_return_to_form();
-
-        // Wait until file manager contents are updated.
-        $this->wait_until_contents_are_updated($filemanagernode);
     }
 
 }
index 9fa490e..050a97b 100644 (file)
@@ -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',
@@ -162,15 +158,9 @@ class behat_repository_upload extends behat_files {
         $submit->press();
 
         if ($overwriteaction !== false) {
-            $this->getSession()->wait(1 * 1000, false);
             $this->find_button($overwriteaction)->click();
         }
 
-        // Ensure the file has been uploaded and all ajax processes finished.
-        $this->wait_until_return_to_form();
-
-        // Wait until file manager contents are updated.
-        $this->wait_until_contents_are_updated($filemanagernode);
     }
 
 }