Merge pull request #17 from NoelDeMartin/MOBILE-3320

MOBILE-3320: Add push notification navigation tests
main
Dani Palou 2021-05-12 09:41:02 +02:00 committed by GitHub
commit 7fd2553f79
5 changed files with 175 additions and 77 deletions

View File

@ -93,7 +93,10 @@ Feature: Test course list shown on app start tab
When I press "Display options" near "Course overview" in the app When I press "Display options" near "Course overview" in the app
And I press "Filter my courses" 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 Then I should find "C3" in the app
And I should find "C4" in the app And I should find "C4" in the app
And I should find "Frog 3" in the app And I should find "Frog 3" in the app

View File

@ -203,7 +203,10 @@
*/ */
var isElementSelected = (element, container) => { var isElementSelected = (element, container) => {
const ariaCurrent = element.getAttribute('aria-current'); const ariaCurrent = element.getAttribute('aria-current');
if (ariaCurrent && ariaCurrent !== 'false') if (
(ariaCurrent && ariaCurrent !== 'false') ||
(element.getAttribute('aria-selected') === 'true')
)
return true; return true;
if (!element.parentElement || element.parentElement === container) 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 {HTMLElement} container Parent element to search the element within
* @param {string} text Text to look for * @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}"]`; const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"]`;
for (const foundByAttributes of container.querySelectorAll(attributesSelector)) { for (const foundByAttributes of container.querySelectorAll(attributesSelector)) {
if (isElementVisible(foundByAttributes, container)) if (!isElementVisible(foundByAttributes, container))
return foundByAttributes; continue;
elements.push(foundByAttributes);
} }
const treeWalker = document.createTreeWalker( const treeWalker = document.createTreeWalker(
@ -280,15 +286,18 @@
while (currentNode = treeWalker.nextNode()) { while (currentNode = treeWalker.nextNode()) {
if (currentNode instanceof Text) { if (currentNode instanceof Text) {
if (currentNode.textContent.includes(text)) { if (currentNode.textContent.includes(text)) {
return currentNode.parentElement; elements.push(currentNode.parentElement);
} }
continue; continue;
} }
const labelledBy = currentNode.getAttribute('aria-labelledby'); const labelledBy = currentNode.getAttribute('aria-labelledby');
if (labelledBy && container.querySelector(`#${labelledBy}`)?.innerText?.includes(text)) if (labelledBy && container.querySelector(`#${labelledBy}`)?.innerText?.includes(text)) {
return currentNode; elements.push(currentNode);
continue;
}
if (currentNode.shadowRoot) { if (currentNode.shadowRoot) {
for (const childNode of currentNode.shadowRoot.childNodes) { for (const childNode of currentNode.shadowRoot.childNodes) {
@ -303,47 +312,51 @@
} }
if (childNode.matches(attributesSelector)) { if (childNode.matches(attributesSelector)) {
return childNode; elements.push(childNode);
continue;
} }
const foundByText = findElementBasedOnTextWithin(childNode, text); elements.push(...findElementsBasedOnTextWithin(childNode, text));
if (foundByText) {
return foundByText;
}
} }
} }
} }
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} text Text (full or partial)
* @param {string} [near] Optional 'near' text - if specified, must have a single match on page * @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'); 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; let container = topContainer;
if (topContainer && near) { if (topContainer && near) {
const nearElement = findElementBasedOnText(near); const nearElements = findElementsBasedOnText(near);
if (!nearElement) { if (nearElements.length === 0) {
return; 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 { do {
const node = findElementBasedOnTextWithin(container, text); const elements = findElementsBasedOnTextWithin(container, text);
if (node) { if (elements.length > 0) {
return node; return elements;
} }
} while ((container = container.parentElement) && container !== topContainer); } while ((container = container.parentElement) && container !== topContainer);
return [];
}; };
/** /**
@ -398,10 +411,10 @@
} else { } else {
switch (button) { switch (button) {
case 'back': case 'back':
foundButton = findElementBasedOnText('Back'); foundButton = findElementsBasedOnText('Back')[0];
break; break;
case 'main menu': case 'main menu':
foundButton = findElementBasedOnText('more', 'Notifications'); foundButton = findElementsBasedOnText('more', 'Notifications')[0];
break; break;
default: default:
return 'ERROR: Unsupported standard button type'; return 'ERROR: Unsupported standard button type';
@ -462,7 +475,7 @@
log(`Action - Find ${text}`); log(`Action - Find ${text}`);
try { try {
const element = findElementBasedOnText(text, near); const element = findElementsBasedOnText(text, near)[0];
if (!element) { if (!element) {
return 'ERROR: No matches for text'; return 'ERROR: No matches for text';
@ -502,7 +515,7 @@
log(`Action - Is Selected: "${text}"${near ? ` near "${near}"`: ''}`); log(`Action - Is Selected: "${text}"${near ? ` near "${near}"`: ''}`);
try { try {
const element = findElementBasedOnText(text, near); const element = findElementsBasedOnText(text, near)[0];
return isElementSelected(element, document.body) ? 'YES' : 'NO'; return isElementSelected(element, document.body) ? 'YES' : 'NO';
} catch (error) { } catch (error) {
@ -522,7 +535,7 @@
var found; var found;
try { try {
found = findElementBasedOnText(text, near); found = findElementsBasedOnText(text, near)[0];
if (!found) { if (!found) {
return 'ERROR: No matches for text'; return 'ERROR: No matches for text';
@ -603,51 +616,60 @@
var behatSetField = function(field, value) { var behatSetField = function(field, value) {
log('Action - Set field ' + field + ' to: ' + value); log('Action - Set field ' + field + ' to: ' + value);
// Find input(s) with given placeholder. if (window.BehatMoodleAppLegacy) {
var escapedText = field.replace('"', '""'); // Find input(s) with given placeholder.
var exactMatches = []; var escapedText = field.replace('"', '""');
var anyMatches = []; var exactMatches = [];
findPossibleMatches( var anyMatches = [];
'//input[contains(@placeholder, "' + escapedText + '")] |' + findPossibleMatches(
'//textarea[contains(@placeholder, "' + escapedText + '")] |' + '//input[contains(@placeholder, "' + escapedText + '")] |' +
'//core-rich-text-editor/descendant::div[contains(@data-placeholder-text, "' + '//textarea[contains(@placeholder, "' + escapedText + '")] |' +
escapedText + '")]', function(match) { '//core-rich-text-editor/descendant::div[contains(@data-placeholder-text, "' +
// Add to array depending on if it's an exact or partial match. escapedText + '")]', function(match) {
var placeholder; // Add to array depending on if it's an exact or partial match.
if (match.nodeName === 'DIV') { var placeholder;
placeholder = match.getAttribute('data-placeholder-text'); if (match.nodeName === 'DIV') {
} else { placeholder = match.getAttribute('data-placeholder-text');
placeholder = match.getAttribute('placeholder'); } else {
} placeholder = match.getAttribute('placeholder');
if (placeholder.trim() === field) { }
exactMatches.push(match); if (placeholder.trim() === field) {
} else { exactMatches.push(match);
anyMatches.push(match); } else {
} anyMatches.push(match);
}); }
});
// Select the resulting match. // Select the resulting match.
var found = null; var found = null;
do { do {
// If there is an exact text match, use that (regardless of other matches). // If there is an exact text match, use that (regardless of other matches).
if (exactMatches.length > 1) { if (exactMatches.length > 1) {
return 'ERROR: Too many exact placeholder matches for text'; return 'ERROR: Too many exact placeholder matches for text';
} else if (exactMatches.length) { } else if (exactMatches.length) {
found = exactMatches[0]; found = exactMatches[0];
break; 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 (!found) {
if (anyMatches.length > 1) { return 'ERROR: No matches for text';
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';
} }
// Functions to get/set value depending on field type. // Functions to get/set value depending on field type.

View File

@ -27,6 +27,7 @@
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Exception\DriverException; use Behat\Mink\Exception\DriverException;
use Behat\Mink\Exception\ExpectationException; 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 // 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. // page. If it's the main page, we just leave it there.
if ($situation === 'login') { 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'); $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(); $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. * Closes a popup by clicking on the 'backdrop' behind it.
* *

View File

@ -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

View File

@ -1,11 +1,11 @@
@app @javascript @app @javascript
Feature: It navigates properly between pages. Feature: It navigates properly in pages with a split-view component.
Background: Background:
Given the following "users" exist: Given the following "users" exist:
| username | | username |
| student1 | | student1 |
Given the following "courses" exist: And the following "courses" exist:
| fullname | shortname | | fullname | shortname |
| Course 2 | C2 | | Course 2 | C2 |
| Course 1 | C1 | | Course 1 | C1 |
@ -22,7 +22,7 @@ Feature: It navigates properly between pages.
| Grade category C1 | Grade item C1 | 20 | 40 | C1 | | Grade category C1 | Grade item C1 | 20 | 40 | C1 |
| Grade category C2 | Grade item C2 | 60 | 80 | C2 | | 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 # Open more tab
Given I enter the app Given I enter the app
@ -85,7 +85,7 @@ Feature: It navigates properly between pages.
And I should find "App settings" in the app And I should find "App settings" in the app
But I should not find "Back" 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 # Open more tab
Given I enter the app Given I enter the app