From 31fa467ff97a017a13786937443a02a61e92b29e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 11 May 2021 18:54:18 +0200 Subject: [PATCH] MOBILE-3320 behat: Push notifications navigation --- mod/course/tests/behat/app_courselist.feature | 5 +- tests/behat/app_behat_runtime.js | 164 ++++++++++-------- tests/behat/behat_app.php | 33 +++- .../navigation_pushnotifications.feature | 42 +++++ ...n.feature => navigation_splitview.feature} | 8 +- 5 files changed, 175 insertions(+), 77 deletions(-) create mode 100644 tests/behat/navigation_pushnotifications.feature rename tests/behat/{navigation.feature => navigation_splitview.feature} (95%) diff --git a/mod/course/tests/behat/app_courselist.feature b/mod/course/tests/behat/app_courselist.feature index 7a508f9f3..fc8adab44 100644 --- a/mod/course/tests/behat/app_courselist.feature +++ b/mod/course/tests/behat/app_courselist.feature @@ -93,7 +93,10 @@ Feature: Test course list shown on app start tab When I press "Display options" near "Course overview" in the app And I press "Filter my courses" in the app - And I set the field "Filter my courses" to "fr" in the app + + # TODO field should be "Filter my courses" + And I set the field "search text" to "fr" in the app + Then I should find "C3" in the app And I should find "C4" in the app And I should find "Frog 3" in the app diff --git a/tests/behat/app_behat_runtime.js b/tests/behat/app_behat_runtime.js index 1a244c42f..e5c99e3f9 100644 --- a/tests/behat/app_behat_runtime.js +++ b/tests/behat/app_behat_runtime.js @@ -203,7 +203,10 @@ */ var isElementSelected = (element, container) => { const ariaCurrent = element.getAttribute('aria-current'); - if (ariaCurrent && ariaCurrent !== 'false') + if ( + (ariaCurrent && ariaCurrent !== 'false') || + (element.getAttribute('aria-selected') === 'true') + ) return true; if (!element.parentElement || element.parentElement === container) @@ -238,18 +241,21 @@ }; /** - * Finds an element within a given container. + * Finds elements within a given container. * * @param {HTMLElement} container Parent element to search the element within * @param {string} text Text to look for - * @return {HTMLElement} Found element + * @return {HTMLElement} Elements containing the given text */ - var findElementBasedOnTextWithin = (container, text) => { + var findElementsBasedOnTextWithin = (container, text) => { + const elements = []; const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"]`; for (const foundByAttributes of container.querySelectorAll(attributesSelector)) { - if (isElementVisible(foundByAttributes, container)) - return foundByAttributes; + if (!isElementVisible(foundByAttributes, container)) + continue; + + elements.push(foundByAttributes); } const treeWalker = document.createTreeWalker( @@ -280,15 +286,18 @@ while (currentNode = treeWalker.nextNode()) { if (currentNode instanceof Text) { if (currentNode.textContent.includes(text)) { - return currentNode.parentElement; + elements.push(currentNode.parentElement); } continue; } const labelledBy = currentNode.getAttribute('aria-labelledby'); - if (labelledBy && container.querySelector(`#${labelledBy}`)?.innerText?.includes(text)) - return currentNode; + if (labelledBy && container.querySelector(`#${labelledBy}`)?.innerText?.includes(text)) { + elements.push(currentNode); + + continue; + } if (currentNode.shadowRoot) { for (const childNode of currentNode.shadowRoot.childNodes) { @@ -303,47 +312,51 @@ } if (childNode.matches(attributesSelector)) { - return childNode; + elements.push(childNode); + + continue; } - const foundByText = findElementBasedOnTextWithin(childNode, text); - - if (foundByText) { - return foundByText; - } + elements.push(...findElementsBasedOnTextWithin(childNode, text)); } } } + + return elements; }; /** - * Function to find an element based on its text or Aria label. + * Function to find elements based on their text or Aria label. * * @param {string} text Text (full or partial) * @param {string} [near] Optional 'near' text - if specified, must have a single match on page - * @return {HTMLElement} Found element + * @return {HTMLElement} Found elements */ - var findElementBasedOnText = function(text, near) { + var findElementsBasedOnText = function(text, near) { const topContainer = document.querySelector('ion-alert, ion-popover, ion-action-sheet, core-ion-tab.show-tab ion-page.show-page, ion-page.show-page, html'); let container = topContainer; if (topContainer && near) { - const nearElement = findElementBasedOnText(near); + const nearElements = findElementsBasedOnText(near); - if (!nearElement) { - return; + if (nearElements.length === 0) { + throw new Error('There was no match for near text') + } else if (nearElements.length > 1) { + throw new Error('Too many matches for near text'); } - container = nearElement.parentElement; + container = nearElements[0].parentElement; } do { - const node = findElementBasedOnTextWithin(container, text); + const elements = findElementsBasedOnTextWithin(container, text); - if (node) { - return node; + if (elements.length > 0) { + return elements; } } while ((container = container.parentElement) && container !== topContainer); + + return []; }; /** @@ -398,10 +411,10 @@ } else { switch (button) { case 'back': - foundButton = findElementBasedOnText('Back'); + foundButton = findElementsBasedOnText('Back')[0]; break; case 'main menu': - foundButton = findElementBasedOnText('more', 'Notifications'); + foundButton = findElementsBasedOnText('more', 'Notifications')[0]; break; default: return 'ERROR: Unsupported standard button type'; @@ -462,7 +475,7 @@ log(`Action - Find ${text}`); try { - const element = findElementBasedOnText(text, near); + const element = findElementsBasedOnText(text, near)[0]; if (!element) { return 'ERROR: No matches for text'; @@ -502,7 +515,7 @@ log(`Action - Is Selected: "${text}"${near ? ` near "${near}"`: ''}`); try { - const element = findElementBasedOnText(text, near); + const element = findElementsBasedOnText(text, near)[0]; return isElementSelected(element, document.body) ? 'YES' : 'NO'; } catch (error) { @@ -522,7 +535,7 @@ var found; try { - found = findElementBasedOnText(text, near); + found = findElementsBasedOnText(text, near)[0]; if (!found) { return 'ERROR: No matches for text'; @@ -603,51 +616,60 @@ var behatSetField = function(field, value) { log('Action - Set field ' + field + ' to: ' + value); - // Find input(s) with given placeholder. - var escapedText = field.replace('"', '""'); - var exactMatches = []; - var anyMatches = []; - findPossibleMatches( - '//input[contains(@placeholder, "' + escapedText + '")] |' + - '//textarea[contains(@placeholder, "' + escapedText + '")] |' + - '//core-rich-text-editor/descendant::div[contains(@data-placeholder-text, "' + - escapedText + '")]', function(match) { - // Add to array depending on if it's an exact or partial match. - var placeholder; - if (match.nodeName === 'DIV') { - placeholder = match.getAttribute('data-placeholder-text'); - } else { - placeholder = match.getAttribute('placeholder'); - } - if (placeholder.trim() === field) { - exactMatches.push(match); - } else { - anyMatches.push(match); - } - }); + if (window.BehatMoodleAppLegacy) { + // Find input(s) with given placeholder. + var escapedText = field.replace('"', '""'); + var exactMatches = []; + var anyMatches = []; + findPossibleMatches( + '//input[contains(@placeholder, "' + escapedText + '")] |' + + '//textarea[contains(@placeholder, "' + escapedText + '")] |' + + '//core-rich-text-editor/descendant::div[contains(@data-placeholder-text, "' + + escapedText + '")]', function(match) { + // Add to array depending on if it's an exact or partial match. + var placeholder; + if (match.nodeName === 'DIV') { + placeholder = match.getAttribute('data-placeholder-text'); + } else { + placeholder = match.getAttribute('placeholder'); + } + if (placeholder.trim() === field) { + exactMatches.push(match); + } else { + anyMatches.push(match); + } + }); - // Select the resulting match. - var found = null; - do { - // If there is an exact text match, use that (regardless of other matches). - if (exactMatches.length > 1) { - return 'ERROR: Too many exact placeholder matches for text'; - } else if (exactMatches.length) { - found = exactMatches[0]; - break; + // Select the resulting match. + var found = null; + do { + // If there is an exact text match, use that (regardless of other matches). + if (exactMatches.length > 1) { + return 'ERROR: Too many exact placeholder matches for text'; + } else if (exactMatches.length) { + found = exactMatches[0]; + break; + } + + // If there is one partial text match, use that. + if (anyMatches.length > 1) { + return 'ERROR: Too many partial placeholder matches for text'; + } else if (anyMatches.length) { + found = anyMatches[0]; + break; + } + } while (false); + + if (!found) { + return 'ERROR: No matches for text'; } + } else { + const elements = findElementsBasedOnText(field); + var found = elements.filter(element => element.matches('input, textarea'))[0]; - // If there is one partial text match, use that. - if (anyMatches.length > 1) { - return 'ERROR: Too many partial placeholder matches for text'; - } else if (anyMatches.length) { - found = anyMatches[0]; - break; + if (!found) { + return 'ERROR: No matches for text'; } - } while (false); - - if (!found) { - return 'ERROR: No matches for text'; } // Functions to get/set value depending on field type. diff --git a/tests/behat/behat_app.php b/tests/behat/behat_app.php index 8eec8fd7b..405ac532c 100644 --- a/tests/behat/behat_app.php +++ b/tests/behat/behat_app.php @@ -27,6 +27,7 @@ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); +use Behat\Gherkin\Node\TableNode; use Behat\Mink\Exception\DriverException; use Behat\Mink\Exception\ExpectationException; @@ -401,7 +402,7 @@ class behat_app extends behat_base { // If it's the login page, we automatically fill in the URL and leave it on the user/pass // page. If it's the main page, we just leave it there. if ($situation === 'login') { - $this->i_set_the_field_in_the_app('campus.example.edu', $CFG->wwwroot); + $this->i_set_the_field_in_the_app($islegacy ? 'campus.example.edu' : 'Your site', $CFG->wwwroot); $this->i_press_in_the_app($islegacy ? 'Connect!' : 'Connect to your site'); } @@ -456,6 +457,36 @@ class behat_app extends behat_base { $this->wait_for_pending_js(); } + /** + * Receives push notifications for forum events. + * + * @Given /^I receive a forum push notification for:$/ + * @param TableNode $data + */ + public function i_receive_a_forum_push_notification(TableNode $data) { + global $DB, $CFG; + + $data = (object) $data->getColumnsHash()[0]; + $module = $DB->get_record('course_modules', ['idnumber' => $data->module]); + $discussion = $DB->get_record('forum_discussions', ['name' => $data->discussion]); + $notification = json_encode([ + 'site' => md5($CFG->wwwroot . $data->username), + 'courseid' => $discussion->course, + 'moodlecomponent' => 'mod_forum', + 'name' => 'posts', + 'contexturl' => '', + 'notif' => 1, + 'customdata' => [ + 'discussionid' => $discussion->id, + 'cmid' => $module->id, + 'instance' => $discussion->forum, + ], + ]); + + $this->evaluate_script("return window.pushNotifications.notificationClicked($notification)"); + $this->wait_for_pending_js(); + } + /** * Closes a popup by clicking on the 'backdrop' behind it. * diff --git a/tests/behat/navigation_pushnotifications.feature b/tests/behat/navigation_pushnotifications.feature new file mode 100644 index 000000000..d36002e0c --- /dev/null +++ b/tests/behat/navigation_pushnotifications.feature @@ -0,0 +1,42 @@ +@app @javascript +Feature: It navigates properly after receiving push notifications. + + Background: + Given the following "users" exist: + | username | + | student1 | + | student2 | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum | Test forum | C1 | forum | + And the following forum discussions exist in course "Course 1": + | forum | user | name | message | + | Test forum | student1 | Forum topic | Forum message | + And the following config values are set as admin: + | forcelogout | 1 | tool_mobile | + + Scenario: Open a forum push notification + When I enter the app + And I log in as "student2" + And I press the main menu button in the app + And I press "Log out" in the app + And I press "Add" in the app + And I set the field "Your site" to "$WWWROOT" in the app + And I press "Connect to your site" in the app + And I log in as "student1" + And I receive a forum push notification for: + | username | course | module | discussion | + | student2 | C1 | forum | Forum topic | + Then I should find "Reconnect" in the app + + When I set the field "Password" to "student2" in the app + And I press "Log in" in the app + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app diff --git a/tests/behat/navigation.feature b/tests/behat/navigation_splitview.feature similarity index 95% rename from tests/behat/navigation.feature rename to tests/behat/navigation_splitview.feature index 5a02138e5..78ecda825 100644 --- a/tests/behat/navigation.feature +++ b/tests/behat/navigation_splitview.feature @@ -1,11 +1,11 @@ @app @javascript -Feature: It navigates properly between pages. +Feature: It navigates properly in pages with a split-view component. Background: Given the following "users" exist: | username | | student1 | - Given the following "courses" exist: + And the following "courses" exist: | fullname | shortname | | Course 2 | C2 | | Course 1 | C1 | @@ -22,7 +22,7 @@ Feature: It navigates properly between pages. | 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 + Scenario: Navigate in grades tab on mobile # Open more tab Given I enter the app @@ -85,7 +85,7 @@ Feature: It navigates properly between pages. 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 + Scenario: Navigate in grades tab on tablet # Open more tab Given I enter the app