Merge pull request #3321 from crazyserver/MOBILE-4047

Mobile 4047
main
Dani Palou 2022-07-13 15:27:12 +02:00 committed by GitHub
commit 5630f5b054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 701 additions and 271 deletions

View File

@ -91,8 +91,6 @@ class behat_app extends behat_app_helper {
* @throws ExpectationException Problem with resizing window * @throws ExpectationException Problem with resizing window
*/ */
public function i_launch_the_app(string $runtime = '') { public function i_launch_the_app(string $runtime = '') {
$this->check_tags();
// Go to page and prepare browser for app. // Go to page and prepare browser for app.
$this->prepare_browser(['skiponboarding' => empty($runtime)]); $this->prepare_browser(['skiponboarding' => empty($runtime)]);
} }
@ -101,18 +99,27 @@ class behat_app extends behat_app_helper {
* @Then I wait the app to restart * @Then I wait the app to restart
*/ */
public function i_wait_the_app_to_restart() { public function i_wait_the_app_to_restart() {
// Wait window to reload.
$this->spin(function() {
if ($this->runtime_js('hasInitialized()')) {
// Behat runtime shouldn't be initialized after reload.
throw new DriverException('Window is not reloading properly.');
}
return true;
});
// Prepare testing runtime again. // Prepare testing runtime again.
$this->prepare_browser(['restart' => false]); $this->prepare_browser();
}
/**
* @Then I log out in the app
*
* @param bool $force If force logout or not.
*/
public function i_log_out_in_app($force = true) {
$options = json_encode([
'forceLogout' => $force,
]);
$result = $this->zone_js("sites.logout($options)");
if ($result !== 'OK') {
throw new DriverException('Error on log out - ' . $result);
}
$this->i_wait_the_app_to_restart();
} }
/** /**
@ -567,7 +574,7 @@ class behat_app extends behat_app_helper {
/** /**
* Performs a pull to refresh gesture. * Performs a pull to refresh gesture.
* *
* @When /^I pull to refresh in the app$/ * @When I pull to refresh in the app
* @throws DriverException If the gesture is not available * @throws DriverException If the gesture is not available
*/ */
public function i_pull_to_refresh_in_the_app() { public function i_pull_to_refresh_in_the_app() {
@ -841,9 +848,31 @@ class behat_app extends behat_app_helper {
* @Given /^I switch offline mode to "(true|false)"$/ * @Given /^I switch offline mode to "(true|false)"$/
* @param string $offline New value for navigator online mode * @param string $offline New value for navigator online mode
* @throws DriverException If the navigator.online mode is not available * @throws DriverException If the navigator.online mode is not available
* @deprecated since 4.1 use i_switch_network_connection instead.
*/ */
public function i_switch_offline_mode(string $offline) { public function i_switch_offline_mode(string $offline) {
$this->runtime_js("network.setForceOffline($offline)"); $this->i_switch_network_connection($offline == 'true' ? 'offline' : 'wifi');
}
/**
* Switch network connection.
*
* @When /^I switch network connection to (wifi|cellular|offline)$/
* @param string $more New network mode.
* @throws DriverException If the navigator.online mode is not available
*/
public function i_switch_network_connection(string $mode) {
switch ($mode) {
case 'wifi':
$this->runtime_js("network.setForceConnectionMode('$mode');");
break;
case 'cellular':
$this->runtime_js("network.setForceConnectionMode('$mode');");
break;
case 'offline':
$this->runtime_js("network.setForceConnectionMode('none');");
break;
}
} }
} }

View File

@ -95,6 +95,12 @@ class behat_app_helper extends behat_base {
public function start_scenario() { public function start_scenario() {
$this->check_behat_setup(); $this->check_behat_setup();
$this->fix_moodle_setup(); $this->fix_moodle_setup();
if ($this->apprunning) {
$this->notify_unload();
$this->apprunning = false;
}
$this->ionicurl = $this->start_or_reuse_ionic(); $this->ionicurl = $this->start_or_reuse_ionic();
} }
@ -274,17 +280,20 @@ class behat_app_helper extends behat_base {
* @throws DriverException If the app fails to load properly * @throws DriverException If the app fails to load properly
*/ */
protected function prepare_browser(array $options = []) { protected function prepare_browser(array $options = []) {
$restart = $options['restart'] ?? true; if ($this->evaluate_script('window.behat') && $this->runtime_js('hasInitialized()')) {
// Already initialized.
return;
}
if ($restart) { $restart = false;
if ($this->apprunning) {
$this->notify_unload();
}
// Restart the browser and set its size. if (!$this->apprunning) {
$this->getSession()->restart(); $this->check_tags();
$restart = true;
// Reset its size.
$this->resize_window($this->windowsize, true); $this->resize_window($this->windowsize, true);
if (empty($this->ionicurl)) { if (empty($this->ionicurl)) {
$this->ionicurl = $this->start_or_reuse_ionic(); $this->ionicurl = $this->start_or_reuse_ionic();
} }
@ -506,6 +515,8 @@ class behat_app_helper extends behat_base {
$successXPath = '//page-core-mainmenu'; $successXPath = '//page-core-mainmenu';
} }
$this->i_log_out_in_app(false);
$this->handle_url($url, $successXPath); $this->handle_url($url, $successXPath);
} }
@ -536,7 +547,6 @@ class behat_app_helper extends behat_base {
if ($result !== 'OK') { if ($result !== 'OK') {
throw new DriverException('Error handling url - ' . $result); throw new DriverException('Error handling url - ' . $result);
} }
if (!empty($successXPath)) { if (!empty($successXPath)) {
// Wait until the page appears. // Wait until the page appears.
$this->spin( $this->spin(
@ -550,6 +560,8 @@ class behat_app_helper extends behat_base {
} }
$this->wait_for_pending_js(); $this->wait_for_pending_js();
$this->i_wait_the_app_to_restart();
} }
/** /**

View File

@ -68,6 +68,8 @@ function do_match {
print_message "$2" print_message "$2"
tput setaf 6 tput setaf 6
grep "$match" $LANGPACKSFOLDER/en/*.php grep "$match" $LANGPACKSFOLDER/en/*.php
else
coincidence=0
fi fi
} }

View File

@ -1705,7 +1705,9 @@
"core.erroropenfilenoextension": "local_moodlemobileapp", "core.erroropenfilenoextension": "local_moodlemobileapp",
"core.erroropenpopup": "local_moodlemobileapp", "core.erroropenpopup": "local_moodlemobileapp",
"core.errorrenamefile": "local_moodlemobileapp", "core.errorrenamefile": "local_moodlemobileapp",
"core.errorsitesupport": "local_moodlemobileapp",
"core.errorsomedatanotdownloaded": "local_moodlemobileapp", "core.errorsomedatanotdownloaded": "local_moodlemobileapp",
"core.errorsomethingwrong": "local_moodlemobileapp",
"core.errorsync": "local_moodlemobileapp", "core.errorsync": "local_moodlemobileapp",
"core.errorsyncblocked": "local_moodlemobileapp", "core.errorsyncblocked": "local_moodlemobileapp",
"core.errorurlschemeinvalidscheme": "local_moodlemobileapp", "core.errorurlschemeinvalidscheme": "local_moodlemobileapp",
@ -2179,6 +2181,8 @@
"core.settings.colorscheme-system": "local_moodlemobileapp", "core.settings.colorscheme-system": "local_moodlemobileapp",
"core.settings.colorscheme-system-notice": "local_moodlemobileapp", "core.settings.colorscheme-system-notice": "local_moodlemobileapp",
"core.settings.compilationinfo": "local_moodlemobileapp", "core.settings.compilationinfo": "local_moodlemobileapp",
"core.settings.connecttosync": "local_moodlemobileapp",
"core.settings.connectwifitosync": "local_moodlemobileapp",
"core.settings.copyinfo": "local_moodlemobileapp", "core.settings.copyinfo": "local_moodlemobileapp",
"core.settings.cordovadevicemodel": "local_moodlemobileapp", "core.settings.cordovadevicemodel": "local_moodlemobileapp",
"core.settings.cordovadeviceosversion": "local_moodlemobileapp", "core.settings.cordovadeviceosversion": "local_moodlemobileapp",
@ -2200,9 +2204,7 @@
"core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp", "core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp",
"core.settings.enablerichtexteditor": "local_moodlemobileapp", "core.settings.enablerichtexteditor": "local_moodlemobileapp",
"core.settings.enablerichtexteditordescription": "local_moodlemobileapp", "core.settings.enablerichtexteditordescription": "local_moodlemobileapp",
"core.settings.enablesyncwifi": "local_moodlemobileapp",
"core.settings.entriesincache": "local_moodlemobileapp", "core.settings.entriesincache": "local_moodlemobileapp",
"core.settings.errorsyncsite": "local_moodlemobileapp",
"core.settings.estimatedfreespace": "local_moodlemobileapp", "core.settings.estimatedfreespace": "local_moodlemobileapp",
"core.settings.filesystemroot": "local_moodlemobileapp", "core.settings.filesystemroot": "local_moodlemobileapp",
"core.settings.fontsize": "local_moodlemobileapp", "core.settings.fontsize": "local_moodlemobileapp",
@ -2219,6 +2221,7 @@
"core.settings.locationhref": "local_moodlemobileapp", "core.settings.locationhref": "local_moodlemobileapp",
"core.settings.loggedin": "message", "core.settings.loggedin": "message",
"core.settings.loggedoff": "message", "core.settings.loggedoff": "message",
"core.settings.logintosync": "local_moodlemobileapp",
"core.settings.navigatorlanguage": "local_moodlemobileapp", "core.settings.navigatorlanguage": "local_moodlemobileapp",
"core.settings.navigatoruseragent": "local_moodlemobileapp", "core.settings.navigatoruseragent": "local_moodlemobileapp",
"core.settings.networkstatus": "local_moodlemobileapp", "core.settings.networkstatus": "local_moodlemobileapp",
@ -2233,7 +2236,9 @@
"core.settings.showdownloadoptions": "local_moodlemobileapp", "core.settings.showdownloadoptions": "local_moodlemobileapp",
"core.settings.siteinfo": "local_moodlemobileapp", "core.settings.siteinfo": "local_moodlemobileapp",
"core.settings.sites": "moodle", "core.settings.sites": "moodle",
"core.settings.sitesyncfailed": "local_moodlemobileapp",
"core.settings.spaceusage": "local_moodlemobileapp", "core.settings.spaceusage": "local_moodlemobileapp",
"core.settings.syncdatasaver": "local_moodlemobileapp",
"core.settings.synchronization": "local_moodlemobileapp", "core.settings.synchronization": "local_moodlemobileapp",
"core.settings.synchronizenow": "local_moodlemobileapp", "core.settings.synchronizenow": "local_moodlemobileapp",
"core.settings.synchronizenowhelp": "local_moodlemobileapp", "core.settings.synchronizenowhelp": "local_moodlemobileapp",

View File

@ -49,7 +49,6 @@ Feature: Timeline block.
@lms_from3.11 @lms_from3.11
Scenario: See courses inside block Scenario: See courses inside block
Given I entered the app as "student1" Given I entered the app as "student1"
And I press "Open block drawer" in the app
Then I should find "Assignment 00" within "Timeline" "ion-card" in the app Then I should find "Assignment 00" within "Timeline" "ion-card" in the app
And I should find "Assignment 02" within "Timeline" "ion-card" in the app And I should find "Assignment 02" within "Timeline" "ion-card" in the app
And I should find "Assignment 05" within "Timeline" "ion-card" in the app And I should find "Assignment 05" within "Timeline" "ion-card" in the app

View File

@ -163,7 +163,7 @@ Feature: Test basic usage of messages in app
And I set the field "Search" to "student1" in the app And I set the field "Search" to "student1" in the app
And I press "Search" "button" in the app And I press "Search" "button" in the app
And I press "Student1 student1" in the app And I press "Student1 student1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "New message" to "heeey student" in the app And I set the field "New message" to "heeey student" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "heeey student" in the app Then I should find "heeey student" in the app
@ -172,7 +172,7 @@ Feature: Test basic usage of messages in app
And I press "Send" in the app And I press "Send" in the app
Then I should find "byee" in the app Then I should find "byee" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Student1 student1" in the app And I press "Student1 student1" in the app
Then I should find "heeey student" in the app Then I should find "heeey student" in the app
@ -192,14 +192,14 @@ Feature: Test basic usage of messages in app
And I set the field "Search" to "student1" in the app And I set the field "Search" to "student1" in the app
And I press "Search" "button" in the app And I press "Search" "button" in the app
And I press "Student1 student1" in the app And I press "Student1 student1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "New message" to "heeey student" in the app And I set the field "New message" to "heeey student" in the app
And I press "Send" in the app And I press "Send" in the app
And I set the field "New message" to "byee" in the app And I set the field "New message" to "byee" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "byee" in the app Then I should find "byee" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I run cron tasks in the app And I run cron tasks in the app
Given I entered the app as "student1" Given I entered the app as "student1"
@ -346,12 +346,12 @@ Feature: Test basic usage of messages in app
And I press "Send" in the app And I press "Send" in the app
Then I should find "self conversation online" in the app Then I should find "self conversation online" in the app
When I switch offline mode to "true" When I switch network connection to offline
And I set the field "New message" to "self conversation offline" in the app And I set the field "New message" to "self conversation offline" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "self conversation offline" in the app Then I should find "self conversation offline" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Student1 student1" in the app And I press "Student1 student1" in the app
And I press "Display options" in the app And I press "Display options" in the app

View File

@ -79,6 +79,7 @@ Feature: Test basic usage of assignment activity in app
# Submit second attempt as a student # Submit second attempt as a student
Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app
When I pull to refresh in the app
Then I should find "Reopened" in the app Then I should find "Reopened" in the app
And I should find "2 out of Unlimited" in the app And I should find "2 out of Unlimited" in the app
And I should find "Add a new attempt based on previous submission" in the app And I should find "Add a new attempt based on previous submission" in the app
@ -97,6 +98,7 @@ Feature: Test basic usage of assignment activity in app
# View second attempt as a teacher # View second attempt as a teacher
Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app
When I press "Participants" in the app When I press "Participants" in the app
And I pull to refresh in the app
And I press "Student student" near "assignment1" in the app And I press "Student student" near "assignment1" in the app
Then I should find "Online text submissions" in the app Then I should find "Online text submissions" in the app
And I should find "Submission test 2nd attempt" in the app And I should find "Submission test 2nd attempt" in the app
@ -104,14 +106,14 @@ Feature: Test basic usage of assignment activity in app
Scenario: Add submission offline (online text) & Submit for grading offline & Sync submissions 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 assign activity "assignment1" on course "Course 1" as "student1" in the app
When I press "Add submission" in the app When I press "Add submission" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Online text submissions" to "Submission test" in the app And I set the field "Online text submissions" to "Submission test" in the app
And I press "Save" in the app And I press "Save" in the app
And I press "Submit assignment" in the app And I press "Submit assignment" in the app
And I press "OK" in the app And I press "OK" in the app
Then I should find "This Assignment has offline data to be synchronised." in the app Then I should find "This Assignment has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "assignment1" in the app And I press "assignment1" in the app
And I press "Information" in the app And I press "Information" in the app
@ -122,7 +124,7 @@ Feature: Test basic usage of assignment activity in app
Scenario: Edit an offline submission before synchronising it 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 assign activity "assignment1" on course "Course 1" as "student1" in the app
When I press "Add submission" in the app When I press "Add submission" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Online text submissions" to "Submission test original offline" in the app And I set the field "Online text submissions" to "Submission test original offline" in the app
And I press "Save" in the app And I press "Save" in the app
Then I should find "This Assignment has offline data to be synchronised." in the app Then I should find "This Assignment has offline data to be synchronised." in the app
@ -139,7 +141,7 @@ Feature: Test basic usage of assignment activity in app
And I press "OK" in the app And I press "OK" in the app
Then I should find "This Assignment has offline data to be synchronised." in the app Then I should find "This Assignment has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "assignment1" in the app And I press "assignment1" in the app
Then I should find "Submitted for grading" in the app Then I should find "Submitted for grading" in the app

View File

@ -34,6 +34,9 @@ Feature: Test basic usage of chat in app
And I press "Send" in the app And I press "Send" in the app
Then I should find "Hi!" in the app Then I should find "Hi!" in the app
And I should find "I am David" in the app And I should find "I am David" in the app
# Confirm leave the page
And I press the back button in the app
And I press "OK" in the app
# Read messages, view connected users, send beep and reply as student2 # Read messages, view connected users, send beep and reply as student2
Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app
@ -62,6 +65,9 @@ Feature: Test basic usage of chat in app
When I set the field "New message" to "I am David" in the app When I set the field "New message" to "I am David" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "I am David" in the app Then I should find "I am David" in the app
# Confirm leave the page
And I press the back button in the app
And I press "OK" in the app
# Read messages from past sessions as student2 # Read messages from past sessions as student2
Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app

View File

@ -23,6 +23,9 @@ Feature: Test chat navigation
And I set the field "New message" to "Test message" in the app And I set the field "New message" to "Test message" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "Test message" in the app Then I should find "Test message" in the app
# Confirm leave the page
And I press the back button in the app
And I press "OK" in the app
Scenario: Tablet navigation on chat Scenario: Tablet navigation on chat
Given I entered the course "Course 1" as "student2" in the app Given I entered the course "Course 1" as "student2" in the app

View File

@ -72,7 +72,7 @@ Feature: Test basic usage of choice activity in app
| choice | Test single choice name | Test single choice description | C1 | choice1 | Option 1, Option 2, Option 3 | 0 | 0 | 1 | | 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 choice activity "Test single choice name" on course "Course 1" as "student1" in the app
When I select "Option 1" in the app When I select "Option 1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I select "Option 2" in the app And I select "Option 2" in the app
And I press "Save my choice" in the app And I press "Save my choice" in the app
Then I should find "Are you sure" in the app Then I should find "Are you sure" in the app
@ -85,7 +85,7 @@ Feature: Test basic usage of choice activity in app
And I should not find "Option 2: 1" in the app And I should not find "Option 2: 1" in the app
And I should not find "Option 3: 0" in the app And I should not find "Option 3: 0" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Test single choice name" in the app And I press "Test single choice name" in the app
Then I should find "Test single choice description" in the app Then I should find "Test single choice description" in the app
@ -103,7 +103,7 @@ Feature: Test basic usage of choice activity in app
| choice | Test single choice name | Test single choice description | C1 | choice1 | Option 1, Option 2, Option 3 | 0 | 0 | 1 | | 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 choice activity "Test single choice name" on course "Course 1" as "student1" in the app
When I select "Option 1" in the app When I select "Option 1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I select "Option 2" in the app And I select "Option 2" in the app
And I press "Save my choice" in the app And I press "Save my choice" in the app
Then I should find "Are you sure" in the app Then I should find "Are you sure" in the app
@ -114,7 +114,7 @@ Feature: Test basic usage of choice activity in app
And I should not find "Option 2: 1" in the app And I should not find "Option 2: 1" in the app
And I should not find "Option 3: 0" in the app And I should not find "Option 3: 0" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I run cron tasks in the app And I run cron tasks in the app
And I wait loading to finish in the app And I wait loading to finish in the app
Then I should find "Option 1: 0" in the app Then I should find "Option 1: 0" in the app
@ -133,7 +133,7 @@ Feature: Test basic usage of choice activity in app
Then I should find "Downloaded" within "Test single choice name" "ion-item" in the app Then I should find "Downloaded" within "Test single choice name" "ion-item" in the app
And I press the back button in the app And I press the back button in the app
When I switch offline mode to "true" When I switch network connection to offline
And I press "Test multi choice name" in the app And I press "Test multi choice name" in the app
Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app
@ -152,7 +152,7 @@ Feature: Test basic usage of choice activity in app
And I should not find "Option 2: 1" in the app And I should not find "Option 2: 1" in the app
And I should not find "Option 3: 0" in the app And I should not find "Option 3: 0" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Test single choice name" in the app And I press "Test single choice name" in the app
Then I should find "Option 1: 0" in the app Then I should find "Option 1: 0" in the app

View File

@ -33,7 +33,7 @@ Feature: Users can store entries in database activities when offline and sync wh
Scenario: Create entry (offline) 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 data activity "Web links" on course "Course 1" as "student1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I should find "No entries in database" in the app And I should find "No entries in database" in the app
When I press "Add entries" in the app When I press "Add entries" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
@ -44,7 +44,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I should find "Moodle community site" 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 should find "This Database has offline data to be synchronised" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Web links" near "General" in the app And I press "Web links" near "General" in the app
And I should find "https://moodle.org/" in the app And I should find "https://moodle.org/" in the app
And I should find "Moodle community site" in the app And I should find "Moodle community site" in the app
@ -63,7 +63,8 @@ Feature: Users can store entries in database activities when offline and sync wh
And I press "Information" in the app And I press "Information" in the app
And I press "Download" in the app And I press "Download" in the app
And I wait until the page is ready And I wait until the page is ready
And I switch offline mode to "true" And I close the popup in the app
And I switch network connection to offline
When I press "Edit" in the app When I press "Edit" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| URL | https://moodlecloud.com/ | | URL | https://moodlecloud.com/ |
@ -75,7 +76,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I should find "Moodle Cloud" 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 should find "This Database has offline data to be synchronised" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Web links" near "General" in the app And I press "Web links" near "General" in the app
And I should not find "https://moodle.org/" in the app And I should not find "https://moodle.org/" in the app
And I should not find "Moodle community site" in the app And I should not find "Moodle community site" in the app
@ -85,7 +86,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I press "Information" in the app And I press "Information" in the app
And I press "Refresh" in the app And I press "Refresh" in the app
And I wait until the page is ready And I wait until the page is ready
And I switch offline mode to "true" And I switch network connection to offline
And I press "Delete" 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 should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
@ -93,7 +94,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I should find "Moodle Cloud" 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 should find "This Database has offline data to be synchronised" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Web links" near "General" in the app And I press "Web links" near "General" in the app
And I should not find "https://moodlecloud.com/" in the app And 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 "Moodle Cloud" in the app
@ -112,7 +113,8 @@ Feature: Users can store entries in database activities when offline and sync wh
And I press "Information" in the app And I press "Information" in the app
And I press "Download" in the app And I press "Download" in the app
And I wait until the page is ready And I wait until the page is ready
When I switch offline mode to "true" And I close the popup in the app
When I switch network connection to offline
And I press "Delete" 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 should find "Are you sure you want to delete this entry?" in the app
And I press "Delete" in the app And I press "Delete" in the app
@ -121,7 +123,7 @@ Feature: Users can store entries in database activities when offline and sync wh
And I should find "This Database has offline data to be synchronised" in the app And I should find "This Database has offline data to be synchronised" in the app
And I press "Restore" in the app And I press "Restore" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Web links" near "General" in the app And I press "Web links" near "General" in the app
Then 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 find "Moodle community site" in the app

View File

@ -96,7 +96,7 @@ Feature: Test basic usage of forum activity in app
Then I should find "Reply" in the app Then I should find "Reply" in the app
When I press the back button in the app When I press the back button in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Initial discussion" in the app And I press "Initial discussion" in the app
And I press "Reply" in the app And I press "Reply" in the app
And I set the field "Message" to "not sent reply" in the app And I set the field "Message" to "not sent reply" in the app
@ -110,7 +110,7 @@ Feature: Test basic usage of forum activity in app
Then I should find "Not sent" in the app Then I should find "Not sent" in the app
And I should find "This Discussion has offline data to be synchronised" in the app And I should find "This Discussion has offline data to be synchronised" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Initial discussion" in the app And I press "Initial discussion" in the app
Then I should not find "Not sent" in the app Then I should not find "Not sent" in the app
@ -118,7 +118,7 @@ Feature: Test basic usage of forum activity in app
Scenario: Edit a not sent new discussion offline Scenario: Edit a not sent new discussion offline
Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app
When I switch offline mode to "true" When I switch network connection to offline
And I press "Add discussion topic" in the app And I press "Add discussion topic" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Subject | Auto-test | | Subject | Auto-test |
@ -129,7 +129,7 @@ Feature: Test basic usage of forum activity in app
And I press "Post to forum" in the app And I press "Post to forum" in the app
Then I should find "This Forum has offline data to be synchronised." in the app Then I should find "This Forum has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press "Auto-test" in the app And I press "Auto-test" in the app
Then I should find "Post to forum" in the app Then I should find "Post to forum" in the app
@ -151,7 +151,7 @@ Feature: Test basic usage of forum activity in app
Then I should find "Edit" in the app Then I should find "Edit" in the app
When I press "Edit" in the app When I press "Edit" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Message" to "Auto-test message edited" in the app And I set the field "Message" to "Auto-test message edited" in the app
And I press "Save changes" in the app And I press "Save changes" in the app
Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app
@ -163,7 +163,7 @@ Feature: Test basic usage of forum activity in app
And I press "Edit" in the app And I press "Edit" in the app
Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press "OK" in the app And I press "OK" in the app
And I press "Edit" in the app And I press "Edit" in the app
And I set the field "Message" to "Auto-test message edited" in the app And I set the field "Message" to "Auto-test message edited" in the app
@ -183,7 +183,7 @@ Feature: Test basic usage of forum activity in app
When I press "Delete" in the app When I press "Delete" in the app
And I press "Cancel" in the app And I press "Cancel" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Display options" near "Reply" in the app And I press "Display options" near "Reply" in the app
Then I should find "Delete" in the app Then I should find "Delete" in the app
@ -192,7 +192,7 @@ Feature: Test basic usage of forum activity in app
When I press "OK" in the app When I press "OK" in the app
And I close the popup in the app And I close the popup in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" near "Reply" in the app And I press "Display options" near "Reply" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I press "Delete" in the app And I press "Delete" in the app
@ -215,14 +215,14 @@ Feature: Test basic usage of forum activity in app
When I press "Auto-test" in the app When I press "Auto-test" in the app
And I press "None" near "Auto-test message" in the app And I press "None" near "Auto-test message" in the app
And I press "1" near "Cancel" in the app And I press "1" near "Cancel" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "None" near "test2" in the app And I press "None" near "test2" in the app
And I press "0" near "Cancel" in the app And I press "0" near "Cancel" in the app
Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app
And I should find "Average of ratings: -" in the app And I should find "Average of ratings: -" in the app
And I should find "Average of ratings: 1" in the app And I should find "Average of ratings: 1" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
Then I should find "This Forum has offline data to be synchronised." in the app Then I should find "This Forum has offline data to be synchronised." in the app
@ -244,7 +244,7 @@ Feature: Test basic usage of forum activity in app
Scenario: Reply a post offline Scenario: Reply a post offline
Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app
When I press "Initial discussion" in the app When I press "Initial discussion" in the app
And I switch offline mode to "true" And I switch network connection to offline
Then I should find "Reply" in the app Then I should find "Reply" in the app
When I press "Reply" in the app When I press "Reply" in the app
@ -255,7 +255,7 @@ Feature: Test basic usage of forum activity in app
And I should find "Not sent" in the app And I should find "Not sent" in the app
When I press the back button in the app When I press the back button in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Initial discussion" in the app And I press "Initial discussion" in the app
Then I should find "Initial discussion message" in the app Then I should find "Initial discussion message" in the app
And I should find "ReplyMessage" in the app And I should find "ReplyMessage" in the app
@ -263,7 +263,7 @@ Feature: Test basic usage of forum activity in app
Scenario: New discussion offline & Sync Forum 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 forum activity "Test forum name" on course "Course 1" as "student1" in the app
When I switch offline mode to "true" When I switch network connection to offline
And I press "Add discussion topic" in the app And I press "Add discussion topic" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Subject | DiscussionSubject | | Subject | DiscussionSubject |
@ -273,7 +273,7 @@ Feature: Test basic usage of forum activity in app
And I should find "Not sent" in the app And I should find "Not sent" in the app
And I should find "This Forum has offline data to be synchronised." in the app And I should find "This Forum has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Test forum name" in the app And I press "Test forum name" in the app
And I press "Information" in the app And I press "Information" in the app
@ -286,7 +286,7 @@ Feature: Test basic usage of forum activity in app
Scenario: New discussion offline & Auto-sync forum Scenario: New discussion offline & Auto-sync forum
Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app
When I switch offline mode to "true" When I switch network connection to offline
And I press "Add discussion topic" in the app And I press "Add discussion topic" in the app
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Subject | DiscussionSubject | | Subject | DiscussionSubject |
@ -296,7 +296,7 @@ Feature: Test basic usage of forum activity in app
And I should find "Not sent" in the app And I should find "Not sent" in the app
And I should find "This Forum has offline data to be synchronised." in the app And I should find "This Forum has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I run cron tasks in the app And I run cron tasks in the app
And I wait loading to finish in the app And I wait loading to finish in the app
Then I should not find "Not sent" in the app Then I should not find "Not sent" in the app
@ -314,7 +314,7 @@ Feature: Test basic usage of forum activity in app
Then I should find "Downloaded" within "Test forum name" "ion-item" in the app Then I should find "Downloaded" within "Test forum name" "ion-item" in the app
When I press the back button in the app When I press the back button in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Test forum name" in the app And I press "Test forum name" in the app
Then I should find "Initial discussion" in the app Then I should find "Initial discussion" in the app

View File

@ -103,7 +103,7 @@ Feature: Test forum navigation
# Offline # Offline
When I press the back button in the app When I press the back button in the app
And I press "Add discussion topic" in the app And I press "Add discussion topic" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Subject | Offline discussion 1 | | Subject | Offline discussion 1 |
| Message | Offline discussion 1 message | | Message | Offline discussion 1 message |
@ -199,7 +199,7 @@ Feature: Test forum navigation
# Offline # Offline
When I press "Add discussion topic" in the app When I press "Add discussion topic" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Subject | Offline discussion 1 | | Subject | Offline discussion 1 |
| Message | Offline discussion 1 message | | Message | Offline discussion 1 message |

View File

@ -100,6 +100,7 @@ Feature: Test basic usage of glossary in app
# View comments as a student # View comments as a student
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
And I press "Eggplant" in the app And I press "Eggplant" in the app
When I pull to refresh in the app
Then I should find "Comments (2)" in the app Then I should find "Comments (2)" in the app
When I press "Comments" in the app When I press "Comments" in the app
@ -111,7 +112,7 @@ Feature: Test basic usage of glossary in app
When I press "Course downloads" in the app When I press "Course downloads" in the app
When I press "Download" within "Test glossary" "ion-item" in the app When I press "Download" within "Test glossary" "ion-item" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Test glossary" in the app And I press "Test glossary" in the app
Then the header should be "Test glossary" in the app Then the header should be "Test glossary" in the app
And I should find "Cucumber" in the app And I should find "Cucumber" in the app
@ -156,7 +157,7 @@ Feature: Test basic usage of glossary in app
Scenario: Sync Scenario: Sync
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
And I press "Add a new entry" in the app And I press "Add a new entry" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Concept | Broccoli | | Concept | Broccoli |
| Definition | Brassica oleracea var. italica | | Definition | Brassica oleracea var. italica |
@ -181,7 +182,7 @@ Feature: Test basic usage of glossary in app
And I should find "Entries to be synced" in the app And I should find "Entries to be synced" in the app
And I should find "This Glossary has offline data to be synchronised." in the app And I should find "This Glossary has offline data to be synchronised." in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press "Information" in the app And I press "Information" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
Then the header should be "Test glossary" in the app Then the header should be "Test glossary" in the app
@ -211,13 +212,13 @@ Feature: Test basic usage of glossary in app
# Rate entries as teacher2 # Rate entries as teacher2
Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher2" in the app Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher2" in the app
And I press "Cucumber" in the app And I press "Cucumber" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "None" in the app And I press "None" in the app
And I press "0" in the app And I press "0" in the app
Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app
And I should find "Average of ratings: 1" in the app And I should find "Average of ratings: 1" in the app
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
Then I should find "This Glossary has offline data to be synchronised." in the app Then I should find "This Glossary has offline data to be synchronised." in the app

View File

@ -173,7 +173,7 @@ Feature: Test glossary navigation
When I press the back button in the app When I press the back button in the app
And I press "Clear search" in the app And I press "Clear search" in the app
And I press "Add a new entry" in the app And I press "Add a new entry" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Concept | Tomato | | Concept | Tomato |
| Definition | Tomato is a fruit | | Definition | Tomato is a fruit |
@ -274,7 +274,7 @@ Feature: Test glossary navigation
# Offline # Offline
When I press "Clear search" in the app When I press "Clear search" in the app
And I press "Add a new entry" in the app And I press "Add a new entry" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the following fields to these values in the app: And I set the following fields to these values in the app:
| Concept | Tomato | | Concept | Tomato |
| Definition | Tomato is a fruit | | Definition | Tomato is a fruit |

View File

@ -233,12 +233,12 @@ Feature: Test basic usage of survey activity in app
| activity | name | intro | template | course | idnumber | groupmode | | activity | name | intro | template | course | idnumber | groupmode |
| survey | Test survey critical incidents | Test survey1 | 5 | C1 | survey1 | 0 | | 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 Given I entered the survey activity "Test survey critical incidents" on course "Course 1" as "student1" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Submit" in the app And I press "Submit" in the app
And I press "OK" in the app And I press "OK" in the app
Then I should see "This Survey has offline data to be synchronised." Then I should see "This Survey has offline data to be synchronised."
When I switch offline mode to "false" When I switch network connection to wifi
And I press the back button in the app And I press the back button in the app
And I press "Test survey critical incidents" in the app And I press "Test survey critical incidents" in the app
And I press "Information" in the app And I press "Information" in the app
@ -255,7 +255,7 @@ Feature: Test basic usage of survey activity in app
And I press "Course downloads" in the app And I press "Course downloads" in the app
And I press "Download" within "Test survey critical incidents" "ion-item" in the app And I press "Download" within "Test survey critical incidents" "ion-item" in the app
And I press the back button in the app And I press the back button in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Test survey name" in the app And I press "Test survey name" in the app
Then I should see "There was a problem connecting to the site. Please check your connection and try again." Then I should see "There was a problem connecting to the site. Please check your connection and try again."
@ -266,7 +266,7 @@ Feature: Test basic usage of survey activity in app
And I press "OK" in the app And I press "OK" in the app
Then I should see "This Survey has offline data to be synchronised." Then I should see "This Survey has offline data to be synchronised."
When I switch offline mode to "false" When I switch network connection to wifi
And I run cron tasks in the app And I run cron tasks in the app
Then I should not see "This Survey has offline data to be synchronised." Then I should not see "This Survey has offline data to be synchronised."
And I should see "You have completed this survey." And I should see "You have completed this survey."

View File

@ -23,7 +23,7 @@ import { CoreAjaxWSError } from './ajaxwserror';
import { CoreCaptureError } from './captureerror'; import { CoreCaptureError } from './captureerror';
import { CoreNetworkError } from './network-error'; import { CoreNetworkError } from './network-error';
import { CoreSiteError } from './siteerror'; import { CoreSiteError } from './siteerror';
import { CoreErrorWithTitle } from './errorwithtitle'; import { CoreErrorWithOptions } from './errorwithtitle';
import { CoreHttpError } from './httperror'; import { CoreHttpError } from './httperror';
export const CORE_ERRORS_CLASSES: Type<unknown>[] = [ export const CORE_ERRORS_CLASSES: Type<unknown>[] = [
@ -36,6 +36,6 @@ export const CORE_ERRORS_CLASSES: Type<unknown>[] = [
CoreSilentError, CoreSilentError,
CoreSiteError, CoreSiteError,
CoreWSError, CoreWSError,
CoreErrorWithTitle, CoreErrorWithOptions,
CoreHttpError, CoreHttpError,
]; ];

View File

@ -12,20 +12,24 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { AlertButton } from '@ionic/angular';
import { CoreError } from './error'; import { CoreError } from './error';
/** /**
* Error with an explicit title describing the problem (instead of just "Error" or a generic message). * Error with an explicit title describing the problem (instead of just "Error" or a generic message).
* This title should be used to communicate the problem with users, and if it's undefined it should be omitted. * This title should be used to communicate the problem with users, and if it's undefined it should be omitted.
* The error also may contain customizable action buttons.
*/ */
export class CoreErrorWithTitle extends CoreError { export class CoreErrorWithOptions extends CoreError {
title?: string; title?: string;
buttons?: AlertButton[];
constructor(message?: string, title?: string) { constructor(message?: string, title?: string, buttons?: AlertButton[]) {
super(message); super(message);
this.title = title; this.title = title;
this.buttons = buttons;
} }
} }

View File

@ -8,9 +8,10 @@
</ion-button> </ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description"> <ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs"> <ng-container *ngFor="let tab of tabs">
<ion-slide role="presentation" [id]="tab.id! + '-tab'" tabindex="-1" [class.selected]="selected == tab.id"> <ion-slide role="presentation" [id]="tab.id! + '-tab'" tabindex="-1" [class.selected]="selected == tab.id"
class="{{tab.class}}">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)" <ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" class="{{tab.class}}" role="tab" (keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id" [attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1"> [tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon> <ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>

View File

@ -7,10 +7,11 @@
</ion-button> </ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description"> <ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs"> <ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.enabled" role="presentation" [id]="tab.id! + '-tab'" [class.selected]="selected == tab.id"> <ion-slide *ngIf="tab.enabled" role="presentation" [id]="tab.id! + '-tab'" [class.selected]="selected == tab.id"
class="{{tab.class}}">
<ion-tab-button (click)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)" <ion-tab-button (click)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown($event)"
(keyup)="tabAction.keyUp(tab.id, $event)" class="{{tab.class}}" [layout]="layout" role="tab" (keyup)="tabAction.keyUp(tab.id, $event)" [layout]="layout" role="tab" [attr.aria-controls]="tab.id"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id" [tabindex]="selected == tab.id ? 0 : -1"> [attr.aria-selected]="selected == tab.id" [tabindex]="selected == tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon> <ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
{{ tab.title | translate}} {{ tab.title | translate}}

View File

@ -36,8 +36,9 @@ Feature: Test basic usage of comments in app
| Field description | Test field description | | Field description | Test field description |
And I press "Save" And I press "Save"
And I close the browser tab opened by the app And I close the browser tab opened by the app
When I entered the course "Course 1" as "teacher1" in the app And I close the popup in the app
And I press "Data" in the app
When I pull to refresh in the app
And I press "Add entries" in the app And I press "Add entries" in the app
And I set the field "Test field name" to "Test" in the app And I set the field "Test field name" to "Test" in the app
And I press "Save" in the app And I press "Save" in the app
@ -75,7 +76,7 @@ Feature: Test basic usage of comments in app
Scenario: Add comments offline & Delete comments offline & Sync comments (database) Scenario: Add comments offline & Delete comments offline & Sync comments (database)
Given I entered the data activity "Data" on course "Course 1" as "teacher1" in the app Given I entered the data activity "Data" on course "Course 1" as "teacher1" in the app
When I press "Information" in the app And I press "Information" in the app
And I press "Open in browser" in the app And I press "Open in browser" in the app
And I switch to the browser tab opened by the app And I switch to the browser tab opened by the app
And I log in as "teacher1" And I log in as "teacher1"
@ -84,14 +85,15 @@ Feature: Test basic usage of comments in app
| Field description | Test field description | | Field description | Test field description |
And I press "Save" And I press "Save"
And I close the browser tab opened by the app And I close the browser tab opened by the app
And I close the popup in the app
Given I entered the data activity "Data" on course "Course 1" as "teacher1" in the app When I pull to refresh in the app
Then I press "Add entries" in the app And I press "Add entries" in the app
And I set the field "Test field name" to "Test" in the app And I set the field "Test field name" to "Test" in the app
And I press "Save" in the app And I press "Save" in the app
And I press "More" in the app And I press "More" in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Add a comment..." to "comment test" in the app And I set the field "Add a comment..." to "comment test" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app
@ -100,7 +102,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app
@ -109,7 +111,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Toggle delete buttons" in the app And I press "Toggle delete buttons" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I press "Delete" near "Cancel" in the app And I press "Delete" near "Cancel" in the app
@ -120,7 +122,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app
@ -179,7 +181,7 @@ Feature: Test basic usage of comments in app
And I press "Save" in the app And I press "Save" in the app
And I press "potato" in the app And I press "potato" in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Add a comment..." to "comment test" in the app And I set the field "Add a comment..." to "comment test" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app
@ -188,7 +190,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app
@ -197,7 +199,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Toggle delete buttons" in the app And I press "Toggle delete buttons" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I press "Delete" near "Cancel" in the app And I press "Delete" near "Cancel" in the app
@ -208,7 +210,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app
@ -262,7 +264,7 @@ Feature: Test basic usage of comments in app
And I should find "Blog body" in the app And I should find "Blog body" in the app
When I press "Comments (0)" in the app When I press "Comments (0)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I set the field "Add a comment..." to "comment test" in the app And I set the field "Add a comment..." to "comment test" in the app
And I press "Send" in the app And I press "Send" in the app
Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app
@ -271,7 +273,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (0)" in the app And I press "Comments (0)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app
@ -280,7 +282,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "true" And I switch network connection to offline
And I press "Toggle delete buttons" in the app And I press "Toggle delete buttons" in the app
And I press "Delete" in the app And I press "Delete" in the app
And I press "Delete" near "Cancel" in the app And I press "Delete" near "Cancel" in the app
@ -291,7 +293,7 @@ Feature: Test basic usage of comments in app
When I press the back button in the app When I press the back button in the app
And I press "Comments (1)" in the app And I press "Comments (1)" in the app
And I switch offline mode to "false" And I switch network connection to wifi
And I press "Display options" in the app And I press "Display options" in the app
And I press "Synchronise now" in the app And I press "Synchronise now" in the app
And I close the popup in the app And I close the popup in the app

View File

@ -177,7 +177,9 @@ export class CoreCourseProvider {
CorePlatform.resume.subscribe(() => { CorePlatform.resume.subscribe(() => {
// Run the handler the app is open to keep user in online status. // Run the handler the app is open to keep user in online status.
setTimeout(() => { setTimeout(() => {
CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name); CoreUtils.ignoreErrors(
CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name),
);
}, 1000); }, 1000);
}); });

View File

@ -411,6 +411,8 @@ Feature: Test basic usage of one course in app
And I select "Enrolment methods" from the "jump" singleselect And I select "Enrolment methods" from the "jump" singleselect
And I click on "Enable" "icon" in the "Self enrolment (Student)" "table_row" And I click on "Enable" "icon" in the "Self enrolment (Student)" "table_row"
And I close the browser tab opened by the app And I close the browser tab opened by the app
And I close the popup in the app
Given I entered the app as "student2" Given I entered the app as "student2"
When I press "Site home" in the app When I press "Site home" in the app
And I press "Available courses" in the app And I press "Available courses" in the app

View File

@ -148,9 +148,12 @@ Feature: Test basic usage of courses in app
# Grade assignment as teacher # Grade assignment as teacher
Given I entered the app as "teacher1" Given I entered the app as "teacher1"
When I press "Grade" in the app When I pull to refresh in the app
And I press "Grade" in the app
Then the header should be "assignment" in the app Then the header should be "assignment" in the app
And I should find "Test assignment description" in the app
When I pull to refresh in the app
Then I should find "Test assignment description" in the app
And I should find "Time remaining" in the app And I should find "Time remaining" in the app
When I press "Needs grading" in the app When I press "Needs grading" in the app

View File

@ -26,8 +26,7 @@ Feature: Test basic usage of login in app
And I should find "Connect to Moodle" in the app And I should find "Connect to Moodle" in the app
Scenario: Add a new account in the app & Site name in displayed when adding a new account Scenario: Add a new account in the app & Site name in displayed when adding a new account
When I enter the app When I launch the app
And I press the back button in the app
And I set the field "Your site" to "$WWWROOT" in the app And I set the field "Your site" to "$WWWROOT" in the app
And I press "Connect to your site" in the app And I press "Connect to your site" in the app
Then I should find "Acceptance test site" in the app Then I should find "Acceptance test site" in the app
@ -42,9 +41,7 @@ Feature: Test basic usage of login in app
Scenario: Add a non existing account Scenario: Add a non existing account
When I enter the app When I enter the app
And I log in as "student1" And I log in as "student1"
And I press the user menu button in the app When I log out in the app
And I press "Log out" in the app
And I wait the app to restart
And I press "Add" in the app And I press "Add" in the app
And I set the field "Your site" to "Wrong Site Address" in the app And I set the field "Your site" to "Wrong Site Address" in the app
And I press enter in the app And I press enter in the app
@ -63,11 +60,16 @@ Feature: Test basic usage of login in app
Then I should find "Cannot connect" in the app Then I should find "Cannot connect" in the app
And I should find "Wrong Site Address" in the app And I should find "Wrong Site Address" 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 Scenario: Delete an account
Given I entered the app as "student1" Given I entered the app as "student1"
When I press the user menu button in the app When I log out in the app
And I press "Log out" in the app
And I wait the app to restart
Then I should find "Acceptance test site" in the app Then I should find "Acceptance test site" in the app
And I press "Edit accounts list" in the app And I press "Edit accounts list" in the app
And I press "Remove account" near "Acceptance test site" in the app And I press "Remove account" near "Acceptance test site" in the app
@ -75,14 +77,14 @@ Feature: Test basic usage of login in app
Then I should find "Connect to Moodle" in the app Then I should find "Connect to Moodle" in the app
But I should not find "Acceptance test site" in the app But I should not find "Acceptance test site" in the app
Scenario: Require minium version of the app for a site Scenario: Require minium (previous) version of the app for a site
# Log in with a previous required version # Log in with a previous required version
Given the following config values are set as admin: Given the following config values are set as admin:
| minimumversion | 3.8.1 | tool_mobile | | minimumversion | 3.8.1 | tool_mobile |
When I enter the app When I enter the app
Then I should not find "App update required" in the app Then I should not find "App update required" in the app
Scenario: Require minium (future) version of the app for a site
# Log in with a future required version # Log in with a future required version
Given the following config values are set as admin: Given the following config values are set as admin:
| minimumversion | 11.0.0 | tool_mobile | | minimumversion | 11.0.0 | tool_mobile |

View File

@ -71,7 +71,14 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
// Load the handlers. // Load the handlers.
if (this.siteInfo) { if (this.siteInfo) {
this.user = await CoreUser.getProfile(this.siteInfo.userid); try {
this.user = await CoreUser.getProfile(this.siteInfo.userid);
} catch {
this.user = {
id: this.siteInfo.userid,
fullname: this.siteInfo.fullname,
};
}
this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user, CoreUserDelegateContext.USER_MENU) this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user, CoreUserDelegateContext.USER_MENU)
.subscribe((handlers) => { .subscribe((handlers) => {

View File

@ -746,11 +746,7 @@ export class CorePushNotificationsProvider {
CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId()); CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId());
// Insert the device in the local DB. // Insert the device in the local DB.
try { await CoreUtils.ignoreErrors(this.registeredDevicesTables[site.getId()].insert(data));
await this.registeredDevicesTables[site.getId()].insert(data);
} catch (err) {
// Ignore errors.
}
} }
} finally { } finally {
// Remove pending unregisters for this site. // Remove pending unregisters for this site.

View File

@ -4,8 +4,8 @@
"appsettings": "App settings", "appsettings": "App settings",
"appversion": "App version", "appversion": "App version",
"cannotsyncloggedout": "This site cannot be synchronised because you've logged out. Please try again when you're logged in the site again.", "cannotsyncloggedout": "This site cannot be synchronised because you've logged out. Please try again when you're logged in the site again.",
"cannotsyncoffline": "Cannot synchronise offline.", "cannotsyncoffline": "Site synchronisation failed because your device is not connected to the internet.",
"cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", "cannotsyncwithoutwifi": "Your device is not connected to Wi-Fi. Connect to a Wi-Fi network or turn off Data Saver in the app settings.",
"changelanguage": "Change to {{$a}}", "changelanguage": "Change to {{$a}}",
"changelanguagealert": "Changing the language will restart the app.", "changelanguagealert": "Changing the language will restart the app.",
"colorscheme-dark": "Dark", "colorscheme-dark": "Dark",
@ -14,6 +14,8 @@
"colorscheme-system": "System default", "colorscheme-system": "System default",
"colorscheme": "Color Scheme", "colorscheme": "Color Scheme",
"compilationinfo": "Compilation info", "compilationinfo": "Compilation info",
"connectwifitosync": "Connect to a Wi-Fi network or turn off Data saver to synchronise sites.",
"connecttosync": "Your device is offline. Connect to the internet to synchronise sites.",
"copyinfo": "Copy device info on the clipboard", "copyinfo": "Copy device info on the clipboard",
"cordovadevicemodel": "Cordova device model", "cordovadevicemodel": "Cordova device model",
"cordovadeviceosversion": "Cordova device OS version", "cordovadeviceosversion": "Cordova device OS version",
@ -35,9 +37,7 @@
"enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.", "enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.",
"enablerichtexteditor": "Enable text editor", "enablerichtexteditor": "Enable text editor",
"enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.", "enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.",
"enablesyncwifi": "Allow sync only when on Wi-Fi",
"entriesincache": "{{$a}} entries in cache", "entriesincache": "{{$a}} entries in cache",
"errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.",
"estimatedfreespace": "Estimated free space", "estimatedfreespace": "Estimated free space",
"filesystemroot": "File system root", "filesystemroot": "File system root",
"fontsize": "Text size", "fontsize": "Text size",
@ -54,6 +54,7 @@
"locationhref": "Web view URL", "locationhref": "Web view URL",
"loggedin": "Online", "loggedin": "Online",
"loggedoff": "Offline", "loggedoff": "Offline",
"logintosync": "Log in to synchronise",
"navigatorlanguage": "Navigator language", "navigatorlanguage": "Navigator language",
"navigatoruseragent": "Navigator userAgent", "navigatoruseragent": "Navigator userAgent",
"networkstatus": "Internet connection status", "networkstatus": "Internet connection status",
@ -68,7 +69,9 @@
"showdownloadoptions": "Show download options", "showdownloadoptions": "Show download options",
"siteinfo": "Site info", "siteinfo": "Site info",
"sites": "Sites", "sites": "Sites",
"sitesyncfailed": "Site synchronisation failed",
"spaceusage": "Space usage", "spaceusage": "Space usage",
"syncdatasaver": "Data saver: Synchronise only when on Wi-Fi",
"synchronization": "Synchronisation", "synchronization": "Synchronisation",
"synchronizenow": "Synchronise now", "synchronizenow": "Synchronise now",
"synchronizenowhelp": "Synchronising a site will send pending changes and all offline activity stored in the device and will synchronise some data like messages and notifications.", "synchronizenowhelp": "Synchronising a site will send pending changes and all offline activity stored in the device and will synchronise some data like messages and notifications.",

View File

@ -37,11 +37,20 @@
</p> </p>
</ion-label> </ion-label>
<core-button-with-spinner [loading]="isSynchronizing()" slot="end"> <core-button-with-spinner [loading]="isSynchronizing()" slot="end">
<ion-button fill="clear" (click)="synchronize()" [attr.aria-label]="'core.settings.synchronizenow' | translate"> <ion-button fill="clear" (click)="synchronize()" [attr.aria-label]="'core.settings.synchronizenow' | translate"
[disabled]="!isOnline || (dataSaver && limitedConnection)">
<ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
</core-button-with-spinner> </core-button-with-spinner>
</ion-item> </ion-item>
<ion-item class="core-warning-item ion-text-wrap" *ngIf="!isOnline || (dataSaver && limitedConnection)">
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<ng-container *ngIf="isOnline && dataSaver && limitedConnection">
{{ 'core.settings.connectwifitosync' | translate }}</ng-container>
<ng-container *ngIf="!isOnline">{{ 'core.settings.connecttosync' | translate }}</ng-container>
</ion-label>
</ion-item>
</ion-card> </ion-card>
</core-loading> </core-loading>
</core-split-view> </core-split-view>

View File

@ -25,6 +25,11 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/
import { CoreSettingsHandlersSource } from '@features/settings/classes/settings-handlers-source'; import { CoreSettingsHandlersSource } from '@features/settings/classes/settings-handlers-source';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreNetwork } from '@services/network';
import { Subscription } from 'rxjs';
import { NgZone } from '@singletons';
import { CoreConstants } from '@/core/constants';
import { CoreConfig } from '@services/config';
/** /**
* Page that displays the list of site settings pages. * Page that displays the list of site settings pages.
@ -39,8 +44,13 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
handlers: CoreListItemsManager<CoreSettingsHandlerToDisplay>; handlers: CoreListItemsManager<CoreSettingsHandlerToDisplay>;
dataSaver = false;
limitedConnection = false;
isOnline = true;
protected siteId: string; protected siteId: string;
protected sitesObserver: CoreEventObserver; protected sitesObserver: CoreEventObserver;
protected networkObserver: Subscription;
protected isDestroyed = false; protected isDestroyed = false;
constructor() { constructor() {
@ -53,12 +63,25 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.refreshData(); this.refreshData();
}, this.siteId); }, this.siteId);
this.isOnline = CoreNetwork.isOnline();
this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited();
this.networkObserver = CoreNetwork.onChange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
NgZone.run(() => {
this.isOnline = CoreNetwork.isOnline();
this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited();
});
});
} }
/** /**
* View loaded. * @inheritdoc
*/ */
async ngAfterViewInit(): Promise<void> { async ngAfterViewInit(): Promise<void> {
this.dataSaver = await CoreConfig.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, true);
const pageToOpen = CoreNavigator.getRouteParam('page'); const pageToOpen = CoreNavigator.getRouteParam('page');
try { try {
@ -94,7 +117,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
if (this.isDestroyed) { if (this.isDestroyed) {
return; return;
} }
CoreDomUtils.showErrorModalDefault(error, 'core.settings.errorsyncsite', true); CoreDomUtils.showErrorModalDefault(error, 'core.settings.sitesyncfailed', true);
} }
} }
@ -121,11 +144,12 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
} }
/** /**
* Page destroyed. * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.isDestroyed = true; this.isDestroyed = true;
this.sitesObserver?.off(); this.sitesObserver.off();
this.networkObserver.unsubscribe();
this.handlers.destroy(); this.handlers.destroy();
} }

View File

@ -17,37 +17,97 @@
</ion-header> </ion-header>
<ion-content class="limited-width"> <ion-content class="limited-width">
<core-loading [hideUntil]="sitesLoaded"> <core-loading [hideUntil]="sitesLoaded">
<ion-list> <ion-list class="core-sitelist limited-width">
<ion-item-divider> <ion-item-divider>
<ion-label> <ion-label>
<h2>{{ 'core.settings.syncsettings' | translate }}</h2> <h2>{{ 'core.settings.syncsettings' | translate }}</h2>
</ion-label> </ion-label>
</ion-item-divider> </ion-item-divider>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label>{{ 'core.settings.enablesyncwifi' | translate }}</ion-label>
<ion-toggle slot="end" [(ngModel)]="syncOnlyOnWifi" (ngModelChange)="syncOnlyOnWifiChanged()">
</ion-toggle>
</ion-item>
<ion-item-divider>
<ion-label>
<h2>{{ 'core.settings.sites' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let site of sites" [class.item-current]="site.id == currentSiteId" class="ion-text-wrap">
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text> {{ 'core.settings.syncdatasaver' | translate }}
</p> </p>
<p>{{ site.fullName }}</p>
<p>{{ site.siteUrlWithoutProtocol }}</p>
</ion-label> </ion-label>
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end"> <ion-toggle slot="end" [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()">
<ion-button fill="clear" (click)="synchronize(site.id)" [title]="site.siteName" </ion-toggle>
[attr.aria-label]="'core.settings.synchronizenow' | translate">
<ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-button-with-spinner>
</ion-item> </ion-item>
<ion-card class="core-warning-card" *ngIf="!isOnline || (dataSaver && limitedConnection)">
<ion-item class="ion-text-wrap">
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<ng-container *ngIf="isOnline && dataSaver && limitedConnection">
{{ 'core.settings.connectwifitosync' | translate }}</ng-container>
<ng-container *ngIf="!isOnline">{{ 'core.settings.connecttosync' | translate }}</ng-container>
</ion-label>
</ion-item>
</ion-card>
<ng-container *ngIf="isOnline && (!dataSaver || !limitedConnection)">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.accounts' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-card *ngIf="accountsList.currentSite">
<ion-item-divider sticky="true" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="accountsList.currentSite.siteName" clean="true"
[siteId]="accountsList.currentSite.id"></core-format-text>
</p>
<p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{
accountsList.currentSite.siteUrlWithoutProtocol }}</a>
</p>
</ion-label>
</ion-item-divider>
<ion-item class="item-current">
<ng-container *ngTemplateOutlet="siteSync; context: {site: accountsList.currentSite}"></ng-container>
</ion-item>
<ion-item *ngFor="let site of accountsList.sameSite">
<ng-container *ngTemplateOutlet="siteSync; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
<ion-card *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
</p>
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let site of sites">
<ng-container *ngTemplateOutlet="siteSync; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
</ng-container>
</ion-list> </ion-list>
</core-loading> </core-loading>
</ion-content> </ion-content>
<!-- Template to render a site to sync. -->
<ng-template #siteSync let-site="site">
<ion-avatar slot="start">
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label>
<p class="item-heading">{{site.fullName}}</p>
<p class="text-danger" *ngIf="site.loggedOut">{{ 'core.settings.logintosync' | translate }}</p>
</ion-label>
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end" *ngIf="!site.loggedOut">
<ion-button fill="clear" (click)="synchronize(site.id)" [attr.aria-label]="'core.settings.synchronizenow' | translate">
<ion-icon name="fas-sync-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-button-with-spinner>
<ion-button fill="clear" (click)="login(site.id)" [attr.aria-label]="'core.login.login' | translate" *ngIf="site.loggedOut" slot="end">
<ion-icon name="fas-sign-in-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ng-template>

View File

@ -16,11 +16,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites, CoreSiteBasicInfo } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { Translate } from '@singletons'; import { NgZone, Translate } from '@singletons';
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNetwork } from '@services/network';
import { Subscription } from 'rxjs';
import { CoreNavigator } from '@services/navigator';
/** /**
* Page that displays the synchronization settings. * Page that displays the synchronization settings.
@ -28,61 +32,101 @@ import { Translate } from '@singletons';
@Component({ @Component({
selector: 'page-core-app-settings-synchronization', selector: 'page-core-app-settings-synchronization',
templateUrl: 'synchronization.html', templateUrl: 'synchronization.html',
styleUrls: ['../../../login/sitelist.scss'],
}) })
export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy { export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
sites: CoreSiteBasicInfo[] = []; accountsList: CoreAccountsList = {
sameSite: [],
otherSites: [],
count: 0,
};
sitesLoaded = false; sitesLoaded = false;
currentSiteId = ''; dataSaver = false;
syncOnlyOnWifi = false; limitedConnection = false;
isOnline = true;
protected isDestroyed = false; protected isDestroyed = false;
protected sitesObserver: CoreEventObserver; protected sitesObserver: CoreEventObserver;
protected networkObserver: Subscription;
constructor() { constructor() {
this.currentSiteId = CoreSites.getCurrentSiteId();
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async (data) => { this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, async (data) => {
const site = await CoreSites.getSite(data.siteId); const siteId = data.siteId;
const siteEntry = this.sites.find((siteEntry) => siteEntry.id == site.id); let siteEntry = siteId === this.accountsList.currentSite?.id
if (siteEntry) { ? this.accountsList.currentSite
const siteInfo = site.getInfo(); : undefined;
siteEntry.siteName = site.getSiteName(); if (!siteEntry) {
siteEntry = this.accountsList.sameSite.find((siteEntry) => siteEntry.id === siteId);
}
if (siteInfo) { if (!siteEntry) {
siteEntry.siteUrl = siteInfo.siteurl; this.accountsList.otherSites.some((sites) => {
siteEntry.fullName = siteInfo.fullname; siteEntry = sites.find((siteEntry) => siteEntry.id === siteId);
}
return siteEntry;
});
}
if (!siteEntry) {
return;
}
const site = await CoreSites.getSite(siteId);
const siteInfo = site.getInfo();
siteEntry.siteName = site.getSiteName();
if (siteInfo) {
siteEntry.siteUrl = siteInfo.siteurl;
siteEntry.fullName = siteInfo.fullname;
} }
}); });
this.isOnline = CoreNetwork.isOnline();
this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited();
this.networkObserver = CoreNetwork.onChange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
NgZone.run(() => {
this.isOnline = CoreNetwork.isOnline();
this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited();
});
});
} }
/** /**
* View loaded. * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const currentSiteId = CoreSites.getCurrentSiteId();
try { try {
this.sites = await CoreSites.getSortedSites(); this.accountsList = await CoreLoginHelper.getAccountsList(currentSiteId);
} catch { } catch {
// Ignore errors. // Ignore errors.
} }
this.sitesLoaded = true; this.sitesLoaded = true;
this.syncOnlyOnWifi = await CoreConfig.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, true); this.dataSaver = await CoreConfig.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, true);
} }
/** /**
* Called when sync only on wifi setting is enabled or disabled. * Called when sync only on wifi setting is enabled or disabled.
*/ */
syncOnlyOnWifiChanged(): void { syncOnlyOnWifiChanged(): void {
CoreConfig.set(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, this.syncOnlyOnWifi ? 1 : 0); CoreConfig.set(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, this.dataSaver ? 1 : 0);
} }
/** /**
* Syncrhonizes a site. * Synchronizes a site.
* *
* @param siteId Site ID. * @param siteId Site ID.
*/ */
@ -95,10 +139,20 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
return; return;
} }
CoreDomUtils.showErrorModalDefault(error, 'core.settings.errorsyncsite', true); CoreDomUtils.showErrorModalDefault(error, 'core.settings.sitesyncfailed', true);
} }
} }
/**
* Changes site.
*
* @param siteId Site ID.
*/
async login(siteId: string): Promise<void> {
// This navigation will logout and navigate to the site home.
await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId });
}
/** /**
* Returns true if site is beeing synchronized. * Returns true if site is beeing synchronized.
* *
@ -120,11 +174,12 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
} }
/** /**
* Page destroyed. * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.isDestroyed = true; this.isDestroyed = true;
this.sitesObserver?.off(); this.sitesObserver.off();
this.networkObserver.unsubscribe();
} }
} }

View File

@ -29,6 +29,7 @@ import { CoreCourse } from '@features/course/services/course';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { CoreTextUtils } from '@services/utils/text';
/** /**
* Object with space usage and cache entries that can be erased. * Object with space usage and cache entries that can be erased.
@ -260,6 +261,7 @@ export class CoreSettingsHelperProvider {
const site = await CoreSites.getSite(siteId); const site = await CoreSites.getSite(siteId);
const hasSyncHandlers = CoreCronDelegate.hasManualSyncHandlers(); const hasSyncHandlers = CoreCronDelegate.hasManualSyncHandlers();
// All these errors should not happen on manual sync because are prevented on UI.
if (site.isLoggedOut()) { if (site.isLoggedOut()) {
// Cannot sync logged out sites. // Cannot sync logged out sites.
throw new CoreError(Translate.instant('core.settings.cannotsyncloggedout')); throw new CoreError(Translate.instant('core.settings.cannotsyncloggedout'));
@ -286,6 +288,8 @@ export class CoreSettingsHelperProvider {
try { try {
await syncPromise; await syncPromise;
} catch (error) {
throw CoreTextUtils.addTitleToError(error, Translate.instant('core.settings.sitesyncfailed'));
} finally { } finally {
delete this.syncPromises[siteId]; delete this.syncPromises[siteId];
} }

View File

@ -1,4 +1,4 @@
@app @javascript @app @javascript @core_settings
Feature: It navigates properly within settings. Feature: It navigates properly within settings.
Background: Background:

View File

@ -0,0 +1,150 @@
@app @javascript @core_settings
Feature: It synchronise sites properly
Background:
Given the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "users" exist:
| username | firstname | lastname |
| student1 | david | student |
| student2 | pau | student2 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activities" exist:
| activity | name | intro | course | idnumber | option | allowmultiple | allowupdate | showresults |
| choice | Sync choice | Intro | C1 | choice1 | Option 1, Option 2, Option 3 | 0 | 0 | 1 |
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
When I switch network connection to offline
And I select "Option 1" in the app
And I press "Save my choice" in the app
And I press "OK" in the app
Then I should find "This Choice has offline data to be synchronised." in the app
# Cannot sync in offline
When I press the back button in the app
And I press the back button in the app
And I press the user menu button in the app
And I press "Preferences" in the app
Then I should find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
When I switch network connection to wifi
Then I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
# Check synced
When I press "Synchronise now" "button" in the app
And I wait loading to finish in the app
And I switch network connection to offline
And I press the back button in the app
And I entered the course "Course 1" in the app
And I press "Sync choice" in the app
Then I should not find "This Choice has offline data to be synchronised." in the app
# Check limited sync.
When I switch network connection to cellular
And I press the back button in the app
And I press the back button in the app
And I press the user menu button in the app
And I press "Preferences" in the app
# Cannot sync in cellular
Then I should find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
Scenario: Sync sites messages with different network connections
Given I entered the app as "student1"
# Wifi + data saver on.
When I press the more menu button in the app
And I press "App settings" in the app
And I press "Synchronisation" in the app
Then I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should find "Accounts" in the app
# Limited + data saver on.
When I switch network connection to cellular
Then I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should not find "Accounts" in the app
# Offline + data saver on.
When I switch network connection to offline
Then I should find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should not find "Accounts" in the app
# Wifi + data saver off.
When I press "Data saver: Synchronise only when on Wi-Fi" in the app
And I switch network connection to wifi
Then I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should find "Accounts" in the app
# Limited + data saver off.
When I switch network connection to cellular
Then I should not find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should find "Accounts" in the app
# Offline + data saver off.
When I switch network connection to offline
Then I should find "Your device is offline. Connect to the internet to synchronise sites." in the app
And I should not find "Connect to a Wi-Fi network or turn off Data saver to synchronise sites." in the app
And I should not find "Accounts" in the app
Scenario: Sync logged in and logged out sites
Given I entered the app as "student1"
And I log out in the app
And I entered the choice activity "Sync choice" on course "Course 1" as "student2" in the app
# Add something offline
When I switch network connection to offline
And I select "Option 1" in the app
And I press "Save my choice" in the app
And I press "OK" in the app
Then I should find "This Choice has offline data to be synchronised." in the app
When I press the back button in the app
And I press the back button in the app
And I press the more menu button in the app
And I press "App settings" in the app
And I press "Synchronisation" in the app
And I switch network connection to wifi
Then I should find "Accounts" in the app
# Check synced
When I press "Synchronise now" "button" in the app
And I wait loading to finish in the app
And I switch network connection to offline
And I press the back button in the app
And I entered the course "Course 1" in the app
And I press "Sync choice" in the app
Then I should not find "This Choice has offline data to be synchronised." in the app
# Test log in to sync
When I press the back button in the app
And I press the back button in the app
And I press the more menu button in the app
And I press "App settings" in the app
And I press "Synchronisation" in the app
And I switch network connection to wifi
Then I should find "Accounts" in the app
And I should find "Log in to synchronise" in the app
When I press "Log in" in the app
Then I should find "Reconnect" in the app
When I set the field "Password" to "student1" in the app
And I press "Log in" in the app
And I press the more menu button in the app
And I press "App settings" in the app
And I press "Synchronisation" in the app
Then I should not find "Log in to synchronise" in the app

View File

@ -1,7 +1,7 @@
:host { :host {
--popover-background: var(--ion-overlay-background-color, var(--ion-background-color, #fff)); --popover-background: var(--ion-overlay-background-color, var(--ion-background-color, #fff));
z-index: 99; z-index: 105; // Main menu is 101.
width: 100%; width: 100%;
height: 100%; height: 100%;
display: none; display: none;

View File

@ -62,6 +62,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy
@Output() afterDismiss = new EventEmitter<void>(); @Output() afterDismiss = new EventEmitter<void>();
@HostBinding('class.is-active') active = false; @HostBinding('class.is-active') active = false;
@HostBinding('class.is-popover') popover = false; @HostBinding('class.is-popover') popover = false;
@HostBinding('class.backdrop') backdrop = true;
@ViewChild('wrapper') wrapper?: ElementRef<HTMLElement>; @ViewChild('wrapper') wrapper?: ElementRef<HTMLElement>;
focusStyles?: string; focusStyles?: string;

View File

@ -96,6 +96,7 @@
"downloading": "Downloading", "downloading": "Downloading",
"edit": "Edit", "edit": "Edit",
"emptysplit": "This page will appear blank if the left panel is empty or is loading.", "emptysplit": "This page will appear blank if the left panel is empty or is loading.",
"endonesteptour": "Got it",
"error": "Error", "error": "Error",
"errorchangecompletion": "An error occurred while changing the completion status. Please try again.", "errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
"errordeletefile": "Error deleting the file. Please try again.", "errordeletefile": "Error deleting the file. Please try again.",
@ -111,7 +112,9 @@
"erroropenfilenoextension": "Error opening file: the file doesn't have an extension.", "erroropenfilenoextension": "Error opening file: the file doesn't have an extension.",
"erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.", "erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.",
"errorrenamefile": "Error renaming file. Please try again.", "errorrenamefile": "Error renaming file. Please try again.",
"errorsitesupport": "If the problem persists, contact site support.",
"errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.", "errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
"errorsomethingwrong": "Something went wrong. Please try again.",
"errorsync": "An error occurred while synchronising. Please try again.", "errorsync": "An error occurred while synchronising. Please try again.",
"errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.", "errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
"errorurlschemeinvalidscheme": "This URL is meant to be used in another app: {{$a}}.", "errorurlschemeinvalidscheme": "This URL is meant to be used in another app: {{$a}}.",
@ -248,8 +251,8 @@
"resources": "Resources", "resources": "Resources",
"restore": "Restore", "restore": "Restore",
"restricted": "Restricted", "restricted": "Restricted",
"retry": "Retry",
"resume": "Resume", "resume": "Resume",
"retry": "Retry",
"save": "Save", "save": "Save",
"savechanges": "Save changes", "savechanges": "Save changes",
"scanqr": "Scan QR code", "scanqr": "Scan QR code",
@ -328,11 +331,10 @@
"user": "User", "user": "User",
"userdeleted": "This user account has been deleted", "userdeleted": "This user account has been deleted",
"userdetails": "User details", "userdetails": "User details",
"usernotfullysetup": "User not fully set-up",
"usernologin": "Authentication has been revoked for this account", "usernologin": "Authentication has been revoked for this account",
"usersuspended": "Registration suspended", "usernotfullysetup": "User not fully set-up",
"endonesteptour": "Got it",
"users": "Users", "users": "Users",
"usersuspended": "Registration suspended",
"view": "View", "view": "View",
"viewcode": "View code", "viewcode": "View code",
"vieweditor": "View editor", "vieweditor": "View editor",
@ -351,8 +353,8 @@
"year": "year", "year": "year",
"years": "years", "years": "years",
"yes": "Yes", "yes": "Yes",
"youreoffline": "You are offline", "youreoffline": "Your device is offline",
"youreonline": "You are back online", "youreonline": "Your device is back online",
"zoomin": "Zoom In", "zoomin": "Zoom In",
"zoomout": "Zoom Out" "zoomout": "Zoom Out"
} }

View File

@ -30,7 +30,7 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreNetwork } from '@services/network'; import { CoreNetwork, CoreNetworkConnection } from '@services/network';
/** /**
* Factory to provide some global functionalities, like access to the global app database. * Factory to provide some global functionalities, like access to the global app database.
@ -644,10 +644,10 @@ export class CoreAppProvider {
* Set value of forceOffline flag. If true, the app will think the device is offline. * Set value of forceOffline flag. If true, the app will think the device is offline.
* *
* @param value Value to set. * @param value Value to set.
* @deprecated since 4.1.0. Use CoreNetwork instead. * @deprecated since 4.1.0. Use CoreNetwork.setForceConnectionMode instead.
*/ */
setForceOffline(value: boolean): void { setForceOffline(value: boolean): void {
CoreNetwork.setForceOffline(value); CoreNetwork.setForceConnectionMode(value ? CoreNetworkConnection.NONE : CoreNetworkConnection.WIFI);
} }
/** /**

View File

@ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { makeSingleton } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/database/cron'; import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/database/cron';
import { asyncInstance } from '../utils/async-instance'; import { asyncInstance } from '../utils/async-instance';
@ -81,10 +81,11 @@ export class CoreCronDelegateService {
protected async checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> { protected async checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> {
if (!this.handlers[name] || !this.handlers[name].execute) { if (!this.handlers[name] || !this.handlers[name].execute) {
// Invalid handler. // Invalid handler.
const message = `Cannot execute handler because is invalid: ${name}`; this.logger.debug(`Cannot execute cron job because is invalid: ${name}`);
this.logger.debug(message);
throw new CoreError(message); throw new CoreError(
Translate.instant('core.errorsomethingwrong') + '<br>' + Translate.instant('core.errorsitesupport'),
);
} }
const usesNetwork = this.handlerUsesNetwork(name); const usesNetwork = this.handlerUsesNetwork(name);
@ -92,11 +93,10 @@ export class CoreCronDelegateService {
if (usesNetwork && !CoreNetwork.isOnline()) { if (usesNetwork && !CoreNetwork.isOnline()) {
// Offline, stop executing. // Offline, stop executing.
const message = `Cannot execute handler because device is offline: ${name}`; this.logger.debug(`Cron job failed because your device is not connected to the internet: ${name}`);
this.logger.debug(message);
this.stopHandler(name); this.stopHandler(name);
throw new CoreError(message); throw new CoreError(Translate.instant('core.settings.cannotsyncoffline'));
} }
if (isSync) { if (isSync) {
@ -105,11 +105,10 @@ export class CoreCronDelegateService {
if (syncOnlyOnWifi && !CoreNetwork.isWifi()) { if (syncOnlyOnWifi && !CoreNetwork.isWifi()) {
// Cannot execute in this network connection, retry soon. // Cannot execute in this network connection, retry soon.
const message = `Cannot execute handler because device is using limited connection: ${name}`; this.logger.debug(`Cron job failed because your device has a limited internet connection: ${name}`);
this.logger.debug(message);
this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL);
throw new CoreError(message); throw new CoreError(Translate.instant('core.settings.cannotsyncwithoutwifi'));
} }
} }
@ -118,7 +117,7 @@ export class CoreCronDelegateService {
try { try {
await this.executeHandler(name, force, siteId); await this.executeHandler(name, force, siteId);
this.logger.debug(`Execution of handler '${name}' was a success.`); this.logger.debug(`Cron job '${name}' was successfully executed.`);
await CoreUtils.ignoreErrors(this.setHandlerLastExecutionTime(name, Date.now())); await CoreUtils.ignoreErrors(this.setHandlerLastExecutionTime(name, Date.now()));
@ -127,11 +126,10 @@ export class CoreCronDelegateService {
return; return;
} catch (error) { } catch (error) {
// Handler call failed. Retry soon. // Handler call failed. Retry soon.
const message = `Execution of handler '${name}' failed.`; this.logger.error(`Cron job '${name}' failed.`, error);
this.logger.error(message, error);
this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL);
throw new CoreError(message); throw error;
} }
}); });

View File

@ -18,6 +18,17 @@ import { Network } from '@ionic-native/network/ngx';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { Observable, Subject, merge } from 'rxjs'; import { Observable, Subject, merge } from 'rxjs';
export enum CoreNetworkConnection {
UNKNOWN = 'unknown',
ETHERNET = 'ethernet',
WIFI = 'wifi',
CELL_2G = '2g',
CELL_3G = '3g',
CELL_4G = '4g',
CELL = 'cellular',
NONE = 'none',
};
/** /**
* Service to manage network connections. * Service to manage network connections.
*/ */
@ -28,9 +39,21 @@ export class CoreNetworkService extends Network {
protected connectObservable = new Subject<'connected'>(); protected connectObservable = new Subject<'connected'>();
protected disconnectObservable = new Subject<'disconnected'>(); protected disconnectObservable = new Subject<'disconnected'>();
protected forceOffline = false; protected forceConnectionMode?: CoreNetworkConnection;
protected online = false; protected online = false;
get connectionType(): CoreNetworkConnection {
if (this.forceConnectionMode !== undefined) {
return this.forceConnectionMode;
}
if (CorePlatform.isMobile()) {
return this.type as CoreNetworkConnection;
}
return this.online ? CoreNetworkConnection.WIFI : CoreNetworkConnection.NONE;
}
/** /**
* Initialize the service. * Initialize the service.
*/ */
@ -38,20 +61,25 @@ export class CoreNetworkService extends Network {
this.checkOnline(); this.checkOnline();
if (CorePlatform.isMobile()) { if (CorePlatform.isMobile()) {
this.onChange().subscribe(() => { // We cannot directly listen to onChange because it depends on
// onConnect and onDisconnect that have been already overriden.
super.onConnect().subscribe(() => {
this.fireObservable();
});
super.onDisconnect().subscribe(() => {
this.fireObservable(); this.fireObservable();
}); });
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any> window).Connection = { (<any> window).Connection = {
UNKNOWN: 'unknown', // eslint-disable-line @typescript-eslint/naming-convention UNKNOWN: CoreNetworkConnection.UNKNOWN, // eslint-disable-line @typescript-eslint/naming-convention
ETHERNET: 'ethernet', // eslint-disable-line @typescript-eslint/naming-convention ETHERNET: CoreNetworkConnection.ETHERNET, // eslint-disable-line @typescript-eslint/naming-convention
WIFI: 'wifi', // eslint-disable-line @typescript-eslint/naming-convention WIFI: CoreNetworkConnection.WIFI, // eslint-disable-line @typescript-eslint/naming-convention
CELL_2G: '2g', // eslint-disable-line @typescript-eslint/naming-convention CELL_2G: CoreNetworkConnection.CELL_2G, // eslint-disable-line @typescript-eslint/naming-convention
CELL_3G: '3g', // eslint-disable-line @typescript-eslint/naming-convention CELL_3G: CoreNetworkConnection.CELL_3G, // eslint-disable-line @typescript-eslint/naming-convention
CELL_4G: '4g', // eslint-disable-line @typescript-eslint/naming-convention CELL_4G: CoreNetworkConnection.CELL_4G, // eslint-disable-line @typescript-eslint/naming-convention
CELL: 'cellular', // eslint-disable-line @typescript-eslint/naming-convention CELL: CoreNetworkConnection.CELL, // eslint-disable-line @typescript-eslint/naming-convention
NONE: 'none', // eslint-disable-line @typescript-eslint/naming-convention NONE: CoreNetworkConnection.NONE, // eslint-disable-line @typescript-eslint/naming-convention
}; };
window.addEventListener('online', () => { window.addEventListener('online', () => {
@ -65,12 +93,13 @@ export class CoreNetworkService extends Network {
} }
/** /**
* Set value of forceOffline flag. If true, the app will think the device is offline. * Set value of forceConnectionMode flag.
* The app will think the device is offline or limited connection.
* *
* @param value Value to set. * @param value Value to set.
*/ */
setForceOffline(value: boolean): void { setForceConnectionMode(value: CoreNetworkConnection): void {
this.forceOffline = !!value; this.forceConnectionMode = value;
this.fireObservable(); this.fireObservable();
} }
@ -89,20 +118,15 @@ export class CoreNetworkService extends Network {
* @return Whether the app is online. * @return Whether the app is online.
*/ */
checkOnline(): void { checkOnline(): void {
if (this.forceOffline) { if (this.forceConnectionMode === CoreNetworkConnection.NONE) {
this.online = false; this.online = false;
return; return;
} }
if (!CorePlatform.isMobile()) { const type = this.connectionType;
this.online = navigator.onLine;
return; let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN;
}
let online = this.type !== null && this.type != this.Connection.NONE &&
this.type != this.Connection.UNKNOWN;
// Double check we are not online because we cannot rely 100% in Cordova APIs. // Double check we are not online because we cannot rely 100% in Cordova APIs.
if (!online && navigator.onLine) { if (!online && navigator.onLine) {
@ -123,6 +147,7 @@ export class CoreNetworkService extends Network {
/** /**
* Returns an observable to notify when the app is connected. * Returns an observable to notify when the app is connected.
* It will also be fired when connection type changes.
* *
* @return Observable. * @return Observable.
*/ */
@ -143,12 +168,11 @@ export class CoreNetworkService extends Network {
* Fires the correct observable depending on the connection status. * Fires the correct observable depending on the connection status.
*/ */
protected fireObservable(): void { protected fireObservable(): void {
const previousOnline = this.online;
this.checkOnline(); this.checkOnline();
if (this.online && !previousOnline) {
if (this.online) {
this.connectObservable.next('connected'); this.connectObservable.next('connected');
} else if (!this.online && previousOnline) { } else {
this.disconnectObservable.next('disconnected'); this.disconnectObservable.next('disconnected');
} }
} }
@ -159,18 +183,16 @@ export class CoreNetworkService extends Network {
* @return Whether the device uses a limited connection. * @return Whether the device uses a limited connection.
*/ */
isNetworkAccessLimited(): boolean { isNetworkAccessLimited(): boolean {
if (!CorePlatform.isMobile()) { const limited: CoreNetworkConnection[] = [
return false; CoreNetworkConnection.CELL_2G,
} CoreNetworkConnection.CELL_3G,
CoreNetworkConnection.CELL_4G,
const limited = [ CoreNetworkConnection.CELL,
this.Connection.CELL_2G,
this.Connection.CELL_3G,
this.Connection.CELL_4G,
this.Connection.CELL,
]; ];
return limited.indexOf(this.type) > -1; const type = this.connectionType;
return limited.indexOf(type) > -1;
} }
/** /**

View File

@ -51,7 +51,7 @@ import { CoreRedirectPayload } from './navigator';
import { CoreSitesFactory } from './sites-factory'; import { CoreSitesFactory } from './sites-factory';
import { CoreText } from '@singletons/text'; import { CoreText } from '@singletons/text';
import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle'; import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle';
import { CoreAjaxError } from '@classes/errors/ajaxerror'; import { CoreAjaxError } from '@classes/errors/ajaxerror';
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
@ -870,7 +870,7 @@ export class CoreSitesProvider {
const siteUrlAllowed = await CoreLoginHelper.isSiteUrlAllowed(site.getURL(), false); const siteUrlAllowed = await CoreLoginHelper.isSiteUrlAllowed(site.getURL(), false);
if (!siteUrlAllowed) { if (!siteUrlAllowed) {
throw new CoreErrorWithTitle(Translate.instant('core.login.sitenotallowed')); throw new CoreErrorWithOptions(Translate.instant('core.login.sitenotallowed'));
} }
this.currentSite = site; this.currentSite = site;
@ -1185,6 +1185,7 @@ export class CoreSitesProvider {
siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename, siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename,
avatar: siteInfo?.userpictureurl, avatar: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1, siteHomeId: siteInfo?.siteid || 1,
loggedOut: !!site.loggedOut,
}; };
formattedSites.push(basicInfo); formattedSites.push(basicInfo);
} }
@ -1277,7 +1278,7 @@ export class CoreSitesProvider {
/** /**
* Logout the user. * Logout the user.
* *
* @param forceLogout If true, site will be marked as logged out, no matter the value tool_mobile_forcelogout. * @param options Logout options.
* @return Promise resolved when the user is logged out. * @return Promise resolved when the user is logged out.
*/ */
async logout(options: CoreSitesLogoutOptions = {}): Promise<void> { async logout(options: CoreSitesLogoutOptions = {}): Promise<void> {
@ -1923,6 +1924,7 @@ export type CoreSiteBasicInfo = {
avatar?: string; // User's avatar. avatar?: string; // User's avatar.
badge?: number; // Badge to display in the site. badge?: number; // Badge to display in the site.
siteHomeId?: number; // Site home ID. siteHomeId?: number; // Site home ID.
loggedOut: boolean; // If Site is logged out.
}; };
/** /**

View File

@ -46,7 +46,6 @@ import { CoreViewerImageComponent } from '@features/viewer/components/image/imag
import { CoreFormFields, CoreForms } from '../../singletons/form'; import { CoreFormFields, CoreForms } from '../../singletons/form';
import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition'; import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition';
import { CoreZoomLevel } from '@features/settings/services/settings-helper'; import { CoreZoomLevel } from '@features/settings/services/settings-helper';
import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle';
import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { NavigationStart } from '@angular/router'; import { NavigationStart } from '@angular/router';
@ -1345,17 +1344,22 @@ export class CoreDomUtilsProvider {
const alertOptions: AlertOptions = { const alertOptions: AlertOptions = {
message: message, message: message,
buttons: [Translate.instant('core.ok')],
}; };
if (this.isNetworkError(message, error)) { if (this.isNetworkError(message, error)) {
alertOptions.cssClass = 'core-alert-network-error'; alertOptions.cssClass = 'core-alert-network-error';
} else if (error instanceof CoreErrorWithTitle) { } else if (typeof error !== 'string' && 'title' in error) {
alertOptions.header = error.title || undefined; alertOptions.header = error.title || undefined;
} else { } else {
alertOptions.header = Translate.instant('core.error'); alertOptions.header = Translate.instant('core.error');
} }
if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') {
alertOptions.buttons = error.buttons;
} else {
alertOptions.buttons = [Translate.instant('core.ok')];
}
return this.showAlertWithOptions(alertOptions, autocloseTime); return this.showAlertWithOptions(alertOptions, autocloseTime);
} }

View File

@ -26,6 +26,7 @@ import { CoreFileHelper } from '@services/file-helper';
import { CoreDomUtils } from './dom'; import { CoreDomUtils } from './dom';
import { CoreText } from '@singletons/text'; import { CoreText } from '@singletons/text';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { AlertButton } from '@ionic/angular';
/** /**
* Different type of errors the app can treat. * Different type of errors the app can treat.
@ -37,6 +38,8 @@ export type CoreTextErrorObject = {
body?: string; body?: string;
debuginfo?: string; debuginfo?: string;
backtrace?: string; backtrace?: string;
title?: string;
buttons?: AlertButton[];
}; };
/* /*
@ -149,6 +152,27 @@ export class CoreTextUtilsProvider {
return error; return error;
} }
/**
* Add some title to an error message.
*
* @param error Error message or object.
* @param title Title to add.
* @return Modified error.
*/
addTitleToError(error: string | CoreError | CoreTextErrorObject | undefined | null, title: string): CoreTextErrorObject {
let improvedError: CoreTextErrorObject = {};
if (typeof error === 'string') {
improvedError.message = error;
} else if (error && 'message' in error) {
improvedError = error;
}
improvedError.title = improvedError.title || title;
return improvedError;
}
/** /**
* Given an address as a string, return a URL to open the address in maps. * Given an address as a string, return a URL to open the address in maps.
* *

View File

@ -18,9 +18,6 @@ import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, NgZone } from '@singletons'; import { makeSingleton, NgZone } from '@singletons';
import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime'; import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime';
// Containers that block containers behind them.
const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'CORE-USER-TOURS-USER-TOUR', 'ION-PAGE'];
/** /**
* Behat Dom Utils helper functions. * Behat Dom Utils helper functions.
*/ */
@ -331,13 +328,14 @@ export class TestingBehatDomUtilsService {
} }
// Get containers until one blocks other views. // Get containers until one blocks other views.
containers.find(container => { containers.some(container => {
if (container.tagName === 'ION-TOAST') { if (container.tagName === 'ION-TOAST') {
container = container.shadowRoot?.querySelector('.toast-container') || container; container = container.shadowRoot?.querySelector('.toast-container') || container;
} }
topContainers.push(container); topContainers.push(container);
return blockingContainers.includes(container.tagName); // If container has backdrop it blocks the rest of the UI.
return container.querySelector(':scope > ion-backdrop') || container.classList.contains('backdrop');
}); });
return topContainers; return topContainers;

View File

@ -25,9 +25,8 @@ import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDom } from '@singletons/dom'; import { CoreDom } from '@singletons/dom';
import { IonRefresher } from '@ionic/angular';
import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dashboard';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreSites, CoreSitesProvider } from '@services/sites';
/** /**
* Behat runtime servive with public API. * Behat runtime servive with public API.
@ -53,6 +52,10 @@ export class TestingBehatRuntimeService {
return CorePushNotifications.instance; return CorePushNotifications.instance;
} }
get sites(): CoreSitesProvider {
return CoreSites.instance;
}
/** /**
* Init behat functions and set options like skipping onboarding. * Init behat functions and set options like skipping onboarding.
* *
@ -348,23 +351,17 @@ export class TestingBehatRuntimeService {
this.log('Action - pullToRefresh'); this.log('Action - pullToRefresh');
try { try {
// TODO We should generalize this to work with other pages. It's not possible to use // 'el' is protected, but there's no other way to trigger refresh programatically.
// an IonRefresher instance because it doesn't expose any methods to trigger refresh, const ionRefresher = this.getAngularInstance<{ el: HTMLIonRefresherElement }>(
// so we'll have to find another way. 'ion-refresher',
'IonRefresher',
const dashboard = this.getAngularInstance<CoreCoursesDashboardPage>(
'page-core-courses-dashboard',
'CoreCoursesDashboardPage',
); );
if (!dashboard) { if (!ionRefresher) {
return 'ERROR: It\'s not possible to pull to refresh the current page ' return 'ERROR: It\'s not possible to pull to refresh the current page.';
+ '(the dashboard page is the only one supported at the moment).';
} }
await new Promise(resolve => { ionRefresher.el.dispatchEvent(new CustomEvent('ionRefresh'));
dashboard.refreshDashboard({ complete: resolve } as IonRefresher);
});
return 'OK'; return 'OK';
} catch (error) { } catch (error) {

View File

@ -25,9 +25,7 @@ Feature: It navigates properly using deep links.
Scenario: Receive a push notification Scenario: Receive a push notification
Given I entered the app as "student2" Given I entered the app as "student2"
When I press the user menu button in the app When I log out in the app
And I press "Log out" in the app
And I wait the app to restart
And I press "Add" in the app And I press "Add" in the app
And I set the field "Your site" to "$WWWROOT" in the app And I set the field "Your site" to "$WWWROOT" in the app
And I press "Connect to your site" in the app And I press "Connect to your site" in the app
@ -67,9 +65,7 @@ Feature: It navigates properly using deep links.
Scenario: Open a link with a custom URL that calls WebServices for a logged out site Scenario: Open a link with a custom URL that calls WebServices for a logged out site
Given I entered the app as "student2" Given I entered the app as "student2"
When I press the user menu button in the app When I log out in the app
And I press "Log out" in the app
And I wait the app to restart
And I open a custom link in the app for: And I open a custom link in the app for:
| forum | | forum |
| Test forum | | Test forum |