Merge pull request #4245 from dpalou/MOBILE-4028

Mobile 4028
main
Pau Ferrer Ocaña 2024-11-25 14:53:06 +01:00 committed by GitHub
commit 476a1795ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 639 additions and 159 deletions

View File

@ -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);
}
/**

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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

View File

@ -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);
};
});

View File

@ -41,6 +41,10 @@ const appRoutes: Routes = [
loadChildren: () => import('./login-lazy.module'),
canActivate: [redirectGuard],
},
{
path: 'logout',
loadComponent: () => import('@features/login/pages/logout/logout'),
},
];
@NgModule({

View File

@ -0,0 +1,3 @@
<ion-content>
<core-loading />
</ion-content>

View File

@ -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<void> {
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<CoreNavigationOptions>('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<void> {
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 });
}
}

View File

@ -412,22 +412,19 @@ export class CoreLoginHelperProvider {
* @returns Promise resolved when done.
*/
async goToAddSite(setRoot = false, showKeyboard = false): Promise<void> {
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 });
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<void> {
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<void> {
if (CoreNavigator.currentRouteCanBlockLeave()) {
await CoreDomUtils.showAlert(undefined, Translate.instant('core.cannotlogoutpageblocks'));
return;
}
const thisModal = await ModalController.getTop();
event.preventDefault();

View File

@ -280,9 +280,7 @@ export class CorePolicySitePolicyPage implements OnInit, OnDestroy {
* @returns Promise resolved when done.
*/
async cancel(): Promise<void> {
await CorePromiseUtils.ignoreErrors(CoreSites.logout());
await CoreNavigator.navigate('/login/sites', { reset: true });
await CoreSites.logout();
}
/**

View File

@ -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

View File

@ -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' }] });

View File

@ -195,7 +195,7 @@ export class CoreNavigatorService {
async navigateToSiteHome(options: Omit<CoreNavigationOptions, 'reset'> & { siteId?: string } = {}): Promise<boolean> {
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] ?? '';

View File

@ -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<void> {
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<void> {
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<boolean> {
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.
};

View File

@ -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 () => {

View File

@ -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);

View File

@ -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

View File

@ -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:"

View File

@ -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<string> {
async runInZone(
operation: () => unknown,
blocking: boolean = false,
locatorToFind?: TestingBehatElementLocator,
): Promise<string> {
const blockKey = blocking && TestingBehatBlocking.block();
let interval: number | undefined;
try {
await NgZone.run(operation);
await new Promise<void>((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);
}
}

View File

@ -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