Merge pull request #17 from NoelDeMartin/MOBILE-3320
MOBILE-3320: Add push notification navigation testsmain
commit
7fd2553f79
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue