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
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
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} [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,6 +616,7 @@
|
|||
var behatSetField = function(field, value) {
|
||||
log('Action - Set field ' + field + ' to: ' + value);
|
||||
|
||||
if (window.BehatMoodleAppLegacy) {
|
||||
// Find input(s) with given placeholder.
|
||||
var escapedText = field.replace('"', '""');
|
||||
var exactMatches = [];
|
||||
|
@ -649,6 +663,14 @@
|
|||
if (!found) {
|
||||
return 'ERROR: No matches for text';
|
||||
}
|
||||
} else {
|
||||
const elements = findElementsBasedOnText(field);
|
||||
var found = elements.filter(element => element.matches('input, textarea'))[0];
|
||||
|
||||
if (!found) {
|
||||
return 'ERROR: No matches for text';
|
||||
}
|
||||
}
|
||||
|
||||
// Functions to get/set value depending on field type.
|
||||
var setValue;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
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
|
Loading…
Reference in New Issue