From ae0af40b10f72b4679748d797e6c9915c7064451 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 26 May 2021 17:09:09 +0200 Subject: [PATCH 1/3] MOBILE-3320 behat: Extract app launch step --- tests/behat/behat_app.php | 157 +++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 61 deletions(-) diff --git a/tests/behat/behat_app.php b/tests/behat/behat_app.php index c2b753d0a..b5abee45e 100644 --- a/tests/behat/behat_app.php +++ b/tests/behat/behat_app.php @@ -46,6 +46,9 @@ class behat_app extends behat_base { /** @var string URL for running Ionic server */ protected $ionicurl = ''; + /** @var bool Checks whether the app is runing a legacy version (ionic 3) */ + protected $islegacy; + /** * Checks if the current OS is Windows, from the point of view of task-executing-and-killing. * @@ -67,9 +70,7 @@ class behat_app extends behat_base { } /** - * Opens the Moodle app in the browser. - * - * Requires JavaScript. + * Opens the Moodle app in the browser and introduces the enters the site. * * @Given /^I enter the app$/ * @throws DriverException Issue with configuration or feature file @@ -77,28 +78,35 @@ class behat_app extends behat_base { * @throws ExpectationException Problem with resizing window */ public function i_enter_the_app() { + $this->i_launch_the_app(); + $this->enter_site(); + } + + /** + * Opens the Moodle app in the browser. + * + * @Given /^I launch the app$/ + * @throws DriverException Issue with configuration or feature file + * @throws dml_exception Problem with Moodle setup + * @throws ExpectationException Problem with resizing window + */ + public function i_launch_the_app() { // Check the app tag was set. if (!$this->has_tag('app')) { throw new DriverException('Requires @app tag on scenario or feature.'); } - // Restart the browser and set its size. - $this->getSession()->restart(); - $this->resize_window('360x720', true); - - if (empty($this->ionicurl)) { - $this->ionicurl = $this->start_or_reuse_ionic(); - } - // Go to page and prepare browser for app. - $this->prepare_browser($this->ionicurl); + $this->prepare_browser(); } /** * Finds elements in the app. * * @Then /^I should(?P not)? find "(?P(?:[^"]|\\")*)"(?: near "(?P(?:[^"]|\\")*)")? in the app$/ + * @param string $not * @param string $text + * @param string $near */ public function i_find_in_the_app($not, $text='', $near='') { $not = !empty($not); @@ -341,75 +349,92 @@ class behat_app extends behat_base { * @param string $url App URL * @throws DriverException If the app fails to load properly */ - protected function prepare_browser(string $url) { - global $CFG; + protected function prepare_browser() { + // Restart the browser and set its size. + $this->getSession()->restart(); + $this->resize_window('360x720', true); + + if (empty($this->ionicurl)) { + $this->ionicurl = $this->start_or_reuse_ionic(); + } // Check whether the app is running a legacy version. - $json = @file_get_contents("$url/assets/env.json") ?: @file_get_contents("$url/config.json"); + $json = @file_get_contents("{$this->ionicurl}/assets/env.json") ?: @file_get_contents("{$this->ionicurl}/config.json"); $data = json_decode($json); $appversion = $data->build->version ?? str_replace('-dev', '', $data->versionname); - $islegacy = version_compare($appversion, '3.9.5', '<'); + + $this->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) use ($islegacy) { - $title = $context->getSession()->getPage()->find('xpath', '//title'); - if ($title) { - $text = $title->getHtml(); - if ( - ($islegacy && $text === 'Moodle Desktop') || - (!$islegacy && $text === 'Moodle App') - ) { - return true; - } - } - throw new DriverException('Moodle app not found in browser'); - }, false, 60); + $this->getSession()->visit($this->ionicurl); + $this->spin(function($context) { + $title = $context->getSession()->getPage()->find('xpath', '//title'); + + if ($title) { + $text = $title->getHtml(); + + if ( + ($this->islegacy && $text === 'Moodle Desktop') || + (!$this->islegacy && $text === 'Moodle App') + ) { + return true; + } + } + + throw new DriverException('Moodle app not found in browser'); + }, false, 60); // Run the scripts to install Moodle 'pending' checks. - $islegacyboolean = $islegacy ? 'true' : 'false'; + $islegacyboolean = $this->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) use ($islegacy) { - $page = $context->getSession()->getPage(); + // Assert initial page. + $this->spin(function($context) { + $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) { - // Wait for the onboarding modal to open, if any. - $this->wait_for_pending_js(); - $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(); - } + if ($element) { + // Wait for the onboarding modal to open, if any. + $this->wait_for_pending_js(); - return 'login'; - } + $element = $this->islegacy + ? $page->find('xpath', '//page-core-login-site-onboarding') + : $page->find('xpath', '//core-login-site-onboarding'); - $element = $page->find('xpath', '//page-core-mainmenu'); - if ($element) { - return 'mainpage'; - } - throw new DriverException('Moodle app login URL prompt not found'); - }, behat_base::get_extended_timeout(), 60); + if ($element) { + $this->i_press_in_the_app('Skip'); + } - // If it's the login page, we automatically fill in the URL and leave it on the user/pass - // page. If it's the main page, we just leave it there. - if ($situation === 'login') { - $this->i_set_the_field_in_the_app($islegacy ? 'campus.example.edu' : 'Your site', $CFG->wwwroot); - $this->i_press_in_the_app($islegacy ? 'Connect!' : 'Connect to your site'); - } + // Login screen found. + return true; + } + + if ($page->find('xpath', '//page-core-mainmenu')) { + // Main menu found. + return true; + } + + throw new DriverException('Moodle app not launched properly'); + }, false, 60); // Continue only after JS finishes. $this->wait_for_pending_js(); } + protected function enter_site() { + if (!$this->is_in_login_page()) { + // Already in the site. + return; + } + + global $CFG; + + $this->i_set_the_field_in_the_app($this->islegacy ? 'campus.example.edu' : 'Your site', $CFG->wwwroot); + $this->i_press_in_the_app($this->islegacy ? 'Connect!' : 'Connect to your site'); + $this->wait_for_pending_js(); + } + /** * Carries out the login steps for the app, assuming the user is on the app login page. Called * from behat_auth.php. @@ -536,6 +561,16 @@ class behat_app extends behat_base { $this->press($text, $near); } + /** + * Check whether the current page is the login form. + */ + protected function is_in_login_page(): bool { + $page = $this->getSession()->getPage(); + $logininput = $page->find('xpath', '//page-core-login-site//input[@name="url"]'); + + return !is_null($logininput); + } + /** * Clicks on / touches something that is visible in the app, near some other text. * From 22497fe7cd8c94fa9fd7e9af51b22e92130450af Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 26 May 2021 17:09:47 +0200 Subject: [PATCH 2/3] MOBILE-3320 behat: Implement browser tab assertion --- tests/behat/behat_app.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/behat/behat_app.php b/tests/behat/behat_app.php index b5abee45e..3d52a63c5 100644 --- a/tests/behat/behat_app.php +++ b/tests/behat/behat_app.php @@ -653,6 +653,31 @@ class behat_app extends behat_base { }); } + /** + * Check that the app opened a new browser tab. + * + * @Given /^the app should(?P not)? have opened a browser tab$/ + * @param string $not + */ + public function the_app_should_have_opened_a_browser_tab($not = '') { + $not = !empty($not); + + $this->spin(function() use ($not) { + $openedbrowsertab = count($this->getSession()->getWindowNames()) === 2; + + if ($not === $openedbrowsertab) { + throw new ExpectationException( + $not + ? 'Did not expect the app to have opened a browser tab' + : 'Expected the app to have opened a browser tab', + $this->getSession()->getDriver() + ); + } + + return true; + }); + } + /** * Switches to a newly-opened browser tab. * From 4673ddd03ed9b383544e48bbe7afa803899ccd92 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 27 May 2021 18:11:52 +0200 Subject: [PATCH 3/3] MOBILE-3320 behat: Update choice tests --- .../tests/behat/app_basic_usage.feature | 137 +++++++++--------- tests/behat/app_behat_runtime.js | 38 ++++- 2 files changed, 104 insertions(+), 71 deletions(-) diff --git a/mod/choice/tests/behat/app_basic_usage.feature b/mod/choice/tests/behat/app_basic_usage.feature index 22ea6aa69..a25265feb 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 @app_upto3.9.4 @javascript +@mod @mod_choice @app @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 @@ -17,7 +17,6 @@ Feature: Test basic usage of choice activity in app | teacher1 | C1 | editingteacher | | student1 | C1 | student | - @app @3.8.0 Scenario: Answer a choice (multi or single, update answer) & View results Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | @@ -26,24 +25,23 @@ Feature: Test basic usage of choice activity in app And I log in as "student1" And I press "Course 1" near "Course overview" in the app And I press "Test single choice name" in the app - And I press "Option 1" in the app - And I press "Option 2" in the app + And I select "Option 1" in the app + And I select "Option 2" in the app And I press "Save my choice" in the app - Then I should see "Are you sure" + Then I should find "Are you sure" in the app When I press "OK" in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 0" - But I should not see "Remove my choice" + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app + But I should not find "Remove my choice" in the app When I press the back button in the app And I press "Test single choice name" in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 0" + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app - @app @3.8.0 Scenario: Answer a choice (multi or single, update answer) & View results & Delete choice Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | @@ -52,29 +50,28 @@ Feature: Test basic usage of choice activity in app And I log in as "student1" And I press "Course 1" near "Course overview" in the app And I press "Test multi choice name" in the app - And I press "Option 1" in the app - And I press "Option 2" in the app + And I select "Option 1" in the app + And I select "Option 2" in the app And I press "Save my choice" in the app - Then I should see "Option 1: 1" - And I should see "Option 2: 1" - And I should see "Option 3: 0" - And I should see "Remove my choice" + Then I should find "Option 1: 1" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app + And I should find "Remove my choice" in the app - When I press "Option 1" in the app - And I press "Option 3" in the app + When I select "Option 1" in the app + And I select "Option 3" in the app And I press "Save my choice" in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 1" + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 1" in the app When I press "Remove my choice" in the app - Then I should see "Are you sure" + Then I should find "Are you sure" in the app When I press "Delete" in the app - Then I should see "The results are not currently viewable" - But I should not see "Remove my choice" + Then I should find "The results are not currently viewable" in the app + But I should not find "Remove my choice" in the app - @app @3.8.0 Scenario: Answer and change answer offline & Sync choice Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | @@ -83,31 +80,32 @@ Feature: Test basic usage of choice activity in app And I log in as "student1" And I press "Course 1" near "Course overview" in the app And I press "Test single choice name" in the app - And I press "Option 1" in the app + And I select "Option 1" in the app And I switch offline mode to "true" - And I press "Option 2" in the app + And I select "Option 2" in the app And I press "Save my choice" in the app - Then I should see "Are you sure" + Then I should find "Are you sure" in the app When I press "OK" in the app And I press the back button in the app And I press "Test single choice name" in the app - Then I should see "This Choice has offline data to be synchronised." - But I should not see "Option 1: 0" - And I should not see "Option 2: 1" - And I should not see "Option 3: 0" + Then I should find "This Choice has offline data to be synchronised." in the app + But I should not find "Option 1: 0" in the app + And I should not find "Option 2: 1" in the app + And I should not find "Option 3: 0" in the app When I switch offline mode to "false" And I press the back button in the app And I press "Test single choice name" in the app - And I press "Display options" in the app - And I press "Refresh" in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 0" - But I should not see "This Choice has offline data to be synchronised." + Then I should find "Test single choice description" in the app + + When I press "Display options" in the app + And I press "Refresh" in the app + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app + But I should not find "This Choice has offline data to be synchronised." in the app - @app @3.8.0 Scenario: Answer and change answer offline & Auto-sync choice Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | @@ -120,23 +118,22 @@ Feature: Test basic usage of choice activity in app And I switch offline mode to "true" And I press "Option 2" in the app And I press "Save my choice" in the app - Then I should see "Are you sure" + Then I should find "Are you sure" in the app When I press "OK" in the app - And I switch offline mode to "false" - Then I should see "This Choice has offline data to be synchronised." - But I should not see "Option 1: 0" - And I should not see "Option 2: 1" - And I should not see "Option 3: 0" + Then I should find "This Choice has offline data to be synchronised." in the app + But I should not find "Option 1: 0" in the app + And I should not find "Option 2: 1" in the app + And I should not find "Option 3: 0" in the app - When I run cron tasks in the app + When I switch offline mode to "false" + And I run cron tasks in the app And I wait loading to finish in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 0" - But I should not see "This Choice has offline data to be synchronised." + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app + But I should not find "This Choice has offline data to be synchronised." in the app - @app @3.8.0 Scenario: Prefetch Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | @@ -147,35 +144,36 @@ Feature: Test basic usage of choice activity in app And I press "Course 1" near "Course overview" in the app And I press "Display options" in the app And I press "Show download options" in the app - And I press "cloud download" near "Test single choice name" in the app - And I switch offline mode to "true" + And I press "Download" near "Test single choice name" in the app + Then I should find "Downloaded" near "Test single choice name" in the app + + When I switch offline mode to "true" And I press "Test multi choice name" in the app - Then I should see "There was a problem connecting to the site. Please check your connection and try again." + Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app When I press "OK" in the app And I press the back button in the app And I press "Test single choice name" in the app And I press "Option 2" in the app And I press "Save my choice" in the app - Then I should see "Are you sure" + Then I should find "Are you sure" in the app When I press "OK" in the app And I press the back button in the app And I press "Test single choice name" in the app - Then I should see "This Choice has offline data to be synchronised." - But I should not see "Option 1: 0" - And I should not see "Option 2: 1" - And I should not see "Option 3: 0" + Then I should find "This Choice has offline data to be synchronised." in the app + But I should not find "Option 1: 0" in the app + And I should not find "Option 2: 1" in the app + And I should not find "Option 3: 0" in the app When I switch offline mode to "false" And I press the back button in the app And I press "Test single choice name" in the app - Then I should see "Option 1: 0" - And I should see "Option 2: 1" - And I should see "Option 3: 0" - But I should not see "This Choice has offline data to be synchronised." + Then I should find "Option 1: 0" in the app + And I should find "Option 2: 1" in the app + And I should find "Option 3: 0" in the app + But I should not find "This Choice has offline data to be synchronised." in the app - @app @3.8.0 Scenario: Download students choice in text format # Submit answer as student Given the following "activities" exist: @@ -194,10 +192,13 @@ Feature: Test basic usage of choice activity in app And I log in as "teacher1" And I press "Course 1" near "Course overview" in the app And I press "Choice name" in the app - And I press "Display options" in the app + Then I should find "Test choice description" in the app + + When I press "Display options" in the app And I press "Open in browser" in the app And I switch to the browser tab opened by the app And I log in as "teacher1" And I press "Actions menu" And I follow "View 1 responses" And I press "Download in text format" + # TODO Then I should find "..." in the downloads folder diff --git a/tests/behat/app_behat_runtime.js b/tests/behat/app_behat_runtime.js index 7da3164a0..db9f68695 100644 --- a/tests/behat/app_behat_runtime.js +++ b/tests/behat/app_behat_runtime.js @@ -326,6 +326,32 @@ return elements; }; + /** + * Given a list of elements, get the top ancestors among all of them. + * + * This will remote duplicates and drop any elements nested within each other. + * + * @param {Array} elements Elements list. + * @return {Array} Top ancestors. + */ + var getTopAncestors = function(elements) { + const uniqueElements = new Set(elements); + + for (const element of uniqueElements) { + for (otherElement of uniqueElements) { + if (otherElement === element) { + continue; + } + + if (element.contains(otherElement)) { + uniqueElements.delete(otherElement); + } + } + } + + return [...uniqueElements]; + }; + /** * Function to find elements based on their text or Aria label. * @@ -343,10 +369,16 @@ if (nearElements.length === 0) { throw new Error('There was no match for near text') } else if (nearElements.length > 1) { - throw new Error('Too many matches for near text'); - } + const nearElementsAncestors = getTopAncestors(nearElements); - container = nearElements[0].parentElement; + if (nearElementsAncestors.length > 1) { + throw new Error('Too many matches for near text'); + } + + container = nearElementsAncestors[0].parentElement; + } else { + container = nearElements[0].parentElement; + } } do {