MOBILE-3738 behat: Prepare 3.9.5 tests
This commit is contained in:
		
							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 | ||||
|   In order to participate in the assignment while using the mobile app | ||||
|   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 | ||||
|   As a student | ||||
|   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 | ||||
|   In order to participate in the choice while using the mobile app | ||||
|   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 | ||||
|   In order to participate in the comments while using the mobile app | ||||
|   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 | ||||
|   In order to participate in one course while using the mobile app | ||||
|   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. | ||||
|   In order to track the progress of the course on mobile device | ||||
|   As a student | ||||
|  | ||||
| @ -22,17 +22,17 @@ Feature: Test course list shown on app start tab | ||||
|   Scenario: View courses (shortnames not displayed) | ||||
|     When I enter the app | ||||
|     And I log in as "student1" | ||||
|     Then I should see "Course 1" | ||||
|     But I should not see "Course 2" | ||||
|     But I should not see "C1" | ||||
|     But I should not see "C2" | ||||
|     Then I should find "Course 1" in the app | ||||
|     But I should not find "Course 2" in the app | ||||
|     But I should not find "C1" in the app | ||||
|     But I should not find "C2" in the app | ||||
| 
 | ||||
|     When I enter the app | ||||
|     And I log in as "student2" | ||||
|     Then I should see "Course 1" | ||||
|     And I should see "Course 2" | ||||
|     But I should not see "C1" | ||||
|     But I should not see "C2" | ||||
|     Then I should find "Course 1" in the app | ||||
|     And I should find "Course 2" in the app | ||||
|     But I should not find "C1" in the app | ||||
|     But I should not find "C2" in the app | ||||
| 
 | ||||
|   Scenario: Filter courses | ||||
|     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 | | ||||
|     When I enter the app | ||||
|     And I log in as "student2" | ||||
|     Then I press "Display options" near "Course overview" in the app | ||||
|     Then I should see "C1" | ||||
|     And I should see "C2" | ||||
|     And I should see "C3" | ||||
|     And I should see "C4" | ||||
|     And I should see "C5" | ||||
|     And I should see "C6" | ||||
|     Then I press "Filter my courses" in the app | ||||
|     And I set the field "Filter my courses" to "fr" in the app | ||||
|     Then I should not see "C1" | ||||
|     And I should not see "C2" | ||||
|     And I should see "C3" | ||||
|     And I should see "C4" | ||||
|     And I should not see "C5" | ||||
|     And I should not see "C6" | ||||
|     And I press "Display options" near "Course overview" 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 | ||||
| 
 | ||||
|     When I press "Display options" near "Course overview" in the app | ||||
|     And I press "Filter my courses" in the app | ||||
|     Then I should see "C1" | ||||
|     And I should see "C2" | ||||
|     And I should see "C3" | ||||
|     And I should see "C4" | ||||
|     And I should see "C5" | ||||
|     And I should see "C6" | ||||
|     And I set the field "Filter my courses" 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 | ||||
|     And I should find "Frog 4" in the app | ||||
|     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 | ||||
|   In order to participate in the courses while using the mobile app | ||||
|   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 | ||||
|   In order to populate databases | ||||
|   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 | ||||
|   In order to populate databases while offline | ||||
|   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 | ||||
|   In order to participate in the forum while using the mobile app | ||||
|   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 | ||||
|   In order to participate in the glossaries while using the mobile app | ||||
|   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 | ||||
|   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 | ||||
|   In order to participate with messages while using the mobile app | ||||
|   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 | ||||
|   As a student | ||||
|   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 | ||||
|   As a student | ||||
|   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 | ||||
|   In order to participate in surveys while using the mobile app | ||||
|   As a student | ||||
|  | ||||
| @ -174,6 +174,26 @@ | ||||
|     var observer = new MutationObserver(mutationCallback); | ||||
|     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, | ||||
|      * 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. | ||||
|      * | ||||
|      * @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 | ||||
|      * @throws {string} Error message beginning 'ERROR:' if something went wrong | ||||
|      */ | ||||
|     var findElementBasedOnText = function(text, near) { | ||||
|         // Find all the elements that contain this text (and don't have a child element that
 | ||||
|         // contains it - i.e. the most specific elements).
 | ||||
|         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(); | ||||
|         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's just check - is this actually a label for something else? If so we will click
 | ||||
|                     // that other thing instead.
 | ||||
|                     var labelId = document.evaluate('string(ancestor-or-self::ion-label[@id][1]/@id)', match).stringValue; | ||||
|                     if (labelId) { | ||||
|                         var target = document.querySelector('*[aria-labelledby=' + labelId + ']'); | ||||
|                         if (target) { | ||||
|                             match = target; | ||||
|                         } | ||||
|                     } | ||||
|         if (topContainer && near) { | ||||
|             const nearElement = findElementBasedOnText(near); | ||||
| 
 | ||||
|                     // Add to array depending on if it's an exact or partial match.
 | ||||
|                     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 (!nearElement) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             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); | ||||
|             container = nearElement.parentElement; | ||||
|         } | ||||
| 
 | ||||
|         // 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 { | ||||
|             // If there is an exact text match, use that (regardless of other matches).
 | ||||
|             if (exactMatches.length > 1) { | ||||
|                 throw new Error('Too many exact matches for text'); | ||||
|             } else if (exactMatches.length) { | ||||
|                 found = exactMatches[0]; | ||||
|                 break; | ||||
|             const node = findElementBasedOnTextWithin(container, text); | ||||
| 
 | ||||
|             if (node) { | ||||
|                 return node; | ||||
|             } | ||||
| 
 | ||||
|             // If there is an exact label match, use that.
 | ||||
|             if (exactLabelMatches.length > 1) { | ||||
|                 throw new Error('Too many exact label matches for text'); | ||||
|             } else if (exactLabelMatches.length) { | ||||
|                 found = exactLabelMatches[0]; | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             // 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; | ||||
|         } while ((container = container.parentElement) && container !== topContainer); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
| @ -476,6 +409,29 @@ | ||||
|         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. | ||||
|      * | ||||
| @ -497,7 +453,7 @@ | ||||
|      * Function to press arbitrary item based on its 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 | ||||
|      * @param {string} near Optional 'near' text | ||||
|      * @return {string} OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     var behatPress = function(text, near) { | ||||
| @ -506,28 +462,37 @@ | ||||
|         var found; | ||||
|         try { | ||||
|             found = findElementBasedOnText(text, near); | ||||
| 
 | ||||
|             if (!found) { | ||||
|                 return 'ERROR: No matches for text'; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             return 'ERROR: ' + error.message; | ||||
|         } | ||||
| 
 | ||||
|         var mainContent = getNavCtrl().getActive().contentRef().nativeElement; | ||||
|         var rect = found.getBoundingClientRect(); | ||||
|         if (window.BehatMoodleAppLegacy) { | ||||
|             var mainContent = getNavCtrl().getActive().contentRef().nativeElement; | ||||
|             var rect = found.getBoundingClientRect(); | ||||
| 
 | ||||
|         // Scroll the item into view.
 | ||||
|         mainContent.scrollTo(rect.x, rect.y); | ||||
|             // Scroll the item into view.
 | ||||
|             mainContent.scrollTo(rect.x, rect.y); | ||||
| 
 | ||||
|         // Simulate a mouse click on the button.
 | ||||
|         var eventOptions = {clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, | ||||
|                 bubbles: true, view: window, cancelable: true}; | ||||
|         setTimeout(function() { | ||||
|             found.dispatchEvent(new MouseEvent('mousedown', eventOptions)); | ||||
|         }, 0); | ||||
|         setTimeout(function() { | ||||
|             found.dispatchEvent(new MouseEvent('mouseup', eventOptions)); | ||||
|         }, 0); | ||||
|         setTimeout(function() { | ||||
|             found.dispatchEvent(new MouseEvent('click', eventOptions)); | ||||
|         }, 0); | ||||
|             // Simulate a mouse click on the button.
 | ||||
|             var eventOptions = {clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, | ||||
|                     bubbles: true, view: window, cancelable: true}; | ||||
|             setTimeout(function() { | ||||
|                 found.dispatchEvent(new MouseEvent('mousedown', eventOptions)); | ||||
|             }, 0); | ||||
|             setTimeout(function() { | ||||
|                 found.dispatchEvent(new MouseEvent('mouseup', eventOptions)); | ||||
|             }, 0); | ||||
|             setTimeout(function() { | ||||
|                 found.dispatchEvent(new MouseEvent('click', eventOptions)); | ||||
|             }, 0); | ||||
|         } else { | ||||
|             found.scrollIntoView(); | ||||
|             setTimeout(() => found.click(), 300); | ||||
|         } | ||||
| 
 | ||||
|         // Mark busy until the button click finishes processing.
 | ||||
|         addPendingDelay(); | ||||
| @ -547,7 +512,10 @@ | ||||
|         var resultCount = 0; | ||||
|         var titles = Array.from(document.querySelectorAll('ion-header ion-title')); | ||||
|         titles.forEach(function(title) { | ||||
|             if (title.offsetParent) { | ||||
|             if ( | ||||
|                 (window.BehatMoodleAppLegacy && title.offsetParent) || | ||||
|                 (!window.BehatMoodleAppLegacy && isElementVisible(title, document.body)) | ||||
|             ) { | ||||
|                 result = title.innerText.trim(); | ||||
|                 resultCount++; | ||||
|             } | ||||
| @ -670,6 +638,7 @@ | ||||
|     window.behat = { | ||||
|         pressStandard : behatPressStandard, | ||||
|         closePopup : behatClosePopup, | ||||
|         find : behatFind, | ||||
|         press : behatPress, | ||||
|         setField : behatSetField, | ||||
|         getHeader : behatGetHeader, | ||||
|  | ||||
| @ -25,7 +25,7 @@ | ||||
| 
 | ||||
| // 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\ExpectationException; | ||||
| @ -63,7 +63,7 @@ class behat_app extends behat_base { | ||||
|         $this->check_behat_setup(); | ||||
|         $this->fix_moodle_setup(); | ||||
|         $this->ionicurl = $this->start_or_reuse_ionic(); | ||||
| } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the Moodle app in the browser. | ||||
| @ -93,6 +93,33 @@ class behat_app extends behat_base { | ||||
|         $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. | ||||
|      * | ||||
| @ -282,14 +309,23 @@ class behat_app extends behat_base { | ||||
|     protected function prepare_browser(string $url) { | ||||
|         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.
 | ||||
|         $this->getSession()->visit($url); | ||||
|         $this->spin( | ||||
|                 function($context, $args) { | ||||
|                 function($context) use ($islegacy) { | ||||
|                     $title = $context->getSession()->getPage()->find('xpath', '//title'); | ||||
|                     if ($title) { | ||||
|                         $text = $title->getHtml(); | ||||
|                         if ($text === 'Moodle Desktop') { | ||||
|                         if ( | ||||
|                             ($islegacy && $text === 'Moodle Desktop') || | ||||
|                             (!$islegacy && $text === 'Moodle App') | ||||
|                         ) { | ||||
|                             return true; | ||||
|                         } | ||||
|                     } | ||||
| @ -297,20 +333,25 @@ class behat_app extends behat_base { | ||||
|                 }, false, 60); | ||||
| 
 | ||||
|         // 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')); | ||||
| 
 | ||||
|         // Wait until the site login field appears OR the main page.
 | ||||
|         $situation = $this->spin( | ||||
|                 function($context, $args) { | ||||
|                 function($context) use ($islegacy) { | ||||
|                     $page = $context->getSession()->getPage(); | ||||
| 
 | ||||
|                     $element = $page->find('xpath', '//page-core-login-site//input[@name="url"]'); | ||||
|                     if ($element) { | ||||
|                         // Wait for the onboarding modal to open, if any.
 | ||||
|                         $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) { | ||||
|                             $this->i_press_in_the_app('Skip'); | ||||
|                             $this->wait_for_pending_js(); | ||||
|                         } | ||||
| 
 | ||||
|                         return 'login'; | ||||
| @ -327,7 +368,7 @@ class behat_app extends behat_base { | ||||
|         // 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_press_in_the_app('Connect!'); | ||||
|             $this->i_press_in_the_app($islegacy ? 'Connect!' : 'Connect to your site'); | ||||
|         } | ||||
| 
 | ||||
|         // 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 | ||||
|      */ | ||||
|     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();'); | ||||
| 
 | ||||
|             if (substr($result, 0, 3) !== 'OK:') { | ||||
|                 throw new DriverException('Error getting header - ' . $result); | ||||
|             } | ||||
|             return $result; | ||||
| 
 | ||||
|             $header = substr($result, 3); | ||||
|             if (trim($header) !== trim($text)) { | ||||
|                 throw new ExpectationException( | ||||
|                     "The header text was not as expected: '$header'", | ||||
|                     $this->getSession()->getDriver() | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         }); | ||||
|         $header = substr($result, 3); | ||||
|         if (trim($header) !== trim($text)) { | ||||
|             throw new ExpectationException('The header text was not as expected: \'' . $header . '\'', | ||||
|                     $this->getSession()->getDriver()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user