diff --git a/mod/assignment/tests/behat/app_basic_usage.feature b/mod/assignment/tests/behat/app_basic_usage.feature index 1bfb70b57..778cfe1ec 100755 --- a/mod/assignment/tests/behat/app_basic_usage.feature +++ b/mod/assignment/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/chat/tests/behat/app_basic_usage.feature b/mod/chat/tests/behat/app_basic_usage.feature index 424c80051..5838fa4ae 100755 --- a/mod/chat/tests/behat/app_basic_usage.feature +++ b/mod/chat/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/choice/tests/behat/app_basic_usage.feature b/mod/choice/tests/behat/app_basic_usage.feature index 86055ca25..22ea6aa69 100755 --- a/mod/choice/tests/behat/app_basic_usage.feature +++ b/mod/choice/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/comments/tests/behat/app_basic_usage.feature b/mod/comments/tests/behat/app_basic_usage.feature index e4066d82d..2bc874f98 100755 --- a/mod/comments/tests/behat/app_basic_usage.feature +++ b/mod/comments/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/course/tests/behat/app_basic_usage.feature b/mod/course/tests/behat/app_basic_usage.feature index 0e66729d6..a74a97831 100755 --- a/mod/course/tests/behat/app_basic_usage.feature +++ b/mod/course/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/course/tests/behat/app_course_completion.feature b/mod/course/tests/behat/app_course_completion.feature index a3c2475a5..34d482722 100644 --- a/mod/course/tests/behat/app_course_completion.feature +++ b/mod/course/tests/behat/app_course_completion.feature @@ -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 diff --git a/mod/course/tests/behat/app_courselist.feature b/mod/course/tests/behat/app_courselist.feature index 1e1bc1e1f..7a508f9f3 100644 --- a/mod/course/tests/behat/app_courselist.feature +++ b/mod/course/tests/behat/app_courselist.feature @@ -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 diff --git a/mod/courses/tests/behat/app_basic_usage.feature b/mod/courses/tests/behat/app_basic_usage.feature index fea9ce84a..e81882cde 100755 --- a/mod/courses/tests/behat/app_basic_usage.feature +++ b/mod/courses/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/data/tests/behat/app_data_entries.feature b/mod/data/tests/behat/app_data_entries.feature index 41e8ad6c5..5fb346918 100644 --- a/mod/data/tests/behat/app_data_entries.feature +++ b/mod/data/tests/behat/app_data_entries.feature @@ -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 diff --git a/mod/data/tests/behat/app_data_sync.feature b/mod/data/tests/behat/app_data_sync.feature index f0b147b95..065ffe35f 100644 --- a/mod/data/tests/behat/app_data_sync.feature +++ b/mod/data/tests/behat/app_data_sync.feature @@ -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 diff --git a/mod/forum/tests/behat/app_basic_usage.feature b/mod/forum/tests/behat/app_basic_usage.feature index 175c4bb52..473cbfc21 100755 --- a/mod/forum/tests/behat/app_basic_usage.feature +++ b/mod/forum/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/glossary/tests/behat/app_basic_usage.feature b/mod/glossary/tests/behat/app_basic_usage.feature index 2f8de6a52..db323a308 100755 --- a/mod/glossary/tests/behat/app_basic_usage.feature +++ b/mod/glossary/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/login/tests/behat/app_basic_usage.feature b/mod/login/tests/behat/app_basic_usage.feature index 0aa02605f..fc0f78236 100755 --- a/mod/login/tests/behat/app_basic_usage.feature +++ b/mod/login/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/messages/tests/behat/app_basic_usage.feature b/mod/messages/tests/behat/app_basic_usage.feature index f33f06c13..a2aa8fa3f 100755 --- a/mod/messages/tests/behat/app_basic_usage.feature +++ b/mod/messages/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/quiz/tests/behat/app_basic_usage.feature b/mod/quiz/tests/behat/app_basic_usage.feature index ca652fdcb..7b2f92086 100755 --- a/mod/quiz/tests/behat/app_basic_usage.feature +++ b/mod/quiz/tests/behat/app_basic_usage.feature @@ -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 diff --git a/mod/quiz/tests/behat/app_quiz_navigation.feature b/mod/quiz/tests/behat/app_quiz_navigation.feature index c28bc1c86..75b8b7a9d 100644 --- a/mod/quiz/tests/behat/app_quiz_navigation.feature +++ b/mod/quiz/tests/behat/app_quiz_navigation.feature @@ -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 diff --git a/mod/survey/tests/behat/app_basic_usage.feature b/mod/survey/tests/behat/app_basic_usage.feature index a8a324415..fcba1840e 100755 --- a/mod/survey/tests/behat/app_basic_usage.feature +++ b/mod/survey/tests/behat/app_basic_usage.feature @@ -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 diff --git a/tests/behat/app_behat_runtime.js b/tests/behat/app_behat_runtime.js index 8bd63730d..c725827f5 100644 --- a/tests/behat/app_behat_runtime.js +++ b/tests/behat/app_behat_runtime.js @@ -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, diff --git a/tests/behat/behat_app.php b/tests/behat/behat_app.php index e474ffc67..c1c14c80b 100644 --- a/tests/behat/behat_app.php +++ b/tests/behat/behat_app.php @@ -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)? find "(?P(?:[^"]|\\")*)"(?: near "(?P(?:[^"]|\\")*)")? 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()); - } } /**