diff --git a/tests/behat/app_behat_runtime.js b/tests/behat/app_behat_runtime.js index c725827f5..9a0e4bff2 100644 --- a/tests/behat/app_behat_runtime.js +++ b/tests/behat/app_behat_runtime.js @@ -194,6 +194,24 @@ return isElementVisible(element.parentElement, container); }; + /** + * Check if an element is selected. + * + * @param {HTMLElement} element Element + * @param {HTMLElement} container Container + * @returns {boolean} Whether the element is selected or not + */ + var isElementSelected = (element, container) => { + const ariaCurrent = element.getAttribute('aria-current'); + if (ariaCurrent && ariaCurrent !== 'false') + return true; + + if (!element.parentElement || element.parentElement === container) + return false; + + return isElementSelected(element.parentElement, container); + }; + /** * Generic shared function to find possible xpath matches within the document, that are visible, * and then process them using a callback function. @@ -330,43 +348,61 @@ */ var behatPressStandard = function(button) { log('Action - Click standard button: ' + button); - var selector; - switch (button) { - case 'back' : - selector = 'ion-navbar > button.back-button-md'; - break; - case 'main menu' : - // Change in app version 3.8. - selector = 'page-core-mainmenu .tab-button > ion-icon[aria-label=more], ' + - 'page-core-mainmenu .tab-button > ion-icon[aria-label=menu]'; - break; - case 'page menu' : - // This lang string was changed in app version 3.6. - selector = 'core-context-menu > button[aria-label=Info], ' + - 'core-context-menu > button[aria-label=Information], ' + - 'core-context-menu > button[aria-label="Display options"]'; - break; - default: - return 'ERROR: Unsupported standard button type'; - } - var buttons = Array.from(document.querySelectorAll(selector)); + + // Find button var foundButton = null; - var tooMany = false; - buttons.forEach(function(button) { - if (button.offsetParent) { - if (foundButton === null) { - foundButton = button; - } else { - tooMany = true; - } + + if (window.BehatMoodleAppLegacy) { + var selector; + switch (button) { + case 'back' : + selector = 'ion-navbar > button.back-button-md'; + break; + case 'main menu' : + // Change in app version 3.8. + selector = 'page-core-mainmenu .tab-button > ion-icon[aria-label=more], ' + + 'page-core-mainmenu .tab-button > ion-icon[aria-label=menu]'; + break; + case 'page menu' : + // This lang string was changed in app version 3.6. + selector = 'core-context-menu > button[aria-label=Info], ' + + 'core-context-menu > button[aria-label=Information], ' + + 'core-context-menu > button[aria-label="Display options"]'; + break; + default: + return 'ERROR: Unsupported standard button type'; + } + var buttons = Array.from(document.querySelectorAll(selector)); + var tooMany = false; + buttons.forEach(function(button) { + if (button.offsetParent) { + if (foundButton === null) { + foundButton = button; + } else { + tooMany = true; + } + } + }); + if (!foundButton) { + return 'ERROR: Could not find button'; + } + if (tooMany) { + return 'ERROR: Found too many buttons'; + } + } else { + switch (button) { + case 'back': + foundButton = findElementBasedOnText('Back'); + break; + case 'main menu': + foundButton = findElementBasedOnText('more', 'Notifications'); + break; + default: + return 'ERROR: Unsupported standard button type'; } - }); - if (!foundButton) { - return 'ERROR: Could not find button'; - } - if (tooMany) { - return 'ERROR: Found too many buttons'; } + + // Click button foundButton.click(); // Mark busy until the button click finishes processing. @@ -449,6 +485,25 @@ return window.appProvider.appCtrl.getRootNavs()[0]; }; + /** + * Check whether an item is selected or not. + * + * @param {string} text Text (full or partial) + * @param {string} near Optional 'near' text + * @return {string} YES or NO if successful, or ERROR: followed by message + */ + var behatIsSelected = function(text, near) { + log(`Action - Is Selected: "${text}"${near ? ` near "${near}"`: ''}`); + + try { + const element = findElementBasedOnText(text, near); + + return isElementSelected(element, document.body) ? 'YES' : 'NO'; + } catch (error) { + return 'ERROR: ' + error.message; + } + } + /** * Function to press arbitrary item based on its text or Aria label. * @@ -639,6 +694,7 @@ pressStandard : behatPressStandard, closePopup : behatClosePopup, find : behatFind, + isSelected : behatIsSelected, press : behatPress, setField : behatSetField, getHeader : behatGetHeader, diff --git a/tests/behat/behat_app.php b/tests/behat/behat_app.php index c1c14c80b..8eec8fd7b 100644 --- a/tests/behat/behat_app.php +++ b/tests/behat/behat_app.php @@ -120,6 +120,40 @@ class behat_app extends behat_base { $this->wait_for_pending_js(); } + /** + * Check if elements are selected in the app. + * + * @Then /^"(?P(?:[^"]|\\")*)"(?: near "(?P(?:[^"]|\\")*)")? should(?P not)? be selected in the app$/ + * @param string $text + */ + public function be_selected_in_the_app($text, $near='', $not='') { + $not = !empty($not); + $text = addslashes_js($text); + $near = addslashes_js($near); + + $this->spin(function() use ($not, $text, $near) { + $result = $this->evaluate_script("return window.behat.isSelected(\"$text\", \"$near\");"); + + switch ($result) { + case 'YES': + if ($not) { + throw new ExpectationException("Item was selected and shouldn't have", $this->getSession()->getDriver()); + } + break; + case 'NO': + if (!$not) { + throw new ExpectationException("Item wasn't selected and should have", $this->getSession()->getDriver()); + } + break; + default: + throw new DriverException('Error finding item - ' . $result); + } + + return true; + }); + $this->wait_for_pending_js(); + } + /** * Checks the Behat setup - tags and configuration. * diff --git a/tests/behat/navigation.feature b/tests/behat/navigation.feature new file mode 100644 index 000000000..5a02138e5 --- /dev/null +++ b/tests/behat/navigation.feature @@ -0,0 +1,133 @@ +@app @javascript +Feature: It navigates properly between pages. + + Background: + Given the following "users" exist: + | username | + | student1 | + Given the following "courses" exist: + | fullname | shortname | + | Course 2 | C2 | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + And the following "grade categories" exist: + | fullname | course | + | Grade category C1 | C1 | + | Grade category C2 | C2 | + And the following "grade items" exist: + | gradecategory | itemname | grademin | grademax | course | + | Grade category C1 | Grade item C1 | 20 | 40 | C1 | + | Grade category C2 | Grade item C2 | 60 | 80 | C2 | + + Scenario: Navigate between split-view items in mobiles + + # Open more tab + Given I enter the app + And I log in as "student1" + And I press the main menu button in the app + + # Open grades tab + When I press "Grades" in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + + # Open C1 course grades + When I press "Course 1" in the app + Then the header should be "Grades" in the app + And I should find "Grade category C1" in the app + + # Open C1 grade item + When I press "Grade item C1" in the app + Then the header should be "Grade" in the app + And I should find "20" near "Range" in the app + And I should find "40" near "Range" in the app + + # Go back to course grades + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Grade category C1" in the app + + # Go back to grades tab + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + + # Open C2 course grades + When I press "Course 2" in the app + Then the header should be "Grades" in the app + And I should find "Grade category C2" in the app + + # Open C2 grade item + When I press "Grade item C2" in the app + Then the header should be "Grade" in the app + And I should find "60" near "Range" in the app + And I should find "80" near "Range" in the app + + # Go back to course grades + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Grade category C2" in the app + + # Go back to grades tab + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + + # Go back to more tab + When I press the back button in the app + Then I should find "Grades" in the app + And I should find "App settings" in the app + But I should not find "Back" in the app + + Scenario: Navigate between split-view items in tablets + + # Open more tab + Given I enter the app + And I change viewport size to "1200x640" + And I log in as "student1" + + # Open grades tab + When I press "Grades" in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + And I should find "Grade category C1" in the app + + # Open C1 course grades + When I press "Grade item C1" in the app + Then the header should be "Grades" in the app + And I should find "Grade category C1" in the app + And I should find "20" near "Range" in the app + And I should find "40" near "Range" in the app + + # Go back to grades tab + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + + # Select C2 course + When I press "Course 2" in the app + Then the header should be "Grades" in the app + And "Course 2" should be selected in the app + And I should find "Grade category C2" in the app + + # Open C2 course grades + When I press "Grade item C2" in the app + Then the header should be "Grades" in the app + And I should find "Grade category C2" in the app + And I should find "60" near "Range" in the app + And I should find "80" near "Range" in the app + + # Go back to grades tab + When I press the back button in the app + Then the header should be "Grades" in the app + And I should find "Course 1" in the app + And I should find "Course 2" in the app + But I should not find "Back" in the app