diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 012aff300..a8e5bbc33 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -604,6 +604,7 @@ class behat_app extends behat_app_helper { $data = $data->getColumnsHash()[0]; $title = array_keys($data)[0]; $data = (object) $data; + $username = $data->user ?? ''; switch ($title) { case 'discussion': @@ -645,7 +646,7 @@ class behat_app extends behat_app_helper { throw new DriverException('Invalid custom link title - ' . $title); } - $this->open_moodleapp_custom_url($pageurl); + $this->open_moodleapp_custom_url($pageurl, '', $username); } /** diff --git a/local_moodleappbehat/tests/behat/behat_app_helper.php b/local_moodleappbehat/tests/behat/behat_app_helper.php index 4621b6a03..d255237d2 100644 --- a/local_moodleappbehat/tests/behat/behat_app_helper.php +++ b/local_moodleappbehat/tests/behat/behat_app_helper.php @@ -366,12 +366,15 @@ class behat_app_helper extends behat_base { * * @param string $script * @param bool $blocking + * @param string $texttofind If set, when this text is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. * @return mixed Result. */ - protected function zone_js(string $script, bool $blocking = false) { + protected function zone_js(string $script, bool $blocking = false, string $texttofind = '') { $blockingjson = json_encode($blocking); + $locatortofind = !empty($texttofind) ? json_encode((object) ['text' => $texttofind]) : null; - return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)"); + return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson, $locatortofind)"); } /** @@ -411,16 +414,14 @@ class behat_app_helper extends behat_base { $privatetoken = $usertoken->privatetoken; } - // Generate custom URL. - $parsed_url = parse_url($CFG->behat_wwwroot); - $site = $parsed_url['host'] ?? ''; - $site .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; - $site .= $parsed_url['path'] ?? ''; - $url = $this->get_mobile_url_scheme() . "://$username@$site?token=$token&privatetoken=$privatetoken"; + $url = $this->generate_custom_url([ + 'username' => $username, + 'token' => $token, + 'privatetoken' => $privatetoken, + 'redirect' => $path, + ]); - if (!empty($path)) { - $url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path); - } else { + if (empty($path)) { $successXPath = '//page-core-mainmenu'; } @@ -434,14 +435,54 @@ class behat_app_helper extends behat_base { * * @param string $path To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. + * @param string $username The username to use. */ - protected function open_moodleapp_custom_url(string $path, string $successXPath = '') { + protected function open_moodleapp_custom_url(string $path, string $successXPath = '', string $username = '') { global $CFG; - $urlscheme = $this->get_mobile_url_scheme(); - $url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); + $url = $this->generate_custom_url([ + 'username' => $username, + 'redirect' => $path, + ]); - $this->handle_url($url); + $this->handle_url($url, $successXPath, $username ? 'This link belongs to another site' : ''); + } + + /** + * Generates a custom URL to be treated by the app. + * + * @param array $data Data to generate the URL. + */ + protected function generate_custom_url(array $data): string { + global $CFG; + + $parsed_url = parse_url($CFG->behat_wwwroot); + $parameters = []; + + $url = $this->get_mobile_url_scheme() . '://' . ($parsed_url['scheme'] ?? 'http') . '://'; + if (!empty($data['username'])) { + $url .= $data['username'] . '@'; + } + $url .= $parsed_url['host'] ?? ''; + $url .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $url .= $parsed_url['path'] ?? ''; + + if (!empty($data['token'])) { + $parameters[] = 'token=' . $data['token']; + if (!empty($data['privatetoken'])) { + $parameters[] = 'privatetoken=' . $data['privatetoken']; + } + } + + if (!empty($data['redirect'])) { + $parameters[] = 'redirect=' . urlencode($data['redirect']); + } + + if (!empty($parameters)) { + $url .= '?' . implode('&', $parameters); + } + + return $url; } /** @@ -449,9 +490,11 @@ class behat_app_helper extends behat_base { * * @param string $customurl To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. + * @param string $texttofind If set, when this text is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. */ - protected function handle_url(string $customurl, string $successXPath = '') { - $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); + protected function handle_url(string $customurl, string $successXPath = '', string $texttofind = '') { + $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')", false, $texttofind); if ($result !== 'OK') { throw new DriverException('Error handling url - ' . $customurl . ' - '.$result); diff --git a/src/addons/mod/assign/tests/behat/basic_usage.feature b/src/addons/mod/assign/tests/behat/basic_usage.feature index 9cbf92c17..9adc224cb 100755 --- a/src/addons/mod/assign/tests/behat/basic_usage.feature +++ b/src/addons/mod/assign/tests/behat/basic_usage.feature @@ -132,7 +132,8 @@ Feature: Test basic usage of assignment activity in app Then I should find "No attempt" in the app Scenario: Add submission offline (online text) & Submit for grading offline & Sync submissions - Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "assignment1" in the app When I press "Add submission" in the app And I switch network connection to offline And I set the field "Online text submissions" to "Submission test" in the app @@ -150,7 +151,8 @@ Feature: Test basic usage of assignment activity in app But I should not find "This Assignment has offline data to be synchronised." in the app Scenario: Edit an offline submission before synchronising it - Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "assignment1" in the app When I press "Add submission" in the app And I switch network connection to offline And I set the field "Online text submissions" to "Submission test original offline" in the app @@ -178,8 +180,9 @@ Feature: Test basic usage of assignment activity in app @lms_from4.5 Scenario: Remove submission offline and syncrhonize it - Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app - And I press "Add submission" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "assignment1" in the app + When I press "Add submission" in the app And I set the field "Online text submissions" to "Submission test" in the app And I press "Save" in the app Then I should find "Draft (not submitted)" in the app @@ -223,7 +226,8 @@ Feature: Test basic usage of assignment activity in app @lms_from4.5 Scenario: Add submission offline after removing a submission offline - Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "assignment1" in the app When I press "Add submission" in the app And I set the field "Online text submissions" to "Submission test online" in the app And I press "Save" in the app diff --git a/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature b/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature index 739cada78..f7260640f 100755 --- a/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature +++ b/src/addons/mod/bigbluebuttonbn/tests/behat/basic_usage.feature @@ -25,7 +25,8 @@ Feature: Test basic usage of BBB activity in app | bigbluebuttonbn | BBB 1 | Test BBB description | C1 | bbb1 | 0 | ## 1 January 2050 00:00 ## | 0 | | bigbluebuttonbn | BBB 2 | Test BBB description | C1 | bbb2 | 0 | 0 | ## 1 January 2000 00:00 ## | | bigbluebuttonbn | BBB 3 | Test BBB description | C1 | bbb3 | 0 | ## 1 January 2000 00:00 ## | ## 1 January 2050 00:00 ## | - And I entered the bigbluebuttonbn activity "BBB 1" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "BBB 1" in the app Then I should find "The session has not started yet." in the app And I should find "Saturday, 1 January 2050, 12:00 AM" within "Open" "ion-item" in the app @@ -107,7 +108,8 @@ Feature: Test basic usage of BBB activity in app | bigbluebuttonbn | Room & recordings | C1 | bbb1 | 0 | | bigbluebuttonbn | Room only | C1 | bbb2 | 1 | | bigbluebuttonbn | Recordings only | C1 | bbb3 | 2 | - And I entered the bigbluebuttonbn activity "Room & recordings" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "Room & recordings" in the app Then I should find "This room is ready. You can join the session now." in the app And I should be able to press "Join session" in the app And I should find "Recordings" in the app diff --git a/src/addons/mod/choice/tests/behat/basic_usage.feature b/src/addons/mod/choice/tests/behat/basic_usage.feature index 5d087bfdf..fb44f8a54 100755 --- a/src/addons/mod/choice/tests/behat/basic_usage.feature +++ b/src/addons/mod/choice/tests/behat/basic_usage.feature @@ -21,7 +21,8 @@ Feature: Test basic usage of choice activity in app Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | | choice | Test single choice name | Test single choice description | C1 | choice1 | Option 1, Option 2, Option 3 | 0 | 0 | 1 | - And I entered the choice activity "Test single choice name" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "Test single choice name" in the app When I select "Option 1" in the app And I select "Option 2" in the app And I press "Save my choice" in the app @@ -74,7 +75,8 @@ Feature: Test basic usage of choice activity in app Given the following "activities" exist: | activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults | | choice | Test single choice name | Test single choice description | C1 | choice1 | Option 1, Option 2, Option 3 | 0 | 0 | 1 | - And I entered the choice activity "Test single choice name" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "Test single choice name" in the app When I select "Option 1" in the app And I switch network connection to offline And I select "Option 2" in the app diff --git a/src/addons/mod/data/tests/behat/sync.feature b/src/addons/mod/data/tests/behat/sync.feature index 50dccc49a..94d2e5a9c 100644 --- a/src/addons/mod/data/tests/behat/sync.feature +++ b/src/addons/mod/data/tests/behat/sync.feature @@ -28,9 +28,11 @@ Feature: Users can store entries in database activities when offline and sync wh | data1 | text | Description | Link description | Scenario: Create entry (offline) - Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Web links" in the app And I switch network connection to offline - And I should find "No entries yet" in the app + Then I should find "No entries yet" in the app + When I press "Add entry" in the app And I set the following fields to these values in the app: | URL | https://moodle.org/ | @@ -39,29 +41,33 @@ Feature: Users can store entries in database activities when offline and sync wh Then I should find "https://moodle.org/" in the app And I should find "Moodle community site" in the app And I should find "This Database has offline data to be synchronised" in the app - And I go back in the app + + When I go back in the app And I switch network connection to wifi And I press "Web links" near "General" in the app - And I should find "https://moodle.org/" in the app + Then I should find "https://moodle.org/" in the app And I should find "Moodle community site" in the app And I should not find "This Database has offline data to be synchronised" in the app Scenario: Update entry (offline) & Delete entry (offline) - Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app - And I should find "No entries yet" in the app - And I press "Add entry" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Web links" in the app + Then I should find "No entries yet" in the app + + When I press "Add entry" in the app And I set the following fields to these values in the app: | URL | https://moodle.org/ | | Description | Moodle community site | And I press "Save" near "Web links" in the app - And I should find "https://moodle.org/" in the app + Then I should find "https://moodle.org/" in the app And I should find "Moodle community site" in the app - And I press "Information" in the app + + When I press "Information" in the app And I press "Download" in the app And I wait until the page is ready And I close the popup in the app And I switch network connection to offline - When I press "Actions menu" in the app + And I press "Actions menu" in the app And I press "Edit" in the app And I set the following fields to these values in the app: | URL | https://moodlecloud.com/ | @@ -72,55 +78,64 @@ Feature: Users can store entries in database activities when offline and sync wh And I should find "https://moodlecloud.com/" in the app And I should find "Moodle Cloud" in the app And I should find "This Database has offline data to be synchronised" in the app - And I go back in the app + + When I go back in the app And I switch network connection to wifi And I press "Web links" near "General" in the app - And I should not find "https://moodle.org/" in the app + Then I should not find "https://moodle.org/" in the app And I should not find "Moodle community site" in the app And I should find "https://moodlecloud.com/" in the app And I should find "Moodle Cloud" in the app And I should not find "This Database has offline data to be synchronised" in the app - And I press "Information" in the app + + When I press "Information" in the app And I press "Refresh" in the app And I wait until the page is ready And I switch network connection to offline And I press "Actions menu" in the app And I press "Delete" in the app - And I should find "Are you sure you want to delete this entry?" in the app - And I press "Delete" in the app - And I should find "https://moodlecloud.com/" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + + When I press "Delete" in the app + Then I should find "https://moodlecloud.com/" in the app And I should find "Moodle Cloud" in the app And I should find "This Database has offline data to be synchronised" in the app - And I go back in the app + + When I go back in the app And I switch network connection to wifi And I press "Web links" near "General" in the app - And I should not find "https://moodlecloud.com/" in the app + Then I should not find "https://moodlecloud.com/" in the app And I should not find "Moodle Cloud" in the app And I should not find "This Database has offline data to be synchronised" in the app Scenario: Students can undo deleting entries to a database in the app while offline - Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app - And I should find "No entries yet" in the app - And I press "Add entry" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Web links" in the app + Then I should find "No entries yet" in the app + + When I press "Add entry" in the app And I set the following fields to these values in the app: | URL | https://moodle.org/ | | Description | Moodle community site | And I press "Save" near "Web links" in the app - And I should find "https://moodle.org/" in the app + Then I should find "https://moodle.org/" in the app And I should find "Moodle community site" in the app - And I press "Information" in the app + + When I press "Information" in the app And I press "Download" in the app And I wait until the page is ready And I close the popup in the app - When I switch network connection to offline + And I switch network connection to offline And I press "Actions menu" in the app And I press "Delete" in the app - And I should find "Are you sure you want to delete this entry?" in the app - And I press "Delete" in the app - And I should find "https://moodle.org/" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + + When I press "Delete" in the app + Then I should find "https://moodle.org/" in the app And I should find "Moodle community site" in the app And I should find "This Database has offline data to be synchronised" in the app - And I press "Actions menu" in the app + + When I press "Actions menu" in the app And I press "Restore" in the app And I go back in the app And I switch network connection to wifi diff --git a/src/addons/mod/forum/tests/behat/basic_usage.feature b/src/addons/mod/forum/tests/behat/basic_usage.feature index e0aebd9be..61dffc7b6 100755 --- a/src/addons/mod/forum/tests/behat/basic_usage.feature +++ b/src/addons/mod/forum/tests/behat/basic_usage.feature @@ -276,7 +276,8 @@ Feature: Test basic usage of forum activity in app But I should not find "Not sent" in the app Scenario: New discussion offline & Sync Forum - Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Test forum name" in the app When I switch network connection to offline And I press "Add discussion topic" in the app And I set the following fields to these values in the app: diff --git a/src/addons/mod/forum/tests/behat/groups.feature b/src/addons/mod/forum/tests/behat/groups.feature index 8c7aba79e..3cbdc09ef 100755 --- a/src/addons/mod/forum/tests/behat/groups.feature +++ b/src/addons/mod/forum/tests/behat/groups.feature @@ -35,7 +35,8 @@ Feature: Test usage of forum activity with groups in app | forum2 | Disc vis ALL | Disc vis ALL | Disc vis ALL content | All participants | Scenario: Student can only see the right groups - Given I entered the forum activity "Separate groups forum" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Separate groups forum" in the app Then I should find "Disc sep G1" in the app And I should find "Disc sep ALL" in the app But I should not find "Disc sep G2" in the app @@ -65,7 +66,8 @@ Feature: Test usage of forum activity with groups in app But I should not find "Disc vis G2" in the app Scenario: Teacher can see all groups - Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app + Given I entered the course "Course 1" as "teacher1" in the app + And I press "Separate groups forum" in the app When I press "Separate groups" in the app Then I should find "All participants" in the app And I should find "Group 1" in the app @@ -107,7 +109,8 @@ Feature: Test usage of forum activity with groups in app But I should not find "Disc vis G2" in the app Scenario: Student can only add discussions in his groups - Given I entered the forum activity "Separate groups forum" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Separate groups forum" in the app When I press "Add discussion topic" in the app And I press "Advanced" in the app Then I should not find "Post a copy to all groups" in the app @@ -165,8 +168,9 @@ Feature: Test usage of forum activity with groups in app Then I should find "My happy subject" in the app Scenario: Teacher can add discussion to any group - Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app - And I press "Separate groups" in the app + Given I entered the course "Course 1" as "teacher1" in the app + And I press "Separate groups forum" in the app + When I press "Separate groups" in the app And I press "All participants" in the app And I press "Add discussion topic" in the app And I press "Advanced" in the app @@ -279,8 +283,9 @@ Feature: Test usage of forum activity with groups in app Then I should find "My third subject" in the app Scenario: Teacher can post a copy in all groups - Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app - And I press "Separate groups" in the app + Given I entered the course "Course 1" as "teacher1" in the app + And I press "Separate groups forum" in the app + When I press "Separate groups" in the app And I press "Group 1" in the app And I press "Add discussion topic" in the app And I press "Advanced" in the app diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index b11156c46..c3ccd71ad 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -52,7 +52,8 @@ Feature: Test basic usage of glossary in app Scenario: Navigate to glossary terms by link (auto-linking) Given the "glossary" filter is "on" - And I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "Test glossary" in the app Then the header should be "Test glossary" in the app And I should find "Eggplant" in the app And I should find "Cucumber" in the app diff --git a/src/addons/mod/quiz/tests/behat/can_review.feature b/src/addons/mod/quiz/tests/behat/can_review.feature index 870be3bb2..364d1dfeb 100644 --- a/src/addons/mod/quiz/tests/behat/can_review.feature +++ b/src/addons/mod/quiz/tests/behat/can_review.feature @@ -49,8 +49,9 @@ Feature: Users can only review attempts that are allowed to be reviewed | 1 | True | Scenario: Can review only when the attempt is allowed to be reviewed - Given I entered the quiz activity "Quiz review after immed" on course "Course 1" as "student1" in the app - And I press "Attempt 1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Quiz review after immed" in the app + When I press "Attempt 1" in the app Then I should not be able to press "Review" in the app When I go back in the app diff --git a/src/addons/mod/survey/tests/behat/basic_usage.feature b/src/addons/mod/survey/tests/behat/basic_usage.feature index 894f90f37..5ee4ed3e6 100755 --- a/src/addons/mod/survey/tests/behat/basic_usage.feature +++ b/src/addons/mod/survey/tests/behat/basic_usage.feature @@ -238,8 +238,9 @@ Feature: Test basic usage of survey activity in app Given the following "activities" exist: | activity | name | intro | template | course | idnumber | groupmode | | survey | Test survey critical incidents | Test survey1 | 5 | C1 | survey1 | 0 | - Given I entered the survey activity "Test survey critical incidents" on course "Course 1" as "student1" in the app - And I switch network connection to offline + And I entered the course "Course 1" as "student1" in the app + And I press "Test survey critical incidents" in the app + When I switch network connection to offline And I press "Submit" in the app And I press "OK" in the app Then I should see "This Survey has offline data to be synchronised." diff --git a/src/addons/mod/workshop/tests/behat/basic_usage.feature b/src/addons/mod/workshop/tests/behat/basic_usage.feature index 06441f66d..1199f166f 100644 --- a/src/addons/mod/workshop/tests/behat/basic_usage.feature +++ b/src/addons/mod/workshop/tests/behat/basic_usage.feature @@ -130,7 +130,8 @@ Feature: Test basic usage of workshop activity in app | \mod_workshop\event\course_module_viewed | workshop | Test workshop | Course 1 | Scenario: Prefetch a workshop - Given I entered the workshop activity "workshop" on course "Course 1" as "teacher1" in the app + Given I entered the course "Course 1" as "teacher1" in the app + And I press "workshop" in the app When I press "Information" in the app And I press "Download" in the app And I press "Close" in the app diff --git a/src/core/features/contentlinks/services/contentlinks-delegate.ts b/src/core/features/contentlinks/services/contentlinks-delegate.ts index 2b3e1a93c..d76bb7ca0 100644 --- a/src/core/features/contentlinks/services/contentlinks-delegate.ts +++ b/src/core/features/contentlinks/services/contentlinks-delegate.ts @@ -19,6 +19,7 @@ import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { CoreText } from '@singletons/text'; import { CorePromiseUtils } from '@singletons/promise-utils'; +import { CoreNavigator } from '@services/navigator'; /** * Interface that all handlers must implement. @@ -208,24 +209,24 @@ export class CoreContentLinksDelegateService { // Wrap the action function in our own function to treat logged out sites. const actionFunction = action.action; action.action = async (siteId) => { - const site = await CoreSites.getSite(siteId); + if (!CoreSites.isLoggedIn()) { + // Not logged in, load site first. + const loggedIn = await CoreSites.loadSite(siteId, { urlToOpen: url }); + if (loggedIn) { + await CoreNavigator.navigateToSiteHome({ params: { urlToOpen: url } }); + } - if (!site.isLoggedOut()) { - // Call the action now. - return actionFunction(siteId); + return; } - // Site is logged out, authenticate first before treating the URL. - const willReload = await CoreSites.logoutForRedirect(siteId, { - urlToOpen: url, - }); + if (siteId !== CoreSites.getCurrentSiteId()) { + // Different site, logout and login first before treating the URL because token could be expired. + await CoreSites.logout({ urlToOpen: url, siteId }); - if (!willReload) { - // Load the site with the redirect data. - await CoreSites.loadSite(siteId, { - urlToOpen: url, - }); + return; } + + actionFunction(siteId); }; }); diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index b0d3d8fbd..8030f6411 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -41,6 +41,10 @@ const appRoutes: Routes = [ loadChildren: () => import('./login-lazy.module'), canActivate: [redirectGuard], }, + { + path: 'logout', + loadComponent: () => import('@features/login/pages/logout/logout'), + }, ]; @NgModule({ diff --git a/src/core/features/login/pages/logout/logout.html b/src/core/features/login/pages/logout/logout.html new file mode 100644 index 000000000..1e61e89a5 --- /dev/null +++ b/src/core/features/login/pages/logout/logout.html @@ -0,0 +1,3 @@ + + + diff --git a/src/core/features/login/pages/logout/logout.ts b/src/core/features/login/pages/logout/logout.ts new file mode 100644 index 000000000..db0bc10ff --- /dev/null +++ b/src/core/features/login/pages/logout/logout.ts @@ -0,0 +1,104 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreConstants } from '@/core/constants'; +import { CoreNavigationOptions, CoreNavigator, CoreRedirectPayload } from '@services/navigator'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { CoreRedirects } from '@singletons/redirects'; + +/** + * Page that logs the user out. + */ +@Component({ + selector: 'page-core-login-logout', + templateUrl: 'logout.html', + standalone: true, + imports: [ + CoreSharedModule, + ], +}) +export default class CoreLoginLogoutPage implements OnInit { + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + const siteId = CoreNavigator.getRouteParam('siteId') ?? CoreConstants.NO_SITE_ID; + const logoutOptions = { + forceLogout: CoreNavigator.getRouteBooleanParam('forceLogout'), + removeAccount: CoreNavigator.getRouteBooleanParam('removeAccount') ?? !!CoreConstants.CONFIG.removeaccountonlogout, + }; + const redirectData = { + redirectPath: CoreNavigator.getRouteParam('redirectPath'), + redirectOptions: CoreNavigator.getRouteParam('redirectOptions'), + urlToOpen: CoreNavigator.getRouteParam('urlToOpen'), + }; + + if (!CoreSites.isLoggedIn()) { + // This page shouldn't open if user isn't logged in, but if that happens just navigate to the right page. + await this.navigateAfterLogout(siteId, redirectData); + + return; + } + + const shouldReload = CoreSitePlugins.hasSitePluginsLoaded; + if (shouldReload && (siteId !== CoreConstants.NO_SITE_ID || redirectData.redirectPath || redirectData.urlToOpen)) { + // The app will reload and we need to open a page that isn't the default page. Store the redirect first. + CoreRedirects.storeRedirect(siteId, redirectData); + } + + await CoreSites.internalLogout(logoutOptions); + + if (shouldReload) { + // We need to reload the app to unload all the plugins. Leave the logout page first. + await CoreNavigator.navigate('/login', { reset: true }); + + window.location.reload(); + + return; + } + + await this.navigateAfterLogout(siteId, redirectData); + } + + /** + * Navigate to the right page after logout is done. + * + * @param siteId Site ID to load. + * @param redirectData Redirect data. + */ + protected async navigateAfterLogout(siteId: string, redirectData: CoreRedirectPayload): Promise { + if (siteId === CoreConstants.NO_SITE_ID) { + // No site to load now, just navigate. + await CoreNavigator.navigate(redirectData.redirectPath ?? '/login/sites', { + ...redirectData.redirectOptions, + reset: true, + }); + + return; + } + + // Load the site and navigate. + const loggedIn = await CoreSites.loadSite(siteId, redirectData); + if (!loggedIn) { + return; // Session expired. + } + + await CoreNavigator.navigateToSiteHome({ params: redirectData, preferCurrentTab: false, siteId }); + } + +} diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 9be527c82..23ee93791 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -412,22 +412,19 @@ export class CoreLoginHelperProvider { * @returns Promise resolved when done. */ async goToAddSite(setRoot = false, showKeyboard = false): Promise { - let path = '/login/sites'; - let params: Params = { openAddSite: true , showKeyboard }; - if (CoreSites.isLoggedIn()) { - const willReload = await CoreSites.logoutForRedirect(CoreConstants.NO_SITE_ID, { - redirectPath: path, - redirectOptions: { params }, + // Logout first. + await CoreSites.logout({ + siteId: CoreConstants.NO_SITE_ID, + redirectPath: '/login/sites', + redirectOptions: { params: { openAddSite: true , showKeyboard } }, }); - if (willReload) { - return; - } - } else { - [path, params] = await this.getAddSiteRouteInfo(showKeyboard); + return; } + const [path, params] = await this.getAddSiteRouteInfo(showKeyboard); + await CoreNavigator.navigate(path, { params, reset: setRoot }); } diff --git a/src/core/features/login/tests/behat/basic_usage.feature b/src/core/features/login/tests/behat/basic_usage.feature index 2285a9d93..39bde7341 100755 --- a/src/core/features/login/tests/behat/basic_usage.feature +++ b/src/core/features/login/tests/behat/basic_usage.feature @@ -66,13 +66,6 @@ Feature: Test basic usage of login in app And I press "Connect to your site" in the app Then I should find "Can't connect to site" in the app - Scenario: Log out from the app - Given I entered the app as "student1" - And I press the user menu button in the app - When I press "Log out" in the app - And I wait the app to restart - Then the header should be "Accounts" in the app - Scenario: Delete an account Given I entered the app as "student1" When I log out in the app diff --git a/src/core/features/login/tests/behat/logout.feature b/src/core/features/login/tests/behat/logout.feature new file mode 100755 index 000000000..15b1ebb73 --- /dev/null +++ b/src/core/features/login/tests/behat/logout.feature @@ -0,0 +1,160 @@ +@core_login @app @javascript +Feature: Test different cases of logout and switch account + I need different logout use cases to work + + Background: + Given the following "users" exist: + | username | firstname | lastname | + | student1 | david | student | + | student2 | pau | student2 | + + Scenario: Log out and re-login + Given I entered the app as "student1" + When I press the user menu button in the app + And I press "Log out" in the app + And I wait the app to restart + Then the header should be "Accounts" in the app + + When I press "david student" in the app + Then the header should be "Reconnect" in the app + And I should find "david student" in the app + + When I set the following fields to these values in the app: + | Password | student1 | + And I press "Log in" near "Lost password?" in the app + Then the header should be "Acceptance test site" in the app + + Scenario: Exit account using switch account and re-enter + Given I entered the app as "student1" + When I press the user menu button in the app + And I press "Switch account" in the app + And I press "Add" in the app + And I wait the app to restart + Then I should find "Connect to Moodle" in the app + + When I go back in the app + And I press "david student" in the app + Then the header should be "Acceptance test site" in the app + + Scenario: Exit account using switch account and re-enter when forcelogout is enabled + Given the following config values are set as admin: + | forcelogout | 1 | tool_mobile | + And I entered the app as "student1" + When I press the user menu button in the app + And I press "Switch account" in the app + And I press "Add" in the app + And I wait the app to restart + And I go back in the app + And I press "david student" in the app + Then the header should be "Reconnect" in the app + And I should find "david student" in the app + + Scenario: Switch to a different account + Given I entered the app as "student1" + And I entered the app as "student2" + When I press the user menu button in the app + Then I should find "pau student2" in the app + + When I press "Switch account" in the app + And I press "david student" in the app + And I wait the app to restart + Then the header should be "Acceptance test site" in the app + + When I press the user menu button in the app + Then I should find "david student" in the app + + Scenario: Logout when there is unsaved data + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum | Test forum | C1 | forum | + And the following forum discussions exist in course "Course 1": + | forum | user | name | message | + | Test forum | student1 | Forum topic 1 | Forum message 1 | + | Test forum | student1 | Forum topic 2 | Forum message 2 | + And I entered the course "Course 1" as "student1" in the app + And I change viewport size to "1200x640" in the app + + When I press "Test forum" in the app + And I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Log out" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + # Check that the app continues working fine if the user cancels the logout. + When I press "Cancel" in the app + And I press "Forum topic 1" in the app + And I press "OK" in the app + Then I should find "Forum message 1" in the app + + When I press "Forum topic 2" in the app + Then I should find "Forum message 2" in the app + + # Now confirm the logout. + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Log out" in the app + And I press "OK" in the app + And I wait the app to restart + Then the header should be "Accounts" in the app + + Scenario: Switch account when there is unsaved data + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum | Test forum | C1 | forum | + And the following forum discussions exist in course "Course 1": + | forum | user | name | message | + | Test forum | student1 | Forum topic 1 | Forum message 1 | + | Test forum | student1 | Forum topic 2 | Forum message 2 | + And I entered the app as "student2" + And I entered the course "Course 1" as "student1" in the app + And I change viewport size to "1200x640" in the app + + When I press "Test forum" in the app + And I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Switch account" in the app + And I press "pau student2" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + # Check that the app continues working fine if the user cancels the switch account. + When I press "Cancel" in the app + And I press "Forum topic 1" in the app + And I press "OK" in the app + Then I should find "Forum message 1" in the app + + When I press "Forum topic 2" in the app + Then I should find "Forum message 2" in the app + + # Now confirm the switch account. + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Switch account" in the app + And I press "pau student2" in the app + And I press "OK" in the app + And I wait the app to restart + And I press the user menu button in the app + Then I should find "pau student2" in the app diff --git a/src/core/features/login/tests/behat/showloginform_setting.feature b/src/core/features/login/tests/behat/showloginform_setting.feature index 7da2d8b77..0c6a2581c 100644 --- a/src/core/features/login/tests/behat/showloginform_setting.feature +++ b/src/core/features/login/tests/behat/showloginform_setting.feature @@ -48,7 +48,7 @@ Feature: Test showloginform setting in the app And I press "Moodle Mobile" in the app And I press "Developer options" in the app And I press "Always show login form" in the app - And I go back 4 times in the app + And I go back to the root page in the app And I press "david student" in the app Then the header should be "Reconnect" in the app And I should find "Log in" "ion-button" in the app diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.ts b/src/core/features/mainmenu/components/user-menu/user-menu.ts index e52071150..1aecec73f 100644 --- a/src/core/features/mainmenu/components/user-menu/user-menu.ts +++ b/src/core/features/mainmenu/components/user-menu/user-menu.ts @@ -18,7 +18,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreSite } from '@classes/sites/site'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; import { CoreFilter } from '@features/filter/services/filter'; -import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreUserSupport } from '@features/user/services/support'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; @@ -33,8 +32,9 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CorePromiseUtils } from '@singletons/promise-utils'; -import { ModalController, Translate } from '@singletons'; +import { ModalController } from '@singletons'; import { Subscription } from 'rxjs'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; /** * Component to display a user menu. @@ -208,12 +208,6 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async logout(event: Event): Promise { - if (CoreNavigator.currentRouteCanBlockLeave()) { - await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); - - return; - } - if (this.removeAccountOnLogout) { // Ask confirm. const siteName = this.siteName ? @@ -242,12 +236,6 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { * @param event Click event */ async switchAccounts(event: Event): Promise { - if (CoreNavigator.currentRouteCanBlockLeave()) { - await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks')); - - return; - } - const thisModal = await ModalController.getTop(); event.preventDefault(); diff --git a/src/core/features/policy/pages/site-policy/site-policy.ts b/src/core/features/policy/pages/site-policy/site-policy.ts index 94be08c61..dfab2c14f 100644 --- a/src/core/features/policy/pages/site-policy/site-policy.ts +++ b/src/core/features/policy/pages/site-policy/site-policy.ts @@ -280,9 +280,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy { * @returns Promise resolved when done. */ async cancel(): Promise { - await CorePromiseUtils.ignoreErrors(CoreSites.logout()); - - await CoreNavigator.navigate('/login/sites', { reset: true }); + await CoreSites.logout(); } /** diff --git a/src/core/features/settings/tests/behat/sync.feature b/src/core/features/settings/tests/behat/sync.feature index 22020eec2..564ab37b1 100644 --- a/src/core/features/settings/tests/behat/sync.feature +++ b/src/core/features/settings/tests/behat/sync.feature @@ -19,7 +19,8 @@ Feature: It synchronise sites properly Scenario: Sync the current site # Add something offline - Given I entered the choice activity "Sync choice" on course "Course 1" as "student1" in the app + Given I entered the course "Course 1" as "student1" in the app + And I press "Sync choice" in the app When I switch network connection to offline And I select "Option 1" in the app And I press "Save my choice" in the app diff --git a/src/core/features/sitehome/tests/links.test.ts b/src/core/features/sitehome/tests/links.test.ts index f137e69ec..c0f58e06f 100644 --- a/src/core/features/sitehome/tests/links.test.ts +++ b/src/core/features/sitehome/tests/links.test.ts @@ -32,6 +32,8 @@ describe('Site Home link handlers', () => { isStoredRootURL: () => Promise.resolve({ siteIds: [siteId] }), getSite: () => Promise.resolve(new CoreSite(siteId, siteUrl, '')), getSiteIdsFromUrl: () => Promise.resolve([siteId]), + getCurrentSiteId: () => siteId, + isLoggedIn: () => true, })); mockSingleton(CoreLoginHelper, { getAvailableSites: async () => [{ url: siteUrl, name: 'Example Campus' }] }); diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 6632359f7..0c840b043 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -195,7 +195,7 @@ export class CoreNavigatorService { async navigateToSiteHome(options: Omit & { siteId?: string } = {}): Promise { const siteId = options.siteId ?? CoreSites.getCurrentSiteId(); const landingPagePath = CoreSites.isLoggedIn() && CoreSites.getCurrentSiteId() === siteId ? - this.getLandingTabPage() : 'main'; + this.getLandingTabPage() : ''; return this.navigateToSitePath(landingPagePath, { ...options, @@ -220,14 +220,12 @@ export class CoreNavigatorService { // If we are logged into a different site, log out first. if (CoreSites.isLoggedIn() && CoreSites.getCurrentSiteId() !== siteId) { - const willReload = await CoreSites.logoutForRedirect(siteId, { - redirectPath: path, - redirectOptions: options || {}, + await CoreSites.logout({ + ...this.getRedirectDataForSitePath(path, options), + siteId, }); - if (willReload) { - return true; - } + return true; } // If the path doesn't belong to a site, call standard navigation. @@ -243,10 +241,7 @@ export class CoreNavigatorService { const modal = await CoreLoadings.show(); try { - const loggedIn = await CoreSites.loadSite(siteId, { - redirectPath: path, - redirectOptions: options, - }); + const loggedIn = await CoreSites.loadSite(siteId, this.getRedirectDataForSitePath(path, options)); if (!loggedIn) { // User has been redirected to the login page and will be redirected to the site path after login. @@ -264,6 +259,31 @@ export class CoreNavigatorService { return this.navigateToMainMenuPath(path, navigationOptions); } + /** + * Get the redirect data to use when navigating to a site path. + * + * @param path Site path. + * @param options Navigation options. + * @returns Redirect data. + */ + protected getRedirectDataForSitePath(path: string, options: CoreNavigationOptions = {}): CoreRedirectPayload { + if (!path || path.match(/^\/?main\/?$/)) { + // Navigating to main, obtain the redirect from the navigation parameters (if any). + // If there is no redirect path or url to open, use 'main' to open the site's main menu. + return { + redirectPath: !options.params?.redirectPath && !options.params?.urlToOpen ? 'main' : options.params?.redirectPath, + redirectOptions: options.params?.redirectOptions, + urlToOpen: options.params?.urlToOpen, + }; + } + + // Use the path to navigate as the redirect path. + return { + redirectPath: path, + redirectOptions: options || {}, + }; + } + /** * Get the active route path. * @@ -547,6 +567,11 @@ export class CoreNavigatorService { ...options, }; + if (!path || path.match(/^\/?main\/?$/)) { + // Navigating to main, nothing else to do. + return this.navigate('/main', options); + } + path = path.replace(/^(\.|\/main)?\//, ''); const pathRoot = /^[^/]+/.exec(path)?.[0] ?? ''; diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 1dfdd725e..b761cc9f6 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -141,14 +141,6 @@ export class CoreSitesProvider { // Remove version classes from body. CoreHTMLClasses.removeSiteClasses(); - - // Go to sites page when user is logged out. - await CoreNavigator.navigate('/login/sites', { reset: true }); - - if (CoreSitePlugins.hasSitePluginsLoaded) { - // Temporary fix. Reload the page to unload all plugins. - window.location.reload(); - } }); CoreEvents.on(CoreEvents.LOGIN, async (data) => { @@ -964,7 +956,7 @@ export class CoreSitesProvider { promise.finally(() => { if (siteId) { // Logout the currentSite and expire the token. - this.logout(); + this.internalLogout(); this.setSiteLoggedOut(siteId); } }); @@ -1123,7 +1115,7 @@ export class CoreSitesProvider { this.logger.debug(`Delete site ${siteId}`); if (this.currentSite !== undefined && this.currentSite.id == siteId) { - this.logout(); + this.internalLogout(); } const site = await this.getSite(siteId); @@ -1457,10 +1449,23 @@ export class CoreSitesProvider { /** * Logout the user. * - * @param options Logout options. - * @returns Promise resolved when the user is logged out. + * @param options Options. */ async logout(options: CoreSitesLogoutOptions = {}): Promise { + await CoreNavigator.navigate('/logout', { + params: { ...options }, + reset: true, + }); + } + + /** + * Logout the user. + * This function is for internal usage, please use CoreSites.logout instead. The reason this function is public is because + * it's called from the CoreLoginLogoutPage page. + * + * @param options Logout options. + */ + async internalLogout(options: InternalLogoutOptions = {}): Promise { if (!this.currentSite) { return; } @@ -1494,6 +1499,7 @@ export class CoreSitesProvider { * @param siteId Site that will be opened after logout. * @param redirectData Page/url to open after logout. * @returns Promise resolved with boolean: true if app will be reloaded after logout. + * @deprecated since 5.0. Use CoreSites.logout instead, it automatically handles redirects. */ async logoutForRedirect(siteId: string, redirectData: CoreRedirectPayload): Promise { if (!this.currentSite) { @@ -1505,7 +1511,7 @@ export class CoreSitesProvider { CoreRedirects.storeRedirect(siteId, redirectData); } - await this.logout(); + await this.internalLogout(); return CoreSitePlugins.hasSitePluginsLoaded; } @@ -2480,7 +2486,14 @@ export type CoreSitesLoginTokenResponse = { /** * Options for logout. */ -export type CoreSitesLogoutOptions = { +export type CoreSitesLogoutOptions = CoreRedirectPayload & InternalLogoutOptions & { + siteId?: string; // Site ID to load after logout. +}; + +/** + * Options for internal logout. + */ +type InternalLogoutOptions = { forceLogout?: boolean; // If true, site will be marked as logged out, no matter the value tool_mobile_forcelogout. removeAccount?: boolean; // If true, site will be removed too after logout. }; diff --git a/src/core/services/tests/sites.test.ts b/src/core/services/tests/sites.test.ts index 68e477405..961c84708 100644 --- a/src/core/services/tests/sites.test.ts +++ b/src/core/services/tests/sites.test.ts @@ -16,7 +16,6 @@ import { CoreEvents } from '@singletons/events'; import { CoreLang, CoreLangProvider } from '@services/lang'; import { mock, mockSingleton } from '@/testing/utils'; -import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { Http } from '@singletons'; import { of } from 'rxjs'; @@ -34,13 +33,10 @@ describe('CoreSitesProvider', () => { }); it('cleans up on logout', async () => { - const navigator: CoreNavigatorService = mockSingleton(CoreNavigator, ['navigate']); - CoreSites.initialize(); CoreEvents.trigger(CoreEvents.LOGOUT); expect(langProvider.clearCustomStrings).toHaveBeenCalled(); - expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); }); it('adds ionic platform and theme classes', async () => { diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index 65cd87bc4..833bdcc8e 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -432,14 +432,13 @@ export class CoreCustomURLSchemesProvider { // Ask the user before changing site. await CoreDomUtils.showConfirm(Translate.instant('core.contentlinks.confirmurlothersite')); - const willReload = await CoreSites.logoutForRedirect(CoreConstants.NO_SITE_ID, { + await CoreSites.logout({ + siteId: CoreConstants.NO_SITE_ID, redirectPath: '/login/credentials', redirectOptions: { params: pageParams }, }); - if (willReload) { - return; - } + return; } await CoreNavigator.navigateToLoginCredentials(pageParams); diff --git a/src/core/tests/behat/navigation_deeplinks.feature b/src/core/tests/behat/navigation_deeplinks.feature index 41bd254c2..7f858d53b 100644 --- a/src/core/tests/behat/navigation_deeplinks.feature +++ b/src/core/tests/behat/navigation_deeplinks.feature @@ -3,9 +3,9 @@ Feature: It navigates properly using deep links. Background: Given the following "users" exist: - | username | - | student1 | - | student2 | + | username | firstname | lastname | + | student1 | david | student | + | student2 | pau | student2 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | @@ -20,7 +20,6 @@ Feature: It navigates properly using deep links. | forum | user | name | message | | Test forum | student1 | Forum topic | Forum message | And the following config values are set as admin: - | forcelogout | 1 | tool_mobile | | defaulthomepage | 0 | | Scenario: Receive a push notification @@ -78,3 +77,98 @@ Feature: It navigates properly using deep links. When I go back in the app Then I should find "Site home" in the app But I should not find "Test forum" in the app + + Scenario: Open a deep link in a different account not stored in the app + Given I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then the header should be "Log in" in the app + + When I set the following fields to these values in the app: + | Password | student2 | + And I press "Log in" near "Lost password?" in the app + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account stored in the app + Given I entered the app as "student2" + And I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account stored in the app but logged out + Given I entered the app as "student2" + And I press the user menu button in the app + And I press "Log out" in the app + And I wait the app to restart + And I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then the header should be "Reconnect" in the app + And I should find "pau student2" in the app + + When I set the following fields to these values in the app: + | Password | student2 | + And I press "Log in" near "Lost password?" in the app + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account when there is unsaved data + Given I entered the app as "student2" + And I entered the forum activity "Test forum" on course "Course 1" as "student1" in the app + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + When I press "Cancel" in the app + Then I should not find "Forum message" in the app + + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + And I press "OK" in the app + And I press "OK" in the app + And I wait the app to restart + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app diff --git a/src/core/tests/behat/open_files.feature b/src/core/tests/behat/open_files.feature index 9e7e097b9..9f675e067 100644 --- a/src/core/tests/behat/open_files.feature +++ b/src/core/tests/behat/open_files.feature @@ -21,7 +21,8 @@ Feature: It opens files properly. | resource | Test DOC | Test DOC description | 5 | C1 | A doc.doc | And the following config values are set as admin: | filetypeexclusionlist | rtf,doc | tool_mobile | - And I entered the resource activity "Test TXT" on course "Course 1" as "student1" in the app + And I entered the course "Course 1" as "student1" in the app + And I press "Test TXT" in the app When I press "Open" in the app Then the app should have opened a browser tab with url "^blob:" diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index f6c7d7ed0..74d0b717a 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -136,19 +136,39 @@ export class TestingBehatRuntimeService { * Run an operation inside the angular zone and return result. * * @param operation Operation callback. + * @param blocking Whether the operation is blocking or not. + * @param locatorToFind If set, when this locator is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. * @returns OK if successful, or ERROR: followed by message. */ - async runInZone(operation: () => unknown, blocking: boolean = false): Promise { + async runInZone( + operation: () => unknown, + blocking: boolean = false, + locatorToFind?: TestingBehatElementLocator, + ): Promise { const blockKey = blocking && TestingBehatBlocking.block(); + let interval: number | undefined; try { - await NgZone.run(operation); + await new Promise((resolve, reject) => { + Promise.resolve(NgZone.run(operation)).then(resolve).catch(reject); + + if (locatorToFind) { + interval = window.setInterval(() => { + if (TestingBehatDomUtils.findElementBasedOnText(locatorToFind, { onlyClickable: false })) { + clearInterval(interval); + resolve(); + } + }, 500); + } + }); return 'OK'; } catch (error) { return 'ERROR: ' + error.message; } finally { blockKey && TestingBehatBlocking.unblock(blockKey); + window.clearInterval(interval); } } diff --git a/upgrade.txt b/upgrade.txt index edb6425e0..a9429f01f 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -2,6 +2,10 @@ This file describes API changes in the Moodle App that affect site plugins, info For more information about upgrading, read the official documentation: https://moodledev.io/general/app/upgrading/ +=== 5.0.0 === + + - The logout process has been refactored, now it uses a logout page to trigger Angular guards. CoreSites.logout now uses this process, and CoreSites.logoutForRedirect is deprecated and shouldn't be used anymore. + === 4.5.0 === - Ionic has been upgraded to major version 8. See breaking changes and upgrade guide here: https://ionicframework.com/docs/updating/8-0