forked from EVOgeek/Vmeda.Online
MOBILE-3738 behat: Prepare 3.9.5 tests
parent
d933421779
commit
17006dcc4e
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_assign @app @javascript
|
@mod @mod_assign @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of assignment activity in app
|
Feature: Test basic usage of assignment activity in app
|
||||||
In order to participate in the assignment while using the mobile app
|
In order to participate in the assignment while using the mobile app
|
||||||
I need basic assignment functionality to work
|
I need basic assignment functionality to work
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_chat @app @javascript
|
@mod @mod_chat @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of chat in app
|
Feature: Test basic usage of chat in app
|
||||||
As a student
|
As a student
|
||||||
I need basic chat functionality to work
|
I need basic chat functionality to work
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_choice @app @javascript
|
@mod @mod_choice @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of choice activity in app
|
Feature: Test basic usage of choice activity in app
|
||||||
In order to participate in the choice while using the mobile app
|
In order to participate in the choice while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_comments @app @javascript
|
@mod @mod_comments @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of comments in app
|
Feature: Test basic usage of comments in app
|
||||||
In order to participate in the comments while using the mobile app
|
In order to participate in the comments while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_course @app @javascript
|
@mod @mod_course @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of one course in app
|
Feature: Test basic usage of one course in app
|
||||||
In order to participate in one course while using the mobile app
|
In order to participate in one course while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@core @core_course @app @javascript
|
@core @core_course @app @app_upto3.9.4 @javascript
|
||||||
Feature: Check course completion feature.
|
Feature: Check course completion feature.
|
||||||
In order to track the progress of the course on mobile device
|
In order to track the progress of the course on mobile device
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -22,17 +22,17 @@ Feature: Test course list shown on app start tab
|
||||||
Scenario: View courses (shortnames not displayed)
|
Scenario: View courses (shortnames not displayed)
|
||||||
When I enter the app
|
When I enter the app
|
||||||
And I log in as "student1"
|
And I log in as "student1"
|
||||||
Then I should see "Course 1"
|
Then I should find "Course 1" in the app
|
||||||
But I should not see "Course 2"
|
But I should not find "Course 2" in the app
|
||||||
But I should not see "C1"
|
But I should not find "C1" in the app
|
||||||
But I should not see "C2"
|
But I should not find "C2" in the app
|
||||||
|
|
||||||
When I enter the app
|
When I enter the app
|
||||||
And I log in as "student2"
|
And I log in as "student2"
|
||||||
Then I should see "Course 1"
|
Then I should find "Course 1" in the app
|
||||||
And I should see "Course 2"
|
And I should find "Course 2" in the app
|
||||||
But I should not see "C1"
|
But I should not find "C1" in the app
|
||||||
But I should not see "C2"
|
But I should not find "C2" in the app
|
||||||
|
|
||||||
Scenario: Filter courses
|
Scenario: Filter courses
|
||||||
Given the following config values are set as admin:
|
Given the following config values are set as admin:
|
||||||
|
@ -78,26 +78,46 @@ Feature: Test course list shown on app start tab
|
||||||
| student2 | Z10 | student |
|
| student2 | Z10 | student |
|
||||||
When I enter the app
|
When I enter the app
|
||||||
And I log in as "student2"
|
And I log in as "student2"
|
||||||
Then I press "Display options" near "Course overview" in the app
|
Then I should find "C1" in the app
|
||||||
Then I should see "C1"
|
And I should find "C2" in the app
|
||||||
And I should see "C2"
|
And I should find "C3" in the app
|
||||||
And I should see "C3"
|
And I should find "C4" in the app
|
||||||
And I should see "C4"
|
And I should find "C5" in the app
|
||||||
And I should see "C5"
|
And I should find "C6" in the app
|
||||||
And I should see "C6"
|
And I should find "Course 1" in the app
|
||||||
Then I press "Filter my courses" in the app
|
And I should find "Course 2" in the app
|
||||||
And I set the field "Filter my courses" to "fr" in the app
|
And I should find "Frog 3" in the app
|
||||||
Then I should not see "C1"
|
And I should find "Frog 4" in the app
|
||||||
And I should not see "C2"
|
And I should find "Course 5" in the app
|
||||||
And I should see "C3"
|
And I should find "Toad 6" in the app
|
||||||
And I should see "C4"
|
|
||||||
And I should not see "C5"
|
When I press "Display options" near "Course overview" in the app
|
||||||
And I should not see "C6"
|
|
||||||
And 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
|
||||||
Then I should see "C1"
|
And I set the field "Filter my courses" to "fr" in the app
|
||||||
And I should see "C2"
|
Then I should find "C3" in the app
|
||||||
And I should see "C3"
|
And I should find "C4" in the app
|
||||||
And I should see "C4"
|
And I should find "Frog 3" in the app
|
||||||
And I should see "C5"
|
And I should find "Frog 4" in the app
|
||||||
And I should see "C6"
|
But I should not find "C1" in the app
|
||||||
|
And I should not find "C2" in the app
|
||||||
|
And I should not find "C5" in the app
|
||||||
|
And I should not find "C6" in the app
|
||||||
|
And I should not find "Course 1" in the app
|
||||||
|
And I should not find "Course 2" in the app
|
||||||
|
And I should not find "Course 5" in the app
|
||||||
|
And I should not find "Toad 6" in the app
|
||||||
|
|
||||||
|
When I press "Display options" near "Course overview" in the app
|
||||||
|
And I press "Filter my courses" in the app
|
||||||
|
Then I should find "C1" in the app
|
||||||
|
And I should find "C2" in the app
|
||||||
|
And I should find "C3" in the app
|
||||||
|
And I should find "C4" in the app
|
||||||
|
And I should find "C5" in the app
|
||||||
|
And I should find "C6" 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 "Frog 3" in the app
|
||||||
|
And I should find "Frog 4" in the app
|
||||||
|
And I should find "Course 5" in the app
|
||||||
|
And I should find "Toad 6" in the app
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_courses @app @javascript
|
@mod @mod_courses @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of courses in app
|
Feature: Test basic usage of courses in app
|
||||||
In order to participate in the courses while using the mobile app
|
In order to participate in the courses while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_data @app @javascript
|
@mod @mod_data @app @app_upto3.9.4 @javascript
|
||||||
Feature: Users can manage entries in database activities
|
Feature: Users can manage entries in database activities
|
||||||
In order to populate databases
|
In order to populate databases
|
||||||
As a user
|
As a user
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_data @app @javascript
|
@mod @mod_data @app @app_upto3.9.4 @javascript
|
||||||
Feature: Users can store entries in database activities when offline and sync when online
|
Feature: Users can store entries in database activities when offline and sync when online
|
||||||
In order to populate databases while offline
|
In order to populate databases while offline
|
||||||
As a user
|
As a user
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_forum @app @javascript
|
@mod @mod_forum @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of forum activity in app
|
Feature: Test basic usage of forum activity in app
|
||||||
In order to participate in the forum while using the mobile app
|
In order to participate in the forum while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_glossary @app @javascript
|
@mod @mod_glossary @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of glossary in app
|
Feature: Test basic usage of glossary in app
|
||||||
In order to participate in the glossaries while using the mobile app
|
In order to participate in the glossaries while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_login @app @javascript
|
@mod @mod_login @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of login in app
|
Feature: Test basic usage of login in app
|
||||||
I need basic login functionality to work
|
I need basic login functionality to work
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_messages @app @javascript
|
@mod @mod_messages @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of messages in app
|
Feature: Test basic usage of messages in app
|
||||||
In order to participate with messages while using the mobile app
|
In order to participate with messages while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_quiz @app @javascript
|
@mod @mod_quiz @app @app_upto3.9.4 @javascript
|
||||||
Feature: Attempt a quiz in app
|
Feature: Attempt a quiz in app
|
||||||
As a student
|
As a student
|
||||||
In order to demonstrate what I know
|
In order to demonstrate what I know
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_quiz @app @javascript
|
@mod @mod_quiz @app @app_upto3.9.4 @javascript
|
||||||
Feature: Attempt a quiz in app
|
Feature: Attempt a quiz in app
|
||||||
As a student
|
As a student
|
||||||
In order to demonstrate what I know
|
In order to demonstrate what I know
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@mod @mod_survey @app @javascript
|
@mod @mod_survey @app @app_upto3.9.4 @javascript
|
||||||
Feature: Test basic usage of survey activity in app
|
Feature: Test basic usage of survey activity in app
|
||||||
In order to participate in surveys while using the mobile app
|
In order to participate in surveys while using the mobile app
|
||||||
As a student
|
As a student
|
||||||
|
|
|
@ -174,6 +174,26 @@
|
||||||
var observer = new MutationObserver(mutationCallback);
|
var observer = new MutationObserver(mutationCallback);
|
||||||
observer.observe(document, {attributes: true, childList: true, subtree: true});
|
observer.observe(document, {attributes: true, childList: true, subtree: true});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element is visible.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element Element
|
||||||
|
* @param {HTMLElement} container Container
|
||||||
|
* @returns {boolean} Whether the element is visible or not
|
||||||
|
*/
|
||||||
|
var isElementVisible = (element, container) => {
|
||||||
|
if (element.getAttribute('aria-hidden') === 'true' || getComputedStyle(element).display === 'none')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (element.parentElement === container)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!element.parentElement)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return isElementVisible(element.parentElement, container);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic shared function to find possible xpath matches within the document, that are visible,
|
* Generic shared function to find possible xpath matches within the document, that are visible,
|
||||||
* and then process them using a callback function.
|
* and then process them using a callback function.
|
||||||
|
@ -199,194 +219,107 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an element 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
|
||||||
|
*/
|
||||||
|
var findElementBasedOnTextWithin = (container, text) => {
|
||||||
|
const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"]`;
|
||||||
|
|
||||||
|
for (const foundByAttributes of container.querySelectorAll(attributesSelector)) {
|
||||||
|
if (isElementVisible(foundByAttributes, container))
|
||||||
|
return foundByAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeWalker = document.createTreeWalker(
|
||||||
|
container,
|
||||||
|
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT | NodeFilter.SHOW_TEXT,
|
||||||
|
{
|
||||||
|
acceptNode: node => {
|
||||||
|
if (
|
||||||
|
node instanceof HTMLStyleElement ||
|
||||||
|
node instanceof HTMLLinkElement ||
|
||||||
|
node instanceof HTMLScriptElement
|
||||||
|
)
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
|
||||||
|
if (
|
||||||
|
node instanceof HTMLElement && (
|
||||||
|
node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let currentNode;
|
||||||
|
while (currentNode = treeWalker.nextNode()) {
|
||||||
|
if (currentNode instanceof Text) {
|
||||||
|
if (currentNode.textContent.includes(text)) {
|
||||||
|
return currentNode.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelledBy = currentNode.getAttribute('aria-labelledby');
|
||||||
|
if (labelledBy && container.querySelector(`#${labelledBy}`)?.innerText?.includes(text))
|
||||||
|
return currentNode;
|
||||||
|
|
||||||
|
if (currentNode.shadowRoot) {
|
||||||
|
for (const childNode of currentNode.shadowRoot.childNodes) {
|
||||||
|
if (!childNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childNode.matches(attributesSelector)) {
|
||||||
|
return childNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundByText = findElementBasedOnTextWithin(childNode, text);
|
||||||
|
|
||||||
|
if (foundByText) {
|
||||||
|
return foundByText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to find an element based on its text or Aria label.
|
* Function to find an element based on its 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 element
|
||||||
* @throws {string} Error message beginning 'ERROR:' if something went wrong
|
|
||||||
*/
|
*/
|
||||||
var findElementBasedOnText = function(text, near) {
|
var findElementBasedOnText = function(text, near) {
|
||||||
// Find all the elements that contain this text (and don't have a child element that
|
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');
|
||||||
// contains it - i.e. the most specific elements).
|
let container = topContainer;
|
||||||
var escapedText = text.replace('"', '""');
|
|
||||||
var exactMatches = [];
|
|
||||||
var anyMatches = [];
|
|
||||||
findPossibleMatches('//*[contains(normalize-space(.), "' + escapedText +
|
|
||||||
'") and not(child::*[contains(normalize-space(.), "' + escapedText + '")])]',
|
|
||||||
function(match) {
|
|
||||||
// Get the text. Note that innerText returns capitalised values for Android buttons
|
|
||||||
// for some reason, so we'll have to do a case-insensitive match.
|
|
||||||
var matchText = match.innerText.trim().toLowerCase();
|
|
||||||
|
|
||||||
// Let's just check - is this actually a label for something else? If so we will click
|
if (topContainer && near) {
|
||||||
// that other thing instead.
|
const nearElement = findElementBasedOnText(near);
|
||||||
var labelId = document.evaluate('string(ancestor-or-self::ion-label[@id][1]/@id)', match).stringValue;
|
|
||||||
if (labelId) {
|
if (!nearElement) {
|
||||||
var target = document.querySelector('*[aria-labelledby=' + labelId + ']');
|
return;
|
||||||
if (target) {
|
|
||||||
match = target;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to array depending on if it's an exact or partial match.
|
container = nearElement.parentElement;
|
||||||
if (matchText === text.toLowerCase()) {
|
|
||||||
exactMatches.push(match);
|
|
||||||
} else {
|
|
||||||
anyMatches.push(match);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find all the Aria labels that contain this text.
|
|
||||||
var exactLabelMatches = [];
|
|
||||||
var anyLabelMatches = [];
|
|
||||||
findPossibleMatches('//*[@aria-label and contains(@aria-label, "' + escapedText + '")]' +
|
|
||||||
'| //a[@title and contains(@title, "' + escapedText + '")]' +
|
|
||||||
'| //img[@alt and contains(@alt, "' + escapedText + '")]', function(match) {
|
|
||||||
// Add to array depending on if it's an exact or partial match.
|
|
||||||
var attributeData = match.getAttribute('aria-label') ||
|
|
||||||
match.getAttribute('title') ||
|
|
||||||
match.getAttribute('alt');
|
|
||||||
if (attributeData.trim() === text) {
|
|
||||||
exactLabelMatches.push(match);
|
|
||||||
} else {
|
|
||||||
anyLabelMatches.push(match);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the 'near' text is set, use it to filter results.
|
|
||||||
var nearAncestors = [];
|
|
||||||
if (near !== undefined) {
|
|
||||||
escapedText = near.replace('"', '""');
|
|
||||||
var exactNearMatches = [];
|
|
||||||
var anyNearMatches = [];
|
|
||||||
findPossibleMatches('//*[contains(normalize-space(.), "' + escapedText +
|
|
||||||
'") and not(child::*[contains(normalize-space(.), "' + escapedText +
|
|
||||||
'")])]', function(match) {
|
|
||||||
// Get the text.
|
|
||||||
var matchText = match.innerText.trim();
|
|
||||||
|
|
||||||
// Add to array depending on if it's an exact or partial match.
|
|
||||||
if (matchText === text) {
|
|
||||||
exactNearMatches.push(match);
|
|
||||||
} else {
|
|
||||||
anyNearMatches.push(match);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var nearFound = null;
|
|
||||||
|
|
||||||
// If there is an exact text match, use that (regardless of other matches).
|
|
||||||
if (exactNearMatches.length > 1) {
|
|
||||||
throw new Error('Too many exact matches for near text');
|
|
||||||
} else if (exactNearMatches.length) {
|
|
||||||
nearFound = exactNearMatches[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nearFound === null) {
|
|
||||||
// If there is one partial text match, use that.
|
|
||||||
if (anyNearMatches.length > 1) {
|
|
||||||
throw new Error('Too many partial matches for near text');
|
|
||||||
} else if (anyNearMatches.length) {
|
|
||||||
nearFound = anyNearMatches[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nearFound) {
|
|
||||||
throw new Error('No matches for near text');
|
|
||||||
}
|
|
||||||
|
|
||||||
while (nearFound) {
|
|
||||||
nearAncestors.push(nearFound);
|
|
||||||
nearFound = nearFound.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the number of steps up the tree from a specified node before getting to an
|
|
||||||
* ancestor of the 'near' item
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} node HTML node
|
|
||||||
* @returns {number} Number of steps up, or Number.MAX_SAFE_INTEGER if it never matched
|
|
||||||
*/
|
|
||||||
var calculateNearDepth = function(node) {
|
|
||||||
var depth = 0;
|
|
||||||
while (node) {
|
|
||||||
var ancestorDepth = nearAncestors.indexOf(node);
|
|
||||||
if (ancestorDepth !== -1) {
|
|
||||||
return depth + ancestorDepth;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
depth++;
|
|
||||||
}
|
|
||||||
return Number.MAX_SAFE_INTEGER;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reduces an array to include only the nearest in each category.
|
|
||||||
*
|
|
||||||
* @param {Array} arr Array to
|
|
||||||
* @return {Array} Array including only the items with minimum 'near' depth
|
|
||||||
*/
|
|
||||||
var filterNonNearest = function(arr) {
|
|
||||||
var nearDepth = arr.map(function(node) {
|
|
||||||
return calculateNearDepth(node);
|
|
||||||
});
|
|
||||||
var minDepth = Math.min.apply(null, nearDepth);
|
|
||||||
return arr.filter(function(element, index) {
|
|
||||||
return nearDepth[index] == minDepth;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter all the category arrays.
|
|
||||||
exactMatches = filterNonNearest(exactMatches);
|
|
||||||
exactLabelMatches = filterNonNearest(exactLabelMatches);
|
|
||||||
anyMatches = filterNonNearest(anyMatches);
|
|
||||||
anyLabelMatches = filterNonNearest(anyLabelMatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the resulting match. Note this 'do' loop is not really a loop, it is just so we
|
|
||||||
// can easily break out of it as soon as we find a match.
|
|
||||||
var found = null;
|
|
||||||
do {
|
do {
|
||||||
// If there is an exact text match, use that (regardless of other matches).
|
const node = findElementBasedOnTextWithin(container, text);
|
||||||
if (exactMatches.length > 1) {
|
|
||||||
throw new Error('Too many exact matches for text');
|
|
||||||
} else if (exactMatches.length) {
|
|
||||||
found = exactMatches[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is an exact label match, use that.
|
if (node) {
|
||||||
if (exactLabelMatches.length > 1) {
|
return node;
|
||||||
throw new Error('Too many exact label matches for text');
|
|
||||||
} else if (exactLabelMatches.length) {
|
|
||||||
found = exactLabelMatches[0];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} while ((container = container.parentElement) && container !== topContainer);
|
||||||
// If there is one partial text match, use that.
|
|
||||||
if (anyMatches.length > 1) {
|
|
||||||
throw new Error('Too many partial matches for text');
|
|
||||||
} else if (anyMatches.length) {
|
|
||||||
found = anyMatches[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally if there is one partial label match, use that.
|
|
||||||
if (anyLabelMatches.length > 1) {
|
|
||||||
throw new Error('Too many partial label matches for text');
|
|
||||||
} else if (anyLabelMatches.length) {
|
|
||||||
found = anyLabelMatches[0];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (false);
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error('No matches for text');
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -476,6 +409,29 @@
|
||||||
return 'OK';
|
return 'OK';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to find an arbitrary item based on its text or aria label.
|
||||||
|
*
|
||||||
|
* @param {string} text Text (full or partial)
|
||||||
|
* @param {string} [near] Optional 'near' text
|
||||||
|
* @return {string} OK if successful, or ERROR: followed by message
|
||||||
|
*/
|
||||||
|
var behatFind = function(text, near) {
|
||||||
|
log(`Action - Find ${text}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const element = findElementBasedOnText(text, near);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return 'ERROR: No matches for text';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'OK';
|
||||||
|
} catch (error) {
|
||||||
|
return 'ERROR: ' + error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get main navigation controller.
|
* Get main navigation controller.
|
||||||
*
|
*
|
||||||
|
@ -497,7 +453,7 @@
|
||||||
* Function to press arbitrary item based on its text or Aria label.
|
* Function to press arbitrary item based on its 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
|
||||||
* @return {string} OK if successful, or ERROR: followed by message
|
* @return {string} OK if successful, or ERROR: followed by message
|
||||||
*/
|
*/
|
||||||
var behatPress = function(text, near) {
|
var behatPress = function(text, near) {
|
||||||
|
@ -506,10 +462,15 @@
|
||||||
var found;
|
var found;
|
||||||
try {
|
try {
|
||||||
found = findElementBasedOnText(text, near);
|
found = findElementBasedOnText(text, near);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return 'ERROR: No matches for text';
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 'ERROR: ' + error.message;
|
return 'ERROR: ' + error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.BehatMoodleAppLegacy) {
|
||||||
var mainContent = getNavCtrl().getActive().contentRef().nativeElement;
|
var mainContent = getNavCtrl().getActive().contentRef().nativeElement;
|
||||||
var rect = found.getBoundingClientRect();
|
var rect = found.getBoundingClientRect();
|
||||||
|
|
||||||
|
@ -528,6 +489,10 @@
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
found.dispatchEvent(new MouseEvent('click', eventOptions));
|
found.dispatchEvent(new MouseEvent('click', eventOptions));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
} else {
|
||||||
|
found.scrollIntoView();
|
||||||
|
setTimeout(() => found.click(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark busy until the button click finishes processing.
|
// Mark busy until the button click finishes processing.
|
||||||
addPendingDelay();
|
addPendingDelay();
|
||||||
|
@ -547,7 +512,10 @@
|
||||||
var resultCount = 0;
|
var resultCount = 0;
|
||||||
var titles = Array.from(document.querySelectorAll('ion-header ion-title'));
|
var titles = Array.from(document.querySelectorAll('ion-header ion-title'));
|
||||||
titles.forEach(function(title) {
|
titles.forEach(function(title) {
|
||||||
if (title.offsetParent) {
|
if (
|
||||||
|
(window.BehatMoodleAppLegacy && title.offsetParent) ||
|
||||||
|
(!window.BehatMoodleAppLegacy && isElementVisible(title, document.body))
|
||||||
|
) {
|
||||||
result = title.innerText.trim();
|
result = title.innerText.trim();
|
||||||
resultCount++;
|
resultCount++;
|
||||||
}
|
}
|
||||||
|
@ -670,6 +638,7 @@
|
||||||
window.behat = {
|
window.behat = {
|
||||||
pressStandard : behatPressStandard,
|
pressStandard : behatPressStandard,
|
||||||
closePopup : behatClosePopup,
|
closePopup : behatClosePopup,
|
||||||
|
find : behatFind,
|
||||||
press : behatPress,
|
press : behatPress,
|
||||||
setField : behatSetField,
|
setField : behatSetField,
|
||||||
getHeader : behatGetHeader,
|
getHeader : behatGetHeader,
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||||
|
|
||||||
require_once(__DIR__ . '/../../behat/behat_base.php');
|
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
|
||||||
|
|
||||||
use Behat\Mink\Exception\DriverException;
|
use Behat\Mink\Exception\DriverException;
|
||||||
use Behat\Mink\Exception\ExpectationException;
|
use Behat\Mink\Exception\ExpectationException;
|
||||||
|
@ -63,7 +63,7 @@ class behat_app extends behat_base {
|
||||||
$this->check_behat_setup();
|
$this->check_behat_setup();
|
||||||
$this->fix_moodle_setup();
|
$this->fix_moodle_setup();
|
||||||
$this->ionicurl = $this->start_or_reuse_ionic();
|
$this->ionicurl = $this->start_or_reuse_ionic();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the Moodle app in the browser.
|
* Opens the Moodle app in the browser.
|
||||||
|
@ -93,6 +93,33 @@ class behat_app extends behat_base {
|
||||||
$this->prepare_browser($this->ionicurl);
|
$this->prepare_browser($this->ionicurl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds elements in the app.
|
||||||
|
*
|
||||||
|
* @Then /^I should(?P<not_boolean> not)? find "(?P<text_string>(?:[^"]|\\")*)"(?: near "(?P<near_string>(?:[^"]|\\")*)")? in the app$/
|
||||||
|
* @param string $text
|
||||||
|
*/
|
||||||
|
public function i_find_in_the_app($not, $text='', $near='') {
|
||||||
|
$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.find(\"$text\", \"$near\");");
|
||||||
|
|
||||||
|
if ($not && $result === 'OK') {
|
||||||
|
throw new DriverException('Error, found an item that should not be found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$not && $result !== 'OK') {
|
||||||
|
throw new DriverException('Error finding item - ' . $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$this->wait_for_pending_js();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the Behat setup - tags and configuration.
|
* Checks the Behat setup - tags and configuration.
|
||||||
*
|
*
|
||||||
|
@ -282,14 +309,23 @@ class behat_app extends behat_base {
|
||||||
protected function prepare_browser(string $url) {
|
protected function prepare_browser(string $url) {
|
||||||
global $CFG;
|
global $CFG;
|
||||||
|
|
||||||
|
// Check whether the app is running a legacy version.
|
||||||
|
$json = @file_get_contents("$url/assets/env.json") ?: @file_get_contents("$url/config.json");
|
||||||
|
$data = json_decode($json);
|
||||||
|
$appversion = $data->build->version ?? str_replace('-dev', '', $data->versionname);
|
||||||
|
$islegacy = version_compare($appversion, '3.9.5', '<');
|
||||||
|
|
||||||
// Visit the Ionic URL and wait for it to load.
|
// Visit the Ionic URL and wait for it to load.
|
||||||
$this->getSession()->visit($url);
|
$this->getSession()->visit($url);
|
||||||
$this->spin(
|
$this->spin(
|
||||||
function($context, $args) {
|
function($context) use ($islegacy) {
|
||||||
$title = $context->getSession()->getPage()->find('xpath', '//title');
|
$title = $context->getSession()->getPage()->find('xpath', '//title');
|
||||||
if ($title) {
|
if ($title) {
|
||||||
$text = $title->getHtml();
|
$text = $title->getHtml();
|
||||||
if ($text === 'Moodle Desktop') {
|
if (
|
||||||
|
($islegacy && $text === 'Moodle Desktop') ||
|
||||||
|
(!$islegacy && $text === 'Moodle App')
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,20 +333,25 @@ class behat_app extends behat_base {
|
||||||
}, false, 60);
|
}, false, 60);
|
||||||
|
|
||||||
// Run the scripts to install Moodle 'pending' checks.
|
// Run the scripts to install Moodle 'pending' checks.
|
||||||
|
$islegacyboolean = $islegacy ? 'true' : 'false';
|
||||||
|
$this->execute_script("window.BehatMoodleAppLegacy = $islegacyboolean;");
|
||||||
$this->execute_script(file_get_contents(__DIR__ . '/app_behat_runtime.js'));
|
$this->execute_script(file_get_contents(__DIR__ . '/app_behat_runtime.js'));
|
||||||
|
|
||||||
// Wait until the site login field appears OR the main page.
|
// Wait until the site login field appears OR the main page.
|
||||||
$situation = $this->spin(
|
$situation = $this->spin(
|
||||||
function($context, $args) {
|
function($context) use ($islegacy) {
|
||||||
$page = $context->getSession()->getPage();
|
$page = $context->getSession()->getPage();
|
||||||
|
|
||||||
$element = $page->find('xpath', '//page-core-login-site//input[@name="url"]');
|
$element = $page->find('xpath', '//page-core-login-site//input[@name="url"]');
|
||||||
if ($element) {
|
if ($element) {
|
||||||
// Wait for the onboarding modal to open, if any.
|
// Wait for the onboarding modal to open, if any.
|
||||||
$this->wait_for_pending_js();
|
$this->wait_for_pending_js();
|
||||||
$element = $page->find('xpath', '//page-core-login-site-onboarding');
|
$element = $islegacy
|
||||||
|
? $page->find('xpath', '//page-core-login-site-onboarding')
|
||||||
|
: $page->find('xpath', '//core-login-site-onboarding');
|
||||||
if ($element) {
|
if ($element) {
|
||||||
$this->i_press_in_the_app('Skip');
|
$this->i_press_in_the_app('Skip');
|
||||||
|
$this->wait_for_pending_js();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'login';
|
return 'login';
|
||||||
|
@ -327,7 +368,7 @@ class behat_app extends behat_base {
|
||||||
// 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('campus.example.edu', $CFG->wwwroot);
|
||||||
$this->i_press_in_the_app('Connect!');
|
$this->i_press_in_the_app($islegacy ? 'Connect!' : 'Connect to your site');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue only after JS finishes.
|
// Continue only after JS finishes.
|
||||||
|
@ -491,18 +532,23 @@ class behat_app extends behat_base {
|
||||||
* @throws ExpectationException If the header text is different to the expected value
|
* @throws ExpectationException If the header text is different to the expected value
|
||||||
*/
|
*/
|
||||||
public function the_header_should_be_in_the_app(string $text) {
|
public function the_header_should_be_in_the_app(string $text) {
|
||||||
$result = $this->spin(function($context, $args) {
|
$this->spin(function() use ($text) {
|
||||||
$result = $this->evaluate_script('return window.behat.getHeader();');
|
$result = $this->evaluate_script('return window.behat.getHeader();');
|
||||||
|
|
||||||
if (substr($result, 0, 3) !== 'OK:') {
|
if (substr($result, 0, 3) !== 'OK:') {
|
||||||
throw new DriverException('Error getting header - ' . $result);
|
throw new DriverException('Error getting header - ' . $result);
|
||||||
}
|
}
|
||||||
return $result;
|
|
||||||
});
|
|
||||||
$header = substr($result, 3);
|
$header = substr($result, 3);
|
||||||
if (trim($header) !== trim($text)) {
|
if (trim($header) !== trim($text)) {
|
||||||
throw new ExpectationException('The header text was not as expected: \'' . $header . '\'',
|
throw new ExpectationException(
|
||||||
$this->getSession()->getDriver());
|
"The header text was not as expected: '$header'",
|
||||||
|
$this->getSession()->getDriver()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue