commit
						5630f5b054
					
				| @ -91,8 +91,6 @@ class behat_app extends behat_app_helper { | ||||
|      * @throws ExpectationException Problem with resizing window | ||||
|      */ | ||||
|     public function i_launch_the_app(string $runtime = '') { | ||||
|         $this->check_tags(); | ||||
| 
 | ||||
|         // Go to page and prepare browser for app.
 | ||||
|         $this->prepare_browser(['skiponboarding' => empty($runtime)]); | ||||
|     } | ||||
| @ -101,18 +99,27 @@ class behat_app extends behat_app_helper { | ||||
|      * @Then 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.
 | ||||
|         $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. | ||||
|      * | ||||
|      * @When /^I pull to refresh in the app$/ | ||||
|      * @When I pull to refresh in the app | ||||
|      * @throws DriverException If the gesture is not available | ||||
|      */ | ||||
|     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)"$/ | ||||
|      * @param string $offline New value for navigator online mode | ||||
|      * @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) { | ||||
|         $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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -95,6 +95,12 @@ class behat_app_helper extends behat_base { | ||||
|     public function start_scenario() { | ||||
|         $this->check_behat_setup(); | ||||
|         $this->fix_moodle_setup(); | ||||
| 
 | ||||
|         if ($this->apprunning) { | ||||
|             $this->notify_unload(); | ||||
|             $this->apprunning = false; | ||||
|         } | ||||
| 
 | ||||
|         $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 | ||||
|      */ | ||||
|     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) { | ||||
|             if ($this->apprunning) { | ||||
|                 $this->notify_unload(); | ||||
|             } | ||||
|         $restart = false; | ||||
| 
 | ||||
|             // Restart the browser and set its size.
 | ||||
|             $this->getSession()->restart(); | ||||
|         if (!$this->apprunning) { | ||||
|             $this->check_tags(); | ||||
| 
 | ||||
|             $restart = true; | ||||
| 
 | ||||
|             // Reset its size.
 | ||||
|             $this->resize_window($this->windowsize, true); | ||||
| 
 | ||||
|             if (empty($this->ionicurl)) { | ||||
|                 $this->ionicurl = $this->start_or_reuse_ionic(); | ||||
|             } | ||||
| @ -506,6 +515,8 @@ class behat_app_helper extends behat_base { | ||||
|             $successXPath = '//page-core-mainmenu'; | ||||
|         } | ||||
| 
 | ||||
|         $this->i_log_out_in_app(false); | ||||
| 
 | ||||
|         $this->handle_url($url, $successXPath); | ||||
|     } | ||||
| 
 | ||||
| @ -536,7 +547,6 @@ class behat_app_helper extends behat_base { | ||||
|         if ($result !== 'OK') { | ||||
|             throw new DriverException('Error handling url - ' . $result); | ||||
|         } | ||||
| 
 | ||||
|         if (!empty($successXPath)) { | ||||
|             // Wait until the page appears.
 | ||||
|             $this->spin( | ||||
| @ -550,6 +560,8 @@ class behat_app_helper extends behat_base { | ||||
|         } | ||||
| 
 | ||||
|         $this->wait_for_pending_js(); | ||||
| 
 | ||||
|         $this->i_wait_the_app_to_restart(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -68,6 +68,8 @@ function do_match { | ||||
|         print_message "$2" | ||||
|         tput setaf 6 | ||||
|         grep "$match" $LANGPACKSFOLDER/en/*.php | ||||
|     else | ||||
|         coincidence=0 | ||||
|     fi | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1705,7 +1705,9 @@ | ||||
|   "core.erroropenfilenoextension": "local_moodlemobileapp", | ||||
|   "core.erroropenpopup": "local_moodlemobileapp", | ||||
|   "core.errorrenamefile": "local_moodlemobileapp", | ||||
|   "core.errorsitesupport": "local_moodlemobileapp", | ||||
|   "core.errorsomedatanotdownloaded": "local_moodlemobileapp", | ||||
|   "core.errorsomethingwrong": "local_moodlemobileapp", | ||||
|   "core.errorsync": "local_moodlemobileapp", | ||||
|   "core.errorsyncblocked": "local_moodlemobileapp", | ||||
|   "core.errorurlschemeinvalidscheme": "local_moodlemobileapp", | ||||
| @ -2179,6 +2181,8 @@ | ||||
|   "core.settings.colorscheme-system": "local_moodlemobileapp", | ||||
|   "core.settings.colorscheme-system-notice": "local_moodlemobileapp", | ||||
|   "core.settings.compilationinfo": "local_moodlemobileapp", | ||||
|   "core.settings.connecttosync": "local_moodlemobileapp", | ||||
|   "core.settings.connectwifitosync": "local_moodlemobileapp", | ||||
|   "core.settings.copyinfo": "local_moodlemobileapp", | ||||
|   "core.settings.cordovadevicemodel": "local_moodlemobileapp", | ||||
|   "core.settings.cordovadeviceosversion": "local_moodlemobileapp", | ||||
| @ -2200,9 +2204,7 @@ | ||||
|   "core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp", | ||||
|   "core.settings.enablerichtexteditor": "local_moodlemobileapp", | ||||
|   "core.settings.enablerichtexteditordescription": "local_moodlemobileapp", | ||||
|   "core.settings.enablesyncwifi": "local_moodlemobileapp", | ||||
|   "core.settings.entriesincache": "local_moodlemobileapp", | ||||
|   "core.settings.errorsyncsite": "local_moodlemobileapp", | ||||
|   "core.settings.estimatedfreespace": "local_moodlemobileapp", | ||||
|   "core.settings.filesystemroot": "local_moodlemobileapp", | ||||
|   "core.settings.fontsize": "local_moodlemobileapp", | ||||
| @ -2219,6 +2221,7 @@ | ||||
|   "core.settings.locationhref": "local_moodlemobileapp", | ||||
|   "core.settings.loggedin": "message", | ||||
|   "core.settings.loggedoff": "message", | ||||
|   "core.settings.logintosync": "local_moodlemobileapp", | ||||
|   "core.settings.navigatorlanguage": "local_moodlemobileapp", | ||||
|   "core.settings.navigatoruseragent": "local_moodlemobileapp", | ||||
|   "core.settings.networkstatus": "local_moodlemobileapp", | ||||
| @ -2233,7 +2236,9 @@ | ||||
|   "core.settings.showdownloadoptions": "local_moodlemobileapp", | ||||
|   "core.settings.siteinfo": "local_moodlemobileapp", | ||||
|   "core.settings.sites": "moodle", | ||||
|   "core.settings.sitesyncfailed": "local_moodlemobileapp", | ||||
|   "core.settings.spaceusage": "local_moodlemobileapp", | ||||
|   "core.settings.syncdatasaver": "local_moodlemobileapp", | ||||
|   "core.settings.synchronization": "local_moodlemobileapp", | ||||
|   "core.settings.synchronizenow": "local_moodlemobileapp", | ||||
|   "core.settings.synchronizenowhelp": "local_moodlemobileapp", | ||||
|  | ||||
| @ -49,7 +49,6 @@ Feature: Timeline block. | ||||
|   @lms_from3.11 | ||||
|   Scenario: See courses inside block | ||||
|     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 | ||||
|     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 | ||||
|  | ||||
| @ -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 press "Search" "button" 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 press "Send" 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 | ||||
|     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 "Student1 student1" 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 press "Search" "button" 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 press "Send" in the app | ||||
|     And I set the field "New message" to "byee" in the app | ||||
|     And I press "Send" 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 | ||||
| 
 | ||||
|     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 | ||||
|     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 press "Send" 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 "Student1 student1" in the app | ||||
|     And I press "Display options" in the app | ||||
|  | ||||
| @ -79,6 +79,7 @@ Feature: Test basic usage of assignment activity in app | ||||
| 
 | ||||
|     # Submit second attempt as a student | ||||
|     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 | ||||
|     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 | ||||
| @ -97,6 +98,7 @@ Feature: Test basic usage of assignment activity in app | ||||
|     # View second attempt as a teacher | ||||
|     Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" 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 | ||||
|     Then I should find "Online text submissions" 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 | ||||
|     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" 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 press "Save" in the app | ||||
|     And I press "Submit assignment" 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 | ||||
| 
 | ||||
|     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 "assignment1" 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 | ||||
|     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" 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 press "Save" 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 | ||||
|     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 "assignment1" in the app | ||||
|     Then I should find "Submitted for grading" in the app | ||||
|  | ||||
| @ -34,6 +34,9 @@ Feature: Test basic usage of chat in app | ||||
|     And I press "Send" in the app | ||||
|     Then I should find "Hi!" 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 | ||||
|     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 | ||||
|     And I press "Send" 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 | ||||
|     Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app | ||||
|  | ||||
| @ -23,6 +23,9 @@ Feature: Test chat navigation | ||||
|     And I set the field "New message" to "Test message" in the app | ||||
|     And I press "Send" 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 | ||||
|     Given I entered the course "Course 1" as "student2" in the app | ||||
|  | ||||
| @ -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           | | ||||
|     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 | ||||
|     And I switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     And I select "Option 2" in the app | ||||
|     And I press "Save my choice" 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 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 "Test single choice name" 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           | | ||||
|     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 | ||||
|     And I switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     And I select "Option 2" in the app | ||||
|     And I press "Save my choice" 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 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 wait loading to finish 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 | ||||
|     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 | ||||
|     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 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 "Test single choice name" in the app | ||||
|     Then I should find "Option 1: 0" in the app | ||||
|  | ||||
| @ -33,7 +33,7 @@ Feature: Users can store entries in database activities when offline and sync wh | ||||
| 
 | ||||
|   Scenario: Create entry (offline) | ||||
|     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 | ||||
|     When I press "Add entries" 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 "This Database has offline data to be synchronised" 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 should find "https://moodle.org/" 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 "Download" in the app | ||||
|     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 | ||||
|     And I set the following fields to these values in the app: | ||||
|       | 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 "This Database has offline data to be synchronised" 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 should not find "https://moodle.org/" 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 "Refresh" in the app | ||||
|     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 should find "Are you sure you want to delete this entry?" 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 "This Database has offline data to be synchronised" 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 should not find "https://moodlecloud.com/" 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 "Download" in the app | ||||
|     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 should find "Are you sure you want to delete this entry?" 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 press "Restore" 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 | ||||
|     Then I should find "https://moodle.org/" in the app | ||||
|     And I should find "Moodle community site" in the app | ||||
|  | ||||
| @ -96,7 +96,7 @@ Feature: Test basic usage of forum activity in app | ||||
|     Then I should find "Reply" 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 "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 | ||||
|     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 "Initial discussion" 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 | ||||
|     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 set the following fields to these values in the app: | ||||
|       | 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 | ||||
|     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 | ||||
|     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 | ||||
| 
 | ||||
|     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 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 | ||||
| @ -163,7 +163,7 @@ Feature: Test basic usage of forum activity in 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 | ||||
| 
 | ||||
|     When I switch offline mode to "false" | ||||
|     When I switch network connection to wifi | ||||
|     And I press "OK" in the app | ||||
|     And I press "Edit" 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 | ||||
|     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 | ||||
|     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 | ||||
|     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 "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 | ||||
|     And I press "None" near "Auto-test message" 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 "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 | ||||
|     And I should find "Average of ratings: -" 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 | ||||
|     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 | ||||
|     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 | ||||
|     And I switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     Then I should find "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 | ||||
| 
 | ||||
|     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 | ||||
|     Then I should find "Initial discussion message" 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 | ||||
|     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 set the following fields to these values in the app: | ||||
|       | 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 "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 "Test forum name" 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 | ||||
|     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 set the following fields to these values in the app: | ||||
|       | 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 "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 wait loading to finish 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 | ||||
| 
 | ||||
|     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 | ||||
|     Then I should find "Initial discussion" in the app | ||||
| 
 | ||||
|  | ||||
| @ -103,7 +103,7 @@ Feature: Test forum navigation | ||||
|     # Offline | ||||
|     When I press the back button 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: | ||||
|       | Subject | Offline discussion 1 | | ||||
|       | Message | Offline discussion 1 message | | ||||
| @ -199,7 +199,7 @@ Feature: Test forum navigation | ||||
| 
 | ||||
|     # Offline | ||||
|     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: | ||||
|       | Subject | Offline discussion 1 | | ||||
|       | Message | Offline discussion 1 message | | ||||
|  | ||||
| @ -100,6 +100,7 @@ Feature: Test basic usage of glossary in app | ||||
|     # View comments as a student | ||||
|     Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" 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 | ||||
| 
 | ||||
|     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 "Download" within "Test glossary" "ion-item" 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 | ||||
|     Then the header should be "Test glossary" 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 | ||||
|     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 switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     And I set the following fields to these values in the app: | ||||
|       | Concept | Broccoli | | ||||
|       | 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 "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 "Synchronise now" 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 | ||||
|     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 switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     And I press "None" 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 | ||||
|     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 | ||||
|     Then I should find "This Glossary has offline data to be synchronised." in the app | ||||
| 
 | ||||
|  | ||||
| @ -173,7 +173,7 @@ Feature: Test glossary navigation | ||||
|     When I press the back button in the app | ||||
|     And I press "Clear search" 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: | ||||
|       | Concept | Tomato | | ||||
|       | Definition | Tomato is a fruit | | ||||
| @ -274,7 +274,7 @@ Feature: Test glossary navigation | ||||
|     # Offline | ||||
|     When I press "Clear search" 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: | ||||
|       | Concept | Tomato | | ||||
|       | Definition | Tomato is a fruit | | ||||
|  | ||||
| @ -233,12 +233,12 @@ Feature: Test basic usage of survey activity in app | ||||
|       | activity | name                           | intro        | template | course | idnumber | groupmode | | ||||
|       | survey   | Test survey critical incidents | Test survey1 | 5        | C1     | survey1  | 0         | | ||||
|     Given I entered the survey activity "Test survey critical incidents" on course "Course 1" as "student1" in the app | ||||
|     And I switch offline mode to "true" | ||||
|     And I switch network connection to offline | ||||
|     And I press "Submit" in the app | ||||
|     And I press "OK" in the app | ||||
|     Then I should see "This Survey has offline data to be synchronised." | ||||
| 
 | ||||
|     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 "Test survey critical incidents" 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 "Download" within "Test survey critical incidents" "ion-item" 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 | ||||
|     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 | ||||
|     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 | ||||
|     Then I should not see "This Survey has offline data to be synchronised." | ||||
|     And I should see "You have completed this survey." | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { CoreAjaxWSError } from './ajaxwserror'; | ||||
| import { CoreCaptureError } from './captureerror'; | ||||
| import { CoreNetworkError } from './network-error'; | ||||
| import { CoreSiteError } from './siteerror'; | ||||
| import { CoreErrorWithTitle } from './errorwithtitle'; | ||||
| import { CoreErrorWithOptions } from './errorwithtitle'; | ||||
| import { CoreHttpError } from './httperror'; | ||||
| 
 | ||||
| export const CORE_ERRORS_CLASSES: Type<unknown>[] = [ | ||||
| @ -36,6 +36,6 @@ export const CORE_ERRORS_CLASSES: Type<unknown>[] = [ | ||||
|     CoreSilentError, | ||||
|     CoreSiteError, | ||||
|     CoreWSError, | ||||
|     CoreErrorWithTitle, | ||||
|     CoreErrorWithOptions, | ||||
|     CoreHttpError, | ||||
| ]; | ||||
|  | ||||
| @ -12,20 +12,24 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AlertButton } from '@ionic/angular'; | ||||
| import { CoreError } from './error'; | ||||
| 
 | ||||
| /** | ||||
|  * 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. | ||||
|  * The error also may contain customizable action buttons. | ||||
|  */ | ||||
| export class CoreErrorWithTitle extends CoreError { | ||||
| export class CoreErrorWithOptions extends CoreError { | ||||
| 
 | ||||
|     title?: string; | ||||
|     buttons?: AlertButton[]; | ||||
| 
 | ||||
|     constructor(message?: string, title?: string) { | ||||
|     constructor(message?: string, title?: string, buttons?: AlertButton[]) { | ||||
|         super(message); | ||||
| 
 | ||||
|         this.title = title; | ||||
|         this.buttons = buttons; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -8,9 +8,10 @@ | ||||
|             </ion-button> | ||||
|             <ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description"> | ||||
|                 <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)" | ||||
|                             (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" | ||||
|                             [tabindex]="selected == tab.id ? 0 : -1"> | ||||
|                             <ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon> | ||||
|  | ||||
| @ -7,10 +7,11 @@ | ||||
|         </ion-button> | ||||
|         <ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description"> | ||||
|             <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)" | ||||
|                         (keyup)="tabAction.keyUp(tab.id, $event)" class="{{tab.class}}" [layout]="layout" role="tab" | ||||
|                         [attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id" [tabindex]="selected == tab.id ? 0 : -1"> | ||||
|                         (keyup)="tabAction.keyUp(tab.id, $event)" [layout]="layout" role="tab" [attr.aria-controls]="tab.id" | ||||
|                         [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-label> | ||||
|                             {{ tab.title | translate}} | ||||
|  | ||||
| @ -36,8 +36,9 @@ Feature: Test basic usage of comments in app | ||||
|         | Field description | Test field description | | ||||
|     And I press "Save" | ||||
|     And I close the browser tab opened by the app | ||||
|     When I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Data" in the app | ||||
|     And I close the popup in the app | ||||
| 
 | ||||
|     When I pull to refresh 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 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) | ||||
|     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 switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
| @ -84,14 +85,15 @@ Feature: Test basic usage of comments in app | ||||
|         | Field description | Test field description | | ||||
|     And I press "Save" | ||||
|     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 | ||||
|     Then I press "Add entries" in the app | ||||
|     When I pull to refresh 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 press "Save" in the app | ||||
|     And I press "More" 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 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 | ||||
| @ -100,7 +102,7 @@ Feature: Test basic usage of comments in app | ||||
| 
 | ||||
|     When I press the back button 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 "Synchronise now" 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 | ||||
|     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 "Delete" 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 | ||||
|     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 "Synchronise now" 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 "potato" 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 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 | ||||
| @ -188,7 +190,7 @@ Feature: Test basic usage of comments in app | ||||
| 
 | ||||
|     When I press the back button 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 "Synchronise now" 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 | ||||
|     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 "Delete" 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 | ||||
|     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 "Synchronise now" 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 | ||||
| 
 | ||||
|     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 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 | ||||
| @ -271,7 +273,7 @@ Feature: Test basic usage of comments in app | ||||
| 
 | ||||
|     When I press the back button 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 "Synchronise now" 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 | ||||
|     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 "Delete" 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 | ||||
|     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 "Synchronise now" in the app | ||||
|     And I close the popup in the app | ||||
|  | ||||
| @ -177,7 +177,9 @@ export class CoreCourseProvider { | ||||
|         CorePlatform.resume.subscribe(() => { | ||||
|             // Run the handler the app is open to keep user in online status.
 | ||||
|             setTimeout(() => { | ||||
|                 CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name); | ||||
|                 CoreUtils.ignoreErrors( | ||||
|                     CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name), | ||||
|                 ); | ||||
|             }, 1000); | ||||
|         }); | ||||
| 
 | ||||
|  | ||||
| @ -411,6 +411,8 @@ Feature: Test basic usage of one course in app | ||||
|     And I select "Enrolment methods" from the "jump" singleselect | ||||
|     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 popup in the app | ||||
| 
 | ||||
|     Given I entered the app as "student2" | ||||
|     When I press "Site home" in the app | ||||
|     And I press "Available courses" in the app | ||||
|  | ||||
| @ -148,9 +148,12 @@ Feature: Test basic usage of courses in app | ||||
| 
 | ||||
|     # Grade assignment as teacher | ||||
|     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 | ||||
|     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 | ||||
| 
 | ||||
|     When I press "Needs grading" in the app | ||||
|  | ||||
| @ -26,8 +26,7 @@ Feature: Test basic usage of login in 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 | ||||
|     When I enter the app | ||||
|     And I press the back button in the app | ||||
|     When I launch the app | ||||
|     And I set the field "Your site" to "$WWWROOT" in the app | ||||
|     And I press "Connect to your 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 | ||||
|     When I enter the app | ||||
|     And I log in as "student1" | ||||
|     And I press the user menu button in the app | ||||
|     And I press "Log out" in the app | ||||
|     And I wait the app to restart | ||||
|     When I log out 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 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 | ||||
|     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 | ||||
|     Given I entered the app as "student1" | ||||
|     When I press the user menu button in the app | ||||
|     And I press "Log out" in the app | ||||
|     And I wait the app to restart | ||||
|     When I log out 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 "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 | ||||
|     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 | ||||
|     Given the following config values are set as admin: | ||||
|       | minimumversion | 3.8.1 | tool_mobile | | ||||
|     When I enter 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 | ||||
|     Given the following config values are set as admin: | ||||
|       | minimumversion | 11.0.0 | tool_mobile | | ||||
|  | ||||
| @ -71,7 +71,14 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         // Load the handlers.
 | ||||
|         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) | ||||
|                 .subscribe((handlers) => { | ||||
|  | ||||
| @ -746,11 +746,7 @@ export class CorePushNotificationsProvider { | ||||
|                 CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId()); | ||||
| 
 | ||||
|                 // Insert the device in the local DB.
 | ||||
|                 try { | ||||
|                     await this.registeredDevicesTables[site.getId()].insert(data); | ||||
|                 } catch (err) { | ||||
|                     // Ignore errors.
 | ||||
|                 } | ||||
|                 await CoreUtils.ignoreErrors(this.registeredDevicesTables[site.getId()].insert(data)); | ||||
|             } | ||||
|         } finally { | ||||
|             // Remove pending unregisters for this site.
 | ||||
|  | ||||
| @ -4,8 +4,8 @@ | ||||
|     "appsettings": "App settings", | ||||
|     "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.", | ||||
|     "cannotsyncoffline": "Cannot synchronise offline.", | ||||
|     "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", | ||||
|     "cannotsyncoffline": "Site synchronisation failed because your device is not connected to the internet.", | ||||
|     "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}}", | ||||
|     "changelanguagealert": "Changing the language will restart the app.", | ||||
|     "colorscheme-dark": "Dark", | ||||
| @ -14,6 +14,8 @@ | ||||
|     "colorscheme-system": "System default", | ||||
|     "colorscheme": "Color Scheme", | ||||
|     "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", | ||||
|     "cordovadevicemodel": "Cordova device model", | ||||
|     "cordovadeviceosversion": "Cordova device OS version", | ||||
| @ -35,9 +37,7 @@ | ||||
|     "enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.", | ||||
|     "enablerichtexteditor": "Enable text editor", | ||||
|     "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", | ||||
|     "errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.", | ||||
|     "estimatedfreespace": "Estimated free space", | ||||
|     "filesystemroot": "File system root", | ||||
|     "fontsize": "Text size", | ||||
| @ -54,6 +54,7 @@ | ||||
|     "locationhref": "Web view URL", | ||||
|     "loggedin": "Online", | ||||
|     "loggedoff": "Offline", | ||||
|     "logintosync": "Log in to synchronise", | ||||
|     "navigatorlanguage": "Navigator language", | ||||
|     "navigatoruseragent": "Navigator userAgent", | ||||
|     "networkstatus": "Internet connection status", | ||||
| @ -68,7 +69,9 @@ | ||||
|     "showdownloadoptions": "Show download options", | ||||
|     "siteinfo": "Site info", | ||||
|     "sites": "Sites", | ||||
|     "sitesyncfailed": "Site synchronisation failed", | ||||
|     "spaceusage": "Space usage", | ||||
|     "syncdatasaver": "Data saver: Synchronise only when on Wi-Fi", | ||||
|     "synchronization": "Synchronisation", | ||||
|     "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.", | ||||
|  | ||||
| @ -37,11 +37,20 @@ | ||||
|                         </p> | ||||
|                     </ion-label> | ||||
|                     <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-button> | ||||
|                     </core-button-with-spinner> | ||||
|                 </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> | ||||
|         </core-loading> | ||||
|     </core-split-view> | ||||
|  | ||||
| @ -25,6 +25,11 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ | ||||
| import { CoreSettingsHandlersSource } from '@features/settings/classes/settings-handlers-source'; | ||||
| import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; | ||||
| 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. | ||||
| @ -39,8 +44,13 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     handlers: CoreListItemsManager<CoreSettingsHandlerToDisplay>; | ||||
| 
 | ||||
|     dataSaver = false; | ||||
|     limitedConnection = false; | ||||
|     isOnline = true; | ||||
| 
 | ||||
|     protected siteId: string; | ||||
|     protected sitesObserver: CoreEventObserver; | ||||
|     protected networkObserver: Subscription; | ||||
|     protected isDestroyed = false; | ||||
| 
 | ||||
|     constructor() { | ||||
| @ -53,12 +63,25 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { | ||||
|         this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { | ||||
|             this.refreshData(); | ||||
|         }, 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> { | ||||
|         this.dataSaver = await CoreConfig.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, true); | ||||
| 
 | ||||
|         const pageToOpen = CoreNavigator.getRouteParam('page'); | ||||
| 
 | ||||
|         try { | ||||
| @ -94,7 +117,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { | ||||
|             if (this.isDestroyed) { | ||||
|                 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 { | ||||
|         this.isDestroyed = true; | ||||
|         this.sitesObserver?.off(); | ||||
|         this.sitesObserver.off(); | ||||
|         this.networkObserver.unsubscribe(); | ||||
|         this.handlers.destroy(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -17,37 +17,97 @@ | ||||
| </ion-header> | ||||
| <ion-content class="limited-width"> | ||||
|     <core-loading [hideUntil]="sitesLoaded"> | ||||
|         <ion-list> | ||||
|         <ion-list class="core-sitelist limited-width"> | ||||
|             <ion-item-divider> | ||||
|                 <ion-label> | ||||
|                     <h2>{{ 'core.settings.syncsettings' | translate }}</h2> | ||||
|                 </ion-label> | ||||
|             </ion-item-divider> | ||||
|             <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> | ||||
|                     <p class="item-heading"> | ||||
|                         <core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text> | ||||
|                         {{ 'core.settings.syncdatasaver' | translate }} | ||||
|                     </p> | ||||
|                     <p>{{ site.fullName }}</p> | ||||
|                     <p>{{ site.siteUrlWithoutProtocol }}</p> | ||||
|                 </ion-label> | ||||
|                 <core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end"> | ||||
|                     <ion-button fill="clear" (click)="synchronize(site.id)" [title]="site.siteName" | ||||
|                         [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-toggle slot="end" [(ngModel)]="dataSaver" (ngModelChange)="syncOnlyOnWifiChanged()"> | ||||
|                 </ion-toggle> | ||||
|             </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> | ||||
|     </core-loading> | ||||
| </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> | ||||
|  | ||||
| @ -16,11 +16,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites, CoreSiteBasicInfo } from '@services/sites'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreConfig } from '@services/config'; | ||||
| 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. | ||||
| @ -28,61 +32,101 @@ import { Translate } from '@singletons'; | ||||
| @Component({ | ||||
|     selector: 'page-core-app-settings-synchronization', | ||||
|     templateUrl: 'synchronization.html', | ||||
|     styleUrls: ['../../../login/sitelist.scss'], | ||||
| }) | ||||
| export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     sites: CoreSiteBasicInfo[] = []; | ||||
|     accountsList: CoreAccountsList = { | ||||
|         sameSite: [], | ||||
|         otherSites: [], | ||||
|         count: 0, | ||||
|     }; | ||||
| 
 | ||||
|     sitesLoaded = false; | ||||
|     currentSiteId = ''; | ||||
|     syncOnlyOnWifi = false; | ||||
|     dataSaver = false; | ||||
|     limitedConnection = false; | ||||
|     isOnline = true; | ||||
| 
 | ||||
|     protected isDestroyed = false; | ||||
|     protected sitesObserver: CoreEventObserver; | ||||
|     protected networkObserver: Subscription; | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
|         this.currentSiteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         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); | ||||
|             if (siteEntry) { | ||||
|                 const siteInfo = site.getInfo(); | ||||
|             let siteEntry = siteId === this.accountsList.currentSite?.id | ||||
|                 ? this.accountsList.currentSite | ||||
|                 : undefined; | ||||
| 
 | ||||
|                 siteEntry.siteName = site.getSiteName(); | ||||
|             if (!siteEntry) { | ||||
|                 siteEntry = this.accountsList.sameSite.find((siteEntry) => siteEntry.id === siteId); | ||||
|             } | ||||
| 
 | ||||
|                 if (siteInfo) { | ||||
|                     siteEntry.siteUrl = siteInfo.siteurl; | ||||
|                     siteEntry.fullName = siteInfo.fullname; | ||||
|                 } | ||||
|             if (!siteEntry) { | ||||
|                 this.accountsList.otherSites.some((sites) => { | ||||
|                     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> { | ||||
|         const currentSiteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         try { | ||||
|             this.sites = await CoreSites.getSortedSites(); | ||||
|             this.accountsList = await CoreLoginHelper.getAccountsList(currentSiteId); | ||||
|         } catch { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         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. | ||||
|      */ | ||||
|     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. | ||||
|      */ | ||||
| @ -95,10 +139,20 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy { | ||||
|                 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. | ||||
|      * | ||||
| @ -120,11 +174,12 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.sitesObserver?.off(); | ||||
|         this.sitesObserver.off(); | ||||
|         this.networkObserver.unsubscribe(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -29,6 +29,7 @@ import { CoreCourse } from '@features/course/services/course'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { Observable, Subject } from 'rxjs'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| 
 | ||||
| /** | ||||
|  * 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 hasSyncHandlers = CoreCronDelegate.hasManualSyncHandlers(); | ||||
| 
 | ||||
|         // All these errors should not happen on manual sync because are prevented on UI.
 | ||||
|         if (site.isLoggedOut()) { | ||||
|             // Cannot sync logged out sites.
 | ||||
|             throw new CoreError(Translate.instant('core.settings.cannotsyncloggedout')); | ||||
| @ -286,6 +288,8 @@ export class CoreSettingsHelperProvider { | ||||
| 
 | ||||
|         try { | ||||
|             await syncPromise; | ||||
|         } catch (error) { | ||||
|             throw CoreTextUtils.addTitleToError(error, Translate.instant('core.settings.sitesyncfailed')); | ||||
|         } finally { | ||||
|             delete this.syncPromises[siteId]; | ||||
|         } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| @app @javascript | ||||
| @app @javascript @core_settings | ||||
| Feature: It navigates properly within settings. | ||||
| 
 | ||||
|   Background: | ||||
|  | ||||
							
								
								
									
										150
									
								
								src/core/features/settings/tests/behat/sync.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/core/features/settings/tests/behat/sync.feature
									
									
									
									
									
										Normal 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 | ||||
| @ -1,7 +1,7 @@ | ||||
| :host { | ||||
|     --popover-background: var(--ion-overlay-background-color, var(--ion-background-color, #fff)); | ||||
| 
 | ||||
|     z-index: 99; | ||||
|     z-index: 105; // Main menu is 101. | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     display: none; | ||||
|  | ||||
| @ -62,6 +62,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy | ||||
|     @Output() afterDismiss = new EventEmitter<void>(); | ||||
|     @HostBinding('class.is-active') active = false; | ||||
|     @HostBinding('class.is-popover') popover = false; | ||||
|     @HostBinding('class.backdrop') backdrop = true; | ||||
|     @ViewChild('wrapper') wrapper?: ElementRef<HTMLElement>; | ||||
| 
 | ||||
|     focusStyles?: string; | ||||
|  | ||||
| @ -96,6 +96,7 @@ | ||||
|     "downloading": "Downloading", | ||||
|     "edit": "Edit", | ||||
|     "emptysplit": "This page will appear blank if the left panel is empty or is loading.", | ||||
|     "endonesteptour": "Got it", | ||||
|     "error": "Error", | ||||
|     "errorchangecompletion": "An error occurred while changing the completion status. 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.", | ||||
|     "erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.", | ||||
|     "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.", | ||||
|     "errorsomethingwrong": "Something went wrong. 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.", | ||||
|     "errorurlschemeinvalidscheme": "This URL is meant to be used in another app: {{$a}}.", | ||||
| @ -248,8 +251,8 @@ | ||||
|     "resources": "Resources", | ||||
|     "restore": "Restore", | ||||
|     "restricted": "Restricted", | ||||
|     "retry": "Retry", | ||||
|     "resume": "Resume", | ||||
|     "retry": "Retry", | ||||
|     "save": "Save", | ||||
|     "savechanges": "Save changes", | ||||
|     "scanqr": "Scan QR code", | ||||
| @ -328,11 +331,10 @@ | ||||
|     "user": "User", | ||||
|     "userdeleted": "This user account has been deleted", | ||||
|     "userdetails": "User details", | ||||
|     "usernotfullysetup": "User not fully set-up", | ||||
|     "usernologin": "Authentication has been revoked for this account", | ||||
|     "usersuspended": "Registration suspended", | ||||
|     "endonesteptour": "Got it", | ||||
|     "usernotfullysetup": "User not fully set-up", | ||||
|     "users": "Users", | ||||
|     "usersuspended": "Registration suspended", | ||||
|     "view": "View", | ||||
|     "viewcode": "View code", | ||||
|     "vieweditor": "View editor", | ||||
| @ -351,8 +353,8 @@ | ||||
|     "year": "year", | ||||
|     "years": "years", | ||||
|     "yes": "Yes", | ||||
|     "youreoffline": "You are offline", | ||||
|     "youreonline": "You are back online", | ||||
|     "youreoffline": "Your device is offline", | ||||
|     "youreonline": "Your device is back online", | ||||
|     "zoomin": "Zoom In", | ||||
|     "zoomout": "Zoom Out" | ||||
| } | ||||
|  | ||||
| @ -30,7 +30,7 @@ import { CoreDatabaseTable } from '@classes/database/database-table'; | ||||
| import { CorePromisedValue } from '@classes/promised-value'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| 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. | ||||
| @ -644,10 +644,10 @@ export class CoreAppProvider { | ||||
|      * Set value of forceOffline flag. If true, the app will think the device is offline. | ||||
|      * | ||||
|      * @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 { | ||||
|         CoreNetwork.setForceOffline(value); | ||||
|         CoreNetwork.setForceConnectionMode(value ? CoreNetworkConnection.NONE : CoreNetworkConnection.WIFI); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/database/cron'; | ||||
| import { asyncInstance } from '../utils/async-instance'; | ||||
| @ -81,10 +81,11 @@ export class CoreCronDelegateService { | ||||
|     protected async checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> { | ||||
|         if (!this.handlers[name] || !this.handlers[name].execute) { | ||||
|             // Invalid handler.
 | ||||
|             const message = `Cannot execute handler because is invalid: ${name}`; | ||||
|             this.logger.debug(message); | ||||
|             this.logger.debug(`Cannot execute cron job because is invalid: ${name}`); | ||||
| 
 | ||||
|             throw new CoreError(message); | ||||
|             throw new CoreError( | ||||
|                 Translate.instant('core.errorsomethingwrong') + '<br>' + Translate.instant('core.errorsitesupport'), | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const usesNetwork = this.handlerUsesNetwork(name); | ||||
| @ -92,11 +93,10 @@ export class CoreCronDelegateService { | ||||
| 
 | ||||
|         if (usesNetwork && !CoreNetwork.isOnline()) { | ||||
|             // Offline, stop executing.
 | ||||
|             const message = `Cannot execute handler because device is offline: ${name}`; | ||||
|             this.logger.debug(message); | ||||
|             this.logger.debug(`Cron job failed because your device is not connected to the internet: ${name}`); | ||||
|             this.stopHandler(name); | ||||
| 
 | ||||
|             throw new CoreError(message); | ||||
|             throw new CoreError(Translate.instant('core.settings.cannotsyncoffline')); | ||||
|         } | ||||
| 
 | ||||
|         if (isSync) { | ||||
| @ -105,11 +105,10 @@ export class CoreCronDelegateService { | ||||
| 
 | ||||
|             if (syncOnlyOnWifi && !CoreNetwork.isWifi()) { | ||||
|                 // Cannot execute in this network connection, retry soon.
 | ||||
|                 const message = `Cannot execute handler because device is using limited connection: ${name}`; | ||||
|                 this.logger.debug(message); | ||||
|                 this.logger.debug(`Cron job failed because your device has a limited internet connection: ${name}`); | ||||
|                 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 { | ||||
|                 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())); | ||||
| 
 | ||||
| @ -127,11 +126,10 @@ export class CoreCronDelegateService { | ||||
|                 return; | ||||
|             } catch (error) { | ||||
|                 // Handler call failed. Retry soon.
 | ||||
|                 const message = `Execution of handler '${name}' failed.`; | ||||
|                 this.logger.error(message, error); | ||||
|                 this.logger.error(`Cron job '${name}' failed.`, error); | ||||
|                 this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL); | ||||
| 
 | ||||
|                 throw new CoreError(message); | ||||
|                 throw error; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|  | ||||
| @ -18,6 +18,17 @@ import { Network } from '@ionic-native/network/ngx'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 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. | ||||
|  */ | ||||
| @ -28,9 +39,21 @@ export class CoreNetworkService extends Network { | ||||
| 
 | ||||
|     protected connectObservable = new Subject<'connected'>(); | ||||
|     protected disconnectObservable = new Subject<'disconnected'>(); | ||||
|     protected forceOffline = false; | ||||
|     protected forceConnectionMode?: CoreNetworkConnection; | ||||
|     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. | ||||
|      */ | ||||
| @ -38,20 +61,25 @@ export class CoreNetworkService extends Network { | ||||
|         this.checkOnline(); | ||||
| 
 | ||||
|         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(); | ||||
|             }); | ||||
|         } else { | ||||
|             // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|             (<any> window).Connection = { | ||||
|                 UNKNOWN: 'unknown', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 ETHERNET: 'ethernet', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 WIFI: 'wifi', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_2G: '2g', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_3G: '3g', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_4G: '4g', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL: 'cellular', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 NONE: 'none', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 UNKNOWN: CoreNetworkConnection.UNKNOWN, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 ETHERNET: CoreNetworkConnection.ETHERNET, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 WIFI: CoreNetworkConnection.WIFI, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_2G: CoreNetworkConnection.CELL_2G, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_3G: CoreNetworkConnection.CELL_3G, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL_4G: CoreNetworkConnection.CELL_4G, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 CELL: CoreNetworkConnection.CELL, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|                 NONE: CoreNetworkConnection.NONE, // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|             }; | ||||
| 
 | ||||
|             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. | ||||
|      */ | ||||
|     setForceOffline(value: boolean): void { | ||||
|         this.forceOffline = !!value; | ||||
|     setForceConnectionMode(value: CoreNetworkConnection): void { | ||||
|         this.forceConnectionMode = value; | ||||
|         this.fireObservable(); | ||||
|     } | ||||
| 
 | ||||
| @ -89,20 +118,15 @@ export class CoreNetworkService extends Network { | ||||
|      * @return Whether the app is online. | ||||
|      */ | ||||
|     checkOnline(): void { | ||||
|         if (this.forceOffline) { | ||||
|         if (this.forceConnectionMode === CoreNetworkConnection.NONE) { | ||||
|             this.online = false; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!CorePlatform.isMobile()) { | ||||
|             this.online = navigator.onLine; | ||||
|         const type = this.connectionType; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let online = this.type !== null && this.type != this.Connection.NONE && | ||||
|             this.type != this.Connection.UNKNOWN; | ||||
|         let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN; | ||||
| 
 | ||||
|         // Double check we are not online because we cannot rely 100% in Cordova APIs.
 | ||||
|         if (!online && navigator.onLine) { | ||||
| @ -123,6 +147,7 @@ export class CoreNetworkService extends Network { | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an observable to notify when the app is connected. | ||||
|      * It will also be fired when connection type changes. | ||||
|      * | ||||
|      * @return Observable. | ||||
|      */ | ||||
| @ -143,12 +168,11 @@ export class CoreNetworkService extends Network { | ||||
|      * Fires the correct observable depending on the connection status. | ||||
|      */ | ||||
|     protected fireObservable(): void { | ||||
|         const previousOnline = this.online; | ||||
| 
 | ||||
|         this.checkOnline(); | ||||
|         if (this.online && !previousOnline) { | ||||
| 
 | ||||
|         if (this.online) { | ||||
|             this.connectObservable.next('connected'); | ||||
|         } else if (!this.online && previousOnline) { | ||||
|         } else { | ||||
|             this.disconnectObservable.next('disconnected'); | ||||
|         } | ||||
|     } | ||||
| @ -159,18 +183,16 @@ export class CoreNetworkService extends Network { | ||||
|      * @return Whether the device uses a limited connection. | ||||
|      */ | ||||
|     isNetworkAccessLimited(): boolean { | ||||
|         if (!CorePlatform.isMobile()) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const limited = [ | ||||
|             this.Connection.CELL_2G, | ||||
|             this.Connection.CELL_3G, | ||||
|             this.Connection.CELL_4G, | ||||
|             this.Connection.CELL, | ||||
|         const limited: CoreNetworkConnection[] = [ | ||||
|             CoreNetworkConnection.CELL_2G, | ||||
|             CoreNetworkConnection.CELL_3G, | ||||
|             CoreNetworkConnection.CELL_4G, | ||||
|             CoreNetworkConnection.CELL, | ||||
|         ]; | ||||
| 
 | ||||
|         return limited.indexOf(this.type) > -1; | ||||
|         const type = this.connectionType; | ||||
| 
 | ||||
|         return limited.indexOf(type) > -1; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -51,7 +51,7 @@ import { CoreRedirectPayload } from './navigator'; | ||||
| import { CoreSitesFactory } from './sites-factory'; | ||||
| import { CoreText } from '@singletons/text'; | ||||
| 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 { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; | ||||
| import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; | ||||
| @ -870,7 +870,7 @@ export class CoreSitesProvider { | ||||
| 
 | ||||
|         const siteUrlAllowed = await CoreLoginHelper.isSiteUrlAllowed(site.getURL(), false); | ||||
|         if (!siteUrlAllowed) { | ||||
|             throw new CoreErrorWithTitle(Translate.instant('core.login.sitenotallowed')); | ||||
|             throw new CoreErrorWithOptions(Translate.instant('core.login.sitenotallowed')); | ||||
|         } | ||||
| 
 | ||||
|         this.currentSite = site; | ||||
| @ -1185,6 +1185,7 @@ export class CoreSitesProvider { | ||||
|                     siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename, | ||||
|                     avatar: siteInfo?.userpictureurl, | ||||
|                     siteHomeId: siteInfo?.siteid || 1, | ||||
|                     loggedOut: !!site.loggedOut, | ||||
|                 }; | ||||
|                 formattedSites.push(basicInfo); | ||||
|             } | ||||
| @ -1277,7 +1278,7 @@ export class CoreSitesProvider { | ||||
|     /** | ||||
|      * 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. | ||||
|      */ | ||||
|     async logout(options: CoreSitesLogoutOptions = {}): Promise<void> { | ||||
| @ -1923,6 +1924,7 @@ export type CoreSiteBasicInfo = { | ||||
|     avatar?: string; // User's avatar.
 | ||||
|     badge?: number; // Badge to display in the site.
 | ||||
|     siteHomeId?: number; // Site home ID.
 | ||||
|     loggedOut: boolean; // If Site is logged out.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -46,7 +46,6 @@ import { CoreViewerImageComponent } from '@features/viewer/components/image/imag | ||||
| import { CoreFormFields, CoreForms } from '../../singletons/form'; | ||||
| import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition'; | ||||
| import { CoreZoomLevel } from '@features/settings/services/settings-helper'; | ||||
| import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle'; | ||||
| import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { NavigationStart } from '@angular/router'; | ||||
| @ -1345,17 +1344,22 @@ export class CoreDomUtilsProvider { | ||||
| 
 | ||||
|         const alertOptions: AlertOptions = { | ||||
|             message: message, | ||||
|             buttons: [Translate.instant('core.ok')], | ||||
|         }; | ||||
| 
 | ||||
|         if (this.isNetworkError(message, 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; | ||||
|         } else { | ||||
|             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); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -26,6 +26,7 @@ import { CoreFileHelper } from '@services/file-helper'; | ||||
| import { CoreDomUtils } from './dom'; | ||||
| import { CoreText } from '@singletons/text'; | ||||
| import { CoreUrl } from '@singletons/url'; | ||||
| import { AlertButton } from '@ionic/angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Different type of errors the app can treat. | ||||
| @ -37,6 +38,8 @@ export type CoreTextErrorObject = { | ||||
|     body?: string; | ||||
|     debuginfo?: string; | ||||
|     backtrace?: string; | ||||
|     title?: string; | ||||
|     buttons?: AlertButton[]; | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
| @ -149,6 +152,27 @@ export class CoreTextUtilsProvider { | ||||
|         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. | ||||
|      * | ||||
|  | ||||
| @ -18,9 +18,6 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton, NgZone } from '@singletons'; | ||||
| 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. | ||||
|  */ | ||||
| @ -331,13 +328,14 @@ export class TestingBehatDomUtilsService { | ||||
|         } | ||||
| 
 | ||||
|         // Get containers until one blocks other views.
 | ||||
|         containers.find(container => { | ||||
|         containers.some(container => { | ||||
|             if (container.tagName === 'ION-TOAST') { | ||||
|                 container = container.shadowRoot?.querySelector('.toast-container') || 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; | ||||
|  | ||||
| @ -25,9 +25,8 @@ import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; | ||||
| import { CoreLoadingComponent } from '@components/loading/loading'; | ||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dashboard'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreSites, CoreSitesProvider } from '@services/sites'; | ||||
| 
 | ||||
| /** | ||||
|  * Behat runtime servive with public API. | ||||
| @ -53,6 +52,10 @@ export class TestingBehatRuntimeService { | ||||
|         return CorePushNotifications.instance; | ||||
|     } | ||||
| 
 | ||||
|     get sites(): CoreSitesProvider { | ||||
|         return CoreSites.instance; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init behat functions and set options like skipping onboarding. | ||||
|      * | ||||
| @ -348,23 +351,17 @@ export class TestingBehatRuntimeService { | ||||
|         this.log('Action - pullToRefresh'); | ||||
| 
 | ||||
|         try { | ||||
|             // TODO We should generalize this to work with other pages. It's not possible to use
 | ||||
|             // an IonRefresher instance because it doesn't expose any methods to trigger refresh,
 | ||||
|             // so we'll have to find another way.
 | ||||
| 
 | ||||
|             const dashboard = this.getAngularInstance<CoreCoursesDashboardPage>( | ||||
|                 'page-core-courses-dashboard', | ||||
|                 'CoreCoursesDashboardPage', | ||||
|             // 'el' is protected, but there's no other way to trigger refresh programatically.
 | ||||
|             const ionRefresher = this.getAngularInstance<{ el: HTMLIonRefresherElement }>( | ||||
|                 'ion-refresher', | ||||
|                 'IonRefresher', | ||||
|             ); | ||||
| 
 | ||||
|             if (!dashboard) { | ||||
|                 return 'ERROR: It\'s not possible to pull to refresh the current page ' | ||||
|                     + '(the dashboard page is the only one supported at the moment).'; | ||||
|             if (!ionRefresher) { | ||||
|                 return 'ERROR: It\'s not possible to pull to refresh the current page.'; | ||||
|             } | ||||
| 
 | ||||
|             await new Promise(resolve => { | ||||
|                 dashboard.refreshDashboard({ complete: resolve } as IonRefresher); | ||||
|             }); | ||||
|             ionRefresher.el.dispatchEvent(new CustomEvent('ionRefresh')); | ||||
| 
 | ||||
|             return 'OK'; | ||||
|         } catch (error) { | ||||
|  | ||||
| @ -25,9 +25,7 @@ Feature: It navigates properly using deep links. | ||||
| 
 | ||||
|   Scenario: Receive a push notification | ||||
|     Given I entered the app as "student2" | ||||
|     When I press the user menu button in the app | ||||
|     And I press "Log out" in the app | ||||
|     And I wait the app to restart | ||||
|     When I log out in the app | ||||
|     And I press "Add" 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 | ||||
| @ -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 | ||||
|     Given I entered the app as "student2" | ||||
|     When I press the user menu button in the app | ||||
|     And I press "Log out" in the app | ||||
|     And I wait the app to restart | ||||
|     When I log out in the app | ||||
|     And I open a custom link in the app for: | ||||
|       | forum      | | ||||
|       | Test forum | | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user