diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local-moodleappbehat/tests/behat/behat_app.php index f4d3946ed..b21f80134 100644 --- a/local-moodleappbehat/tests/behat/behat_app.php +++ b/local-moodleappbehat/tests/behat/behat_app.php @@ -94,7 +94,7 @@ class behat_app extends behat_app_helper { public function i_wait_the_app_to_restart() { // Wait window to reload. $this->spin(function() { - $result = $this->evaluate_script("return !window.behat;"); + $result = $this->js("return !window.behat;"); if (!$result) { throw new DriverException('Window is not reloading properly.'); @@ -121,7 +121,7 @@ class behat_app extends behat_app_helper { $containerName = json_encode($containerName); $this->spin(function() use ($not, $locator, $containerName) { - $result = $this->evaluate_script("return window.behat.find($locator, $containerName);"); + $result = $this->js("return window.behat.find($locator, $containerName);"); if ($not && $result === 'OK') { throw new DriverException('Error, found an item that should not be found'); @@ -147,7 +147,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->evaluate_script("return window.behat.scrollTo($locator);"); + $result = $this->js("return window.behat.scrollTo($locator);"); if ($result !== 'OK') { throw new DriverException('Error finding item - ' . $result); @@ -170,7 +170,7 @@ class behat_app extends behat_app_helper { */ public function i_load_more_items_in_the_app(bool $not = false) { $this->spin(function() use ($not) { - $result = $this->evaluate_async_script('return window.behat.loadMoreItems();'); + $result = $this->js('return await window.behat.loadMoreItems();'); if ($not && $result !== 'ERROR: All items are already loaded.') { throw new DriverException('It should not have been possible to load more items'); @@ -195,7 +195,7 @@ class behat_app extends behat_app_helper { public function i_swipe_in_the_app(string $direction) { $method = 'swipe' . ucwords($direction); - $this->evaluate_script("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); + $this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); $this->wait_for_pending_js(); @@ -214,7 +214,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator, $not) { - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); switch ($result) { case 'YES': @@ -318,7 +318,7 @@ class behat_app extends behat_app_helper { $this->login($username); } - $mycoursesfound = $this->evaluate_script("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); + $mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); if ($mycoursesfound !== 'OK') { // My courses not present enter from Dashboard. @@ -370,7 +370,7 @@ class behat_app extends behat_app_helper { */ public function i_press_the_standard_button_in_the_app(string $button) { $this->spin(function() use ($button) { - $result = $this->evaluate_script("return window.behat.pressStandard('$button');"); + $result = $this->js("return await window.behat.pressStandard('$button');"); if ($result !== 'OK') { throw new DriverException('Error pressing standard button - ' . $result); @@ -408,7 +408,7 @@ class behat_app extends behat_app_helper { ], ]); - $this->evaluate_script("return window.pushNotifications.notificationClicked($notification)"); + $this->js("window.behat.notificationClicked($notification)"); $this->wait_for_pending_js(); } @@ -494,7 +494,7 @@ class behat_app extends behat_app_helper { */ public function i_close_the_popup_in_the_app() { $this->spin(function() { - $result = $this->evaluate_script("return window.behat.closePopup();"); + $result = $this->js("return window.behat.closePopup();"); if ($result !== 'OK') { throw new DriverException('Error closing popup - ' . $result); @@ -532,7 +532,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->evaluate_script("return window.behat.press($locator);"); + $result = $this->js("return await window.behat.press($locator);"); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -562,14 +562,14 @@ class behat_app extends behat_app_helper { $this->spin(function() use ($selectedtext, $selected, $locator) { // Don't do anything if the item is already in the expected state. - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); if ($result === $selected) { return true; } // Press item. - $result = $this->evaluate_script("return window.behat.press($locator);"); + $result = $this->js("return await window.behat.press($locator);"); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -578,7 +578,7 @@ class behat_app extends behat_app_helper { // Check that it worked as expected. $this->wait_for_pending_js(); - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); switch ($result) { case 'YES': @@ -612,7 +612,7 @@ class behat_app extends behat_app_helper { $value = addslashes_js($value); $this->spin(function() use ($field, $value) { - $result = $this->evaluate_script("return window.behat.setField(\"$field\", \"$value\");"); + $result = $this->js("return await window.behat.setField(\"$field\", \"$value\");"); if ($result !== 'OK') { throw new DriverException('Error setting field - ' . $result); @@ -624,6 +624,21 @@ class behat_app extends behat_app_helper { $this->wait_for_pending_js(); } + /** + * Fills a form with field/value data. + * + * @Given /^I set the following fields to these values in the app:$/ + * @param TableNode $data + */ + public function i_set_the_following_fields_to_these_values_in_the_app(TableNode $data) { + $datahash = $data->getRowsHash(); + + // The action depends on the field type. + foreach ($datahash as $locator => $value) { + $this->i_set_the_field_in_the_app($locator, $value); + } + } + /** * Checks that the current header stripe in the app contains the expected text. * @@ -636,7 +651,7 @@ class behat_app extends behat_app_helper { */ public function the_header_should_be_in_the_app(string $text) { $this->spin(function() use ($text) { - $result = $this->evaluate_script('return window.behat.getHeader();'); + $result = $this->js('return window.behat.getHeader();'); if (substr($result, 0, 3) !== 'OK:') { throw new DriverException('Error getting header - ' . $result); @@ -717,25 +732,8 @@ class behat_app extends behat_app_helper { * @When I run cron tasks in the app */ public function i_run_cron_tasks_in_the_app() { - $session = $this->getSession(); - - // Force cron tasks execution and wait until they are completed. - $operationid = random_string(); - - $session->executeScript( - "cronProvider.forceSyncExecution().then(() => { window['behat_{$operationid}_completed'] = true; });" - ); - $this->spin( - function() use ($session, $operationid) { - return $session->evaluateScript("window['behat_{$operationid}_completed'] || false"); - }, - false, - 60, - new ExpectationException('Forced cron tasks in the app took too long to complete', $session) - ); - - // Trigger Angular change detection. - $this->trigger_angular_change_detection(); + $this->js('await window.behat.forceSyncExecution()'); + $this->wait_for_pending_js(); } /** @@ -744,28 +742,8 @@ class behat_app extends behat_app_helper { * @When I wait loading to finish in the app */ public function i_wait_loading_to_finish_in_the_app() { - $session = $this->getSession(); - - $this->spin( - function() use ($session) { - $this->trigger_angular_change_detection(); - - $nodes = $this->find_all('css', 'core-loading ion-spinner'); - - foreach ($nodes as $node) { - if (!$node->isVisible()) { - continue; - } - - return false; - } - - return true; - }, - false, - 60, - new ExpectationException('"Loading took too long to complete', $session) - ); + $this->js('await window.behat.waitLoadingToFinish()'); + $this->wait_for_pending_js(); } /** @@ -786,7 +764,7 @@ class behat_app extends behat_app_helper { $this->getSession()->switchToWindow($names[1]); } - $this->execute_script('window.close();'); + $this->js('window.close();'); $this->getSession()->switchToWindow($names[0]); } @@ -798,7 +776,7 @@ class behat_app extends behat_app_helper { * @throws DriverException If the navigator.online mode is not available */ public function i_switch_offline_mode(string $offline) { - $this->execute_script("appProvider.setForceOffline($offline);"); + $this->js("window.behat.network.setForceOffline($offline);"); } } diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local-moodleappbehat/tests/behat/behat_app_helper.php index 57134b662..b929e3402 100644 --- a/local-moodleappbehat/tests/behat/behat_app_helper.php +++ b/local-moodleappbehat/tests/behat/behat_app_helper.php @@ -318,7 +318,7 @@ class behat_app_helper extends behat_base { $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; $initOptions->configOverrides = $this->appconfig; - $this->execute_script('window.behatInit(' . json_encode($initOptions) . ');'); + $this->js('window.behatInit(' . json_encode($initOptions) . ');'); } catch (Exception $error) { throw new DriverException('Moodle App not running or not running on Automated mode.'); } @@ -433,28 +433,28 @@ class behat_app_helper extends behat_base { } } - /** - * Trigger Angular change detection. - */ - protected function trigger_angular_change_detection() { - $this->getSession()->executeScript('ngZone.run(() => {});'); - } - - /** - * Evaluate a script that returns a Promise. + * Evaluate and execute scripts checking for promises if needed. * * @param string $script * @return mixed Resolved promise result. */ - protected function evaluate_async_script(string $script) { - $script = preg_replace('/^return\s+/', '', $script); - $script = preg_replace('/;$/', '', $script); + protected function js(string $script) { + $scriptnoreturn = preg_replace('/^return\s+/', '', $script); + $scriptnoreturn = preg_replace('/;$/', '', $scriptnoreturn); + + if (!preg_match('/^await\s+/', $scriptnoreturn)) { + // No async. + return $this->evaluate_script($script); + } + + $script = preg_replace('/^await\s+/', '', $scriptnoreturn); + $start = microtime(true); $promisevariable = 'PROMISE_RESULT_' . time(); - $timeout = self::get_timeout(); + $timeout = self::get_extended_timeout(); - $this->evaluate_script("Promise.resolve($script) + $res = $this->evaluate_script("Promise.resolve($script) .then(result => window.$promisevariable = result) .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);"); @@ -463,6 +463,7 @@ class behat_app_helper extends behat_base { throw new DriverException("Async script not resolved after $timeout seconds"); } + // 0.1 seconds. usleep(100000); } while (!$this->evaluate_script("return '$promisevariable' in window;")); @@ -522,7 +523,7 @@ class behat_app_helper extends behat_base { $successXPath = '//page-core-mainmenu'; } - $this->handle_url_and_wait_page_to_load($url, $successXPath); + $this->handle_url($url, $successXPath); } /** @@ -537,7 +538,7 @@ class behat_app_helper extends behat_base { $urlscheme = $this->get_mobile_url_scheme(); $url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); - $this->handle_url_and_wait_page_to_load($url); + $this->handle_url($url); } /** @@ -546,11 +547,13 @@ class behat_app_helper extends behat_base { * @param string $customurl To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. */ - protected function handle_url_and_wait_page_to_load(string $customurl, string $successXPath = '') { + protected function handle_url(string $customurl, string $successXPath = '') { // Instead of using evaluate_async_script, we wait for the path to load. - $this->evaluate_script("return window.behat.handleCustomURL('$customurl')"); + $result = $this->js("return await window.behat.handleCustomURL('$customurl');"); - $this->wait_for_pending_js(); + if ($result !== 'OK') { + throw new DriverException('Error handling url - ' . $result); + } if (!empty($successXPath)) { // Wait until the page appears. @@ -562,10 +565,9 @@ class behat_app_helper extends behat_base { } throw new DriverException('Moodle App custom URL page not loaded'); }, false, 30); - - // Wait for JS to finish as well. - $this->wait_for_pending_js(); } + + $this->wait_for_pending_js(); } /** diff --git a/src/addons/calendar/pages/day/day.page.ts b/src/addons/calendar/pages/day/day.page.ts index 5efb9dfa2..b7d63ed0e 100644 --- a/src/addons/calendar/pages/day/day.page.ts +++ b/src/addons/calendar/pages/day/day.page.ts @@ -33,7 +33,7 @@ import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { AddonCalendarFilterComponent } from '../../components/filter/filter'; import moment from 'moment'; -import { Network, NgZone } from '@singletons'; +import { NgZone } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -180,7 +180,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { ); // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index 95c35dccd..b448e9280 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -32,7 +32,7 @@ import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreCourse } from '@features/course/services/course'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreGroups } from '@services/groups'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { Subscription } from 'rxjs'; import { CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; @@ -123,7 +123,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { ); // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/calendar/pages/index/index.page.ts b/src/addons/calendar/pages/index/index.page.ts index cac6d7fde..e35a44bcf 100644 --- a/src/addons/calendar/pages/index/index.page.ts +++ b/src/addons/calendar/pages/index/index.page.ts @@ -23,7 +23,7 @@ import { AddonCalendar, AddonCalendarProvider } from '../../services/calendar'; import { AddonCalendarOffline } from '../../services/calendar-offline'; import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calendar-helper'; -import { Network, NgZone } from '@singletons'; +import { NgZone } from '@singletons'; import { Subscription } from 'rxjs'; import { CoreEnrolledCourseData } from '@features/courses/services/courses'; import { ActivatedRoute, Params } from '@angular/router'; @@ -153,7 +153,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { ); // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/messages/messages.module.ts b/src/addons/messages/messages.module.ts index 5616c65e6..80e3875d8 100644 --- a/src/addons/messages/messages.module.ts +++ b/src/addons/messages/messages.module.ts @@ -32,7 +32,8 @@ import { CorePushNotificationsDelegate } from '@features/pushnotifications/servi import { AddonMessagesPushClickHandler } from './services/handlers/push-click'; import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-send-message'; -import { Network, NgZone } from '@singletons'; +import { NgZone } from '@singletons'; +import { CoreNetwork } from '@services/network'; import { AddonMessagesSync, AddonMessagesSyncProvider } from './services/messages-sync'; import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron'; import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; @@ -86,7 +87,7 @@ const preferencesRoutes: Routes = [ CoreUserDelegate.registerHandler(AddonMessagesSendMessageUserHandler.instance); // Sync some discussions when device goes online. - Network.onConnect().subscribe(() => { + CoreNetwork.onConnect().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { AddonMessagesSync.syncAllDiscussions(undefined, true); diff --git a/src/addons/messages/pages/discussion/discussion.html b/src/addons/messages/pages/discussion/discussion.html index bb9cc05ff..f5c3e8dba 100644 --- a/src/addons/messages/pages/discussion/discussion.html +++ b/src/addons/messages/pages/discussion/discussion.html @@ -81,47 +81,16 @@ - - - -
- -
{{ members[message.useridfrom].fullname }}
-
-
- {{ message.useridfrom == currentUserId - ? ('addon.messages.you' | translate) - : members[message.useridfrom].fullname }} -
- - -
- -
-
- {{ message.timecreated | coreFormatDate: "strftimetime" }} - - - - - - - -
-
+ + + [message]="'addon.messages.nomessagesfound' | translate"> + diff --git a/src/addons/messages/pages/discussion/discussion.page.ts b/src/addons/messages/pages/discussion/discussion.page.ts index 4f009de55..17cca33e9 100644 --- a/src/addons/messages/pages/discussion/discussion.page.ts +++ b/src/addons/messages/pages/discussion/discussion.page.ts @@ -37,7 +37,6 @@ import { CoreApp } from '@services/app'; import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading'; import { Md5 } from 'ts-md5/dist/md5'; import moment from 'moment'; -import { CoreAnimations } from '@components/animations'; import { CoreError } from '@classes/errors/error'; import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; @@ -53,7 +52,6 @@ import { CoreDom } from '@singletons/dom'; @Component({ selector: 'page-addon-messages-discussion', templateUrl: 'discussion.html', - animations: [CoreAnimations.SLIDE_IN_OUT], styleUrls: ['discussion.scss'], }) export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterViewInit { @@ -305,7 +303,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView } else { if (this.userId) { // Fake the user member info. - promises.push(CoreUser.getProfile(this.userId!).then(async (user) => { + promises.push(CoreUser.getProfile(this.userId).then(async (user) => { this.otherMember = { id: user.id, fullname: user.fullname, @@ -524,7 +522,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView return; } - const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine')) + const messages = Array.from(this.hostElement.querySelectorAll('core-message:not(.is-mine)')) .slice(-this.newMessages) .reverse(); @@ -555,7 +553,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView // Try to get the conversationId if we don't have it. if (!conversationId && userId) { try { - if (userId == this.currentUserId && AddonMessages.isSelfConversationEnabled()) { + if (userId === this.currentUserId && AddonMessages.isSelfConversationEnabled()) { fallbackConversation = await AddonMessages.getSelfConversation(); } else { fallbackConversation = await AddonMessages.getConversationBetweenUsers(userId, undefined, true); @@ -563,7 +561,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView conversationId = fallbackConversation.id; } catch (error) { // Probably conversation does not exist or user is offline. Try to load offline messages. - this.isSelf = userId == this.currentUserId; + this.isSelf = userId === this.currentUserId; const messages = await AddonMessagesOffline.getMessages(userId); @@ -584,11 +582,15 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView } } + if (!conversationId) { + return false; + } + // Retrieve the conversation. Invalidate data first to get the right unreadcount. - await AddonMessages.invalidateConversation(conversationId!); + await AddonMessages.invalidateConversation(conversationId); try { - this.conversation = await AddonMessages.getConversation(conversationId!, undefined, true); + this.conversation = await AddonMessages.getConversation(conversationId, undefined, true); } catch (error) { // Get conversation failed, use the fallback one if we have it. if (fallbackConversation) { @@ -947,7 +949,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView message: AddonMessagesConversationMessageFormatted, index: number, ): Promise { - const canDeleteAll = this.conversation && this.conversation.candeletemessagesforallusers; const langKey = message.pending || canDeleteAll || this.isSelf ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation'; @@ -1099,7 +1100,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView */ scrollToFirstUnreadMessage(): void { if (this.newMessages > 0) { - const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine')); + const messages = Array.from(this.hostElement.querySelectorAll('core-message:not(.is-mine)')); CoreDom.scrollToElement(messages[messages.length - this.newMessages]); } diff --git a/src/addons/messages/tests/behat/basic_usage.feature b/src/addons/messages/tests/behat/basic_usage.feature index c84ed18fd..0fcc067db 100755 --- a/src/addons/messages/tests/behat/basic_usage.feature +++ b/src/addons/messages/tests/behat/basic_usage.feature @@ -106,7 +106,6 @@ Feature: Test basic usage of messages in app And I should find "hi" in the app And I should find "byee" in the app - # TODO Fix this test in all Moodle versions Scenario: User profile: send message, add/remove contact Given I entered the app as "teacher1" When I press "Messages" in the app diff --git a/src/addons/mod/chat/components/users-modal/users-modal.ts b/src/addons/mod/chat/components/users-modal/users-modal.ts index 73782e491..3a8e0b435 100644 --- a/src/addons/mod/chat/components/users-modal/users-modal.ts +++ b/src/addons/mod/chat/components/users-modal/users-modal.ts @@ -16,7 +16,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { ModalController, Network, NgZone } from '@singletons'; +import { ModalController, NgZone } from '@singletons'; import { Subscription } from 'rxjs'; import { AddonModChat, AddonModChatUser } from '../../services/chat'; @@ -42,7 +42,7 @@ export class AddonModChatUsersModalComponent implements OnInit, OnDestroy { constructor() { this.isOnline = CoreNetwork.isOnline(); this.currentUserId = CoreSites.getCurrentSiteUserId(); - this.onlineSubscription = Network.onChange().subscribe(() => { + this.onlineSubscription = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/mod/chat/pages/chat/chat.html b/src/addons/mod/chat/pages/chat/chat.html index cc3112b5b..dafc0260e 100644 --- a/src/addons/mod/chat/pages/chat/chat.html +++ b/src/addons/mod/chat/pages/chat/chat.html @@ -81,27 +81,10 @@ - - - -

- - -
{{ message.userfullname }}
-

- -
- - -
-
- {{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }} -
-
+ + diff --git a/src/addons/mod/chat/pages/chat/chat.ts b/src/addons/mod/chat/pages/chat/chat.ts index 47bde10f6..0fce55789 100644 --- a/src/addons/mod/chat/pages/chat/chat.ts +++ b/src/addons/mod/chat/pages/chat/chat.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; -import { CoreAnimations } from '@components/animations'; import { CoreSendMessageFormComponent } from '@components/send-message-form/send-message-form'; import { CanLeave } from '@guards/can-leave'; import { IonContent } from '@ionic/angular'; @@ -23,7 +22,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; import { AddonModChatUsersModalComponent, AddonModChatUsersModalResult } from '../../components/users-modal/users-modal'; @@ -36,7 +35,6 @@ import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services @Component({ selector: 'page-addon-mod-chat-chat', templateUrl: 'chat.html', - animations: [CoreAnimations.SLIDE_IN_OUT], styleUrls: ['chat.scss'], }) export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { @@ -67,7 +65,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { constructor() { this.currentUserId = CoreSites.getCurrentSiteUserId(); this.isOnline = CoreNetwork.isOnline(); - this.onlineSubscription = Network.onChange().subscribe(() => { + this.onlineSubscription = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.html b/src/addons/mod/chat/pages/session-messages/session-messages.html index 9e18962a8..fd9f65e02 100644 --- a/src/addons/mod/chat/pages/session-messages/session-messages.html +++ b/src/addons/mod/chat/pages/session-messages/session-messages.html @@ -75,26 +75,9 @@ - - - -

- - -
{{ message.userfullname }}
-

- -
- - -
-
- {{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }} -
-
+ + diff --git a/src/addons/mod/data/tests/behat/entries.feature b/src/addons/mod/data/tests/behat/entries.feature new file mode 100644 index 000000000..0551dc081 --- /dev/null +++ b/src/addons/mod/data/tests/behat/entries.feature @@ -0,0 +1,210 @@ +@mod @mod_data @app @javascript +Feature: Users can manage entries in database activities + In order to populate databases + As a user + I need to add and manage entries to databases + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | data | Web links | Useful links | C1 | data1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + # TODO Create and use a generator for database fields. + And I add a "Text input" field to "Web links" database and I fill the form with: + | Field name | URL | + | Field description | URL link | + And I add a "Text input" field to "Web links" database and I fill the form with: + | Field name | Description | + | Field description | Link description | + And I log out + + Scenario: Create entry + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + Then 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: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + Then I should find "https://moodle.org/" in the app + And I should find "Moodle community site" in the app + + Scenario: Browse entry + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + # TODO Create and use a generator for database entries. + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + And I entered the data activity "Web links" on course "Course 1" as "student2" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + And I press "More" near "Moodle community site" in the app + Then I should find "Moodle community site" in the app + And I should not find "Next" in the app + And I should find "Previous" in the app + And I press "Previous" in the app + And I should find "Moodle Cloud" in the app + And I should find "Next" in the app + And I should not find "Previous" in the app + And I press "Next" in the app + And I should find "Moodle community site" in the app + And I should not find "Moodle Cloud" in the app + And I press the back button in the app + And I should find "Moodle community site" in the app + And I should find "Moodle Cloud" in the app + + Scenario: Students can not edit or delete other user's entries from list and single view in the app + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + And I entered the course "Course 1" as "student2" in the app + When I press "Web links" near "General" in the app + Then "Edit" "link" should not exist + And "Delete" "link" should not exist + And I press "More" in the app + And "Edit" "link" should not exist + And "Delete" "link" should not exist + + Scenario: Delete entry (student) & Update entry (student) + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + + # Edit the entry from list view. + When I press "Edit" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + Then I should not find "https://moodle.org/" in the app + And I should not find "Moodle community site" in the app + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + + # Delete the entry from list view. + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Cancel" in the app + And I should find "Moodle Cloud" in the app + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should not find "Moodle Cloud" in the app + + # Repeat again with single view. + Given I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + + # Edit the entry from single view. + When I press "More" in the app + And I press "Edit" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + Then I should not find "https://moodle.org/" in the app + And I should not find "Moodle community site" in the app + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + + # Delete the entry from list view. + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Cancel" in the app + And I should find "Moodle Cloud" in the app + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should not find "Moodle Cloud" in the app + And I should find "No entries in database" in the app + + Scenario: Delete entry (teacher) & Update entry (teacher) + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://telegram.org/ | + | Description | Telegram | + And I press "Save" near "Web links" in the app + + And I entered the course "Course 1" as "teacher1" in the app + When 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 + + # Edit the entry from list view. + When I press "Edit" near "Moodle community site" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + Then I should not find "https://moodle.org/" in the app + And I should not find "Moodle community site" in the app + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + + # Delete the entry from list view. + When I press "Delete" near "Moodle Cloud" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Cancel" in the app + And I should find "Moodle Cloud" in the app + When I press "Delete" near "Moodle Cloud" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should not find "Moodle Cloud" in the app + + # Edit the entry from single view. + When I press "More" in the app + And I should find "https://telegram.org/" in the app + And I should find "Telegram" in the app + And I press "Edit" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + Then I should not find "https://telegram.org/" in the app + And I should not find "Telegram" in the app + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + + # Delete the entry from single view. + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Cancel" in the app + And I should find "Moodle Cloud" in the app + When I press "Delete" in the app + Then I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should not find "Moodle Cloud" in the app diff --git a/src/addons/mod/data/tests/behat/sync.feature b/src/addons/mod/data/tests/behat/sync.feature new file mode 100644 index 000000000..699739215 --- /dev/null +++ b/src/addons/mod/data/tests/behat/sync.feature @@ -0,0 +1,128 @@ +@mod @mod_data @app @javascript +Feature: Users can store entries in database activities when offline and sync when online + In order to populate databases while offline + As a user + I need to add and manage entries to databases and sync then when online + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | data | Web links | Useful links | C1 | data1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I add a "Text input" field to "Web links" database and I fill the form with: + | Field name | URL | + | Field description | URL link | + And I add a "Text input" field to "Web links" database and I fill the form with: + | Field name | Description | + | Field description | Link description | + And I log out + + 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 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: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + Then I should find "https://moodle.org/" in the app + And I should find "Moodle community site" in the app + And I should find "This Database has offline data to be synchronised" in the app + And I press the back button in the app + And I switch offline mode to "false" + 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 + And I should not find "This Database has offline data to be synchronised" in the app + + Scenario: Update entry (offline) & Delete entry (offline) + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + And I should find "No entries in database" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + And I should find "https://moodle.org/" in the app + And I should find "Moodle community site" in the app + 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" + When I press "Edit" in the app + And I set the following fields to these values in the app: + | URL | https://moodlecloud.com/ | + | Description | Moodle Cloud | + And I press "Save" near "Web links" in the app + Then I should not find "https://moodle.org/" in the app + And I should not find "Moodle community site" in the app + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + And I should 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 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 + And I should find "https://moodlecloud.com/" in the app + And I should find "Moodle Cloud" in the app + And I should not find "This Database has offline data to be synchronised" in the app + And I press "Information" in the app + And I press "Refresh" in the app + And I wait until the page is ready + And I switch offline mode to "true" + And I press "Delete" in the app + And I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should find "https://moodlecloud.com/" in the app + 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 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 + And I should not find "This Database has offline data to be synchronised" in the app + + Scenario: Students can undo deleting entries to a database in the app while offline + Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + And I should find "No entries in database" in the app + And I press "Add entries" in the app + And I set the following fields to these values in the app: + | URL | https://moodle.org/ | + | Description | Moodle community site | + And I press "Save" near "Web links" in the app + And I should find "https://moodle.org/" in the app + And I should find "Moodle community site" in the app + 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 press "Delete" in the app + And I should find "Are you sure you want to delete this entry?" in the app + And I press "Delete" in the app + And I should find "https://moodle.org/" in the app + 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 "Restore" in the app + And I press the back button in the app + And I switch offline mode to "false" + 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 + And I should not find "This Database has offline data to be synchronised" in the app diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts index 7ef9df00a..6226940cc 100644 --- a/src/addons/mod/feedback/pages/form/form.ts +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -24,7 +24,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; import { @@ -80,7 +80,7 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { this.currentSite = CoreSites.getRequiredCurrentSite(); // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.offline = !CoreNetwork.isOnline(); diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index 39af8c389..6556402dd 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -30,7 +30,7 @@ import { CoreScreen } from '@services/screen'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreArray } from '@singletons/array'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -166,7 +166,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes this.isOnline = CoreNetwork.isOnline(); this.externalUrl = CoreSites.getCurrentSite()?.createSiteUrl('/mod/forum/discuss.php', { d: this.discussionId.toString() }); - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/mod/forum/tests/behat/basic_usage.feature b/src/addons/mod/forum/tests/behat/basic_usage.feature index f2cfc19c1..c2f3553b4 100755 --- a/src/addons/mod/forum/tests/behat/basic_usage.feature +++ b/src/addons/mod/forum/tests/behat/basic_usage.feature @@ -27,8 +27,9 @@ Feature: Test basic usage of forum activity in app Scenario: Create new discussion Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "My happy subject" in the app - And I set the field "Message" to "An awesome message" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | And I press "Post to forum" in the app Then I should find "My happy subject" in the app @@ -38,8 +39,9 @@ Feature: Test basic usage of forum activity in app Scenario: Reply a post Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject" in the app - And I set the field "Message" to "DiscussionMessage" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject | + | Message | DiscussionMessage | And I press "Post to forum" in the app And I press "DiscussionSubject" in the app Then I should find "Reply" in the app @@ -53,12 +55,14 @@ Feature: Test basic usage of forum activity in app Scenario: Star and pin discussions (student) Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "starred subject" in the app - And I set the field "Message" to "starred message" in the app + And I set the following fields to these values in the app: + | Subject | starred subject | + | Message | starred message | And I press "Post to forum" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "normal subject" in the app - And I set the field "Message" to "normal message" in the app + And I set the following fields to these values in the app: + | Subject | normal subject | + | Message | normal message | And I press "Post to forum" in the app And I press "starred subject" in the app Then I should find "starred message" in the app @@ -86,16 +90,19 @@ Feature: Test basic usage of forum activity in app Scenario: Star and pin discussions (teacher) Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test star" in the app - And I set the field "Message" to "Auto-test star message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test star | + | Message | Auto-test star message | And I press "Post to forum" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test pin" in the app - And I set the field "Message" to "Auto-test pin message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test pin | + | Message | Auto-test pin message | And I press "Post to forum" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test plain" in the app - And I set the field "Message" to "Auto-test plain message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test plain | + | Message | Auto-test plain message | And I press "Post to forum" in the app And I press "Display options" near "Auto-test star" in the app And I press "Star this discussion" in the app @@ -115,8 +122,9 @@ Feature: Test basic usage of forum activity in app Scenario: Edit a not sent reply offline Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test" in the app - And I set the field "Message" to "Auto-test message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test | + | Message | Auto-test message | And I press "Post to forum" in the app And I press "Auto-test" near "Sort by last post creation date in descending order" in the app And I should find "Reply" in the app @@ -148,8 +156,9 @@ Feature: Test basic usage of forum activity in app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I switch offline mode to "true" And I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test" in the app - And I set the field "Message" to "Auto-test message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test | + | Message | Auto-test message | And I press "Post to forum" in the app And I press "Auto-test" in the app And I set the field "Message" to "Auto-test message edited" in the app @@ -169,8 +178,9 @@ Feature: Test basic usage of forum activity in app Scenario: Edit a forum post (only online) Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test" in the app - And I set the field "Message" to "Auto-test message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test | + | Message | Auto-test message | And I press "Post to forum" in the app Then I should find "Auto-test" in the app @@ -194,8 +204,9 @@ Feature: Test basic usage of forum activity in app Scenario: Delete a forum post (only online) Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test" in the app - And I set the field "Message" to "Auto-test message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test | + | Message | Auto-test message | And I press "Post to forum" in the app Then I should find "Auto-test" in the app @@ -230,8 +241,9 @@ Feature: Test basic usage of forum activity in app Scenario: Add/view ratings Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "Auto-test" in the app - And I set the field "Message" to "Auto-test message" in the app + And I set the following fields to these values in the app: + | Subject | Auto-test | + | Message | Auto-test message | And I press "Post to forum" in the app And I press "Auto-test" in the app Then I should find "Reply" in the app @@ -276,8 +288,9 @@ 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 "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject" in the app - And I set the field "Message" to "DiscussionMessage" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject | + | Message | DiscussionMessage | And I press "Post to forum" in the app And I press the back button in the app And I press "Course downloads" in the app @@ -306,8 +319,9 @@ Feature: Test basic usage of forum activity in app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I switch offline mode to "true" And I press "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject" in the app - And I set the field "Message" to "DiscussionMessage" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject | + | Message | DiscussionMessage | And I press "Post to forum" in the app Then I should find "DiscussionSubject" in the app And I should find "Not sent" in the app @@ -328,8 +342,9 @@ Feature: Test basic usage of forum activity in app Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I switch offline mode to "true" And I press "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject" in the app - And I set the field "Message" to "DiscussionMessage" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject | + | Message | DiscussionMessage | And I press "Post to forum" in the app Then I should find "DiscussionSubject" in the app And I should find "Not sent" in the app @@ -349,8 +364,9 @@ Feature: Test basic usage of forum activity in app Scenario: Prefetch Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject 1" in the app - And I set the field "Message" to "DiscussionMessage 1" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject 1 | + | Message | DiscussionMessage 1 | And I press "Post to forum" in the app Then I should find "DiscussionSubject 1" in the app @@ -362,8 +378,9 @@ Feature: Test basic usage of forum activity in app When I press "Test forum name" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "DiscussionSubject 2" in the app - And I set the field "Message" to "DiscussionMessage 2" in the app + And I set the following fields to these values in the app: + | Subject | DiscussionSubject 2 | + | Message | DiscussionMessage 2 | And I press "Post to forum" in the app Then I should find "DiscussionSubject 1" in the app And I should find "DiscussionSubject 2" in the app diff --git a/src/addons/mod/forum/tests/behat/navigation.feature b/src/addons/mod/forum/tests/behat/navigation.feature index f3a9a4f7a..80bdef26b 100644 --- a/src/addons/mod/forum/tests/behat/navigation.feature +++ b/src/addons/mod/forum/tests/behat/navigation.feature @@ -104,20 +104,23 @@ Feature: Test forum navigation 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 set the field "Subject" to "Offline discussion 1" in the app - And I set the field "Message" to "Offline discussion 1 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 1 | + | Message | Offline discussion 1 message | And I press "Post to forum" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "Offline discussion 2" in the app - And I set the field "Message" to "Offline discussion 2 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 2 | + | Message | Offline discussion 2 message | And I press "Post to forum" in the app Then I should find "Not sent" in the app And I should find "Offline discussion 1" in the app And I should find "Offline discussion 2" in the app When I press "Offline discussion 2" in the app - And I set the field "Subject" to "Offline discussion 3" in the app - And I set the field "Message" to "Offline discussion 3 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 3 | + | Message | Offline discussion 3 message | And I press "Post to forum" in the app Then I should find "Not sent" in the app And I should find "Offline discussion 1" in the app @@ -197,20 +200,23 @@ Feature: Test forum navigation # Offline When I press "Add discussion topic" in the app And I switch offline mode to "true" - And I set the field "Subject" to "Offline discussion 1" in the app - And I set the field "Message" to "Offline discussion 1 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 1 | + | Message | Offline discussion 1 message | And I press "Post to forum" in the app And I press "Add discussion topic" in the app - And I set the field "Subject" to "Offline discussion 2" in the app - And I set the field "Message" to "Offline discussion 2 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 2 | + | Message | Offline discussion 2 message | And I press "Post to forum" in the app Then I should find "Not sent" in the app And I should find "Offline discussion 1" in the app And I should find "Offline discussion 2" in the app When I press "Offline discussion 2" in the app - And I set the field "Subject" to "Offline discussion 3" in the app - And I set the field "Message" to "Offline discussion 3 message" in the app + And I set the following fields to these values in the app: + | Subject | Offline discussion 3 | + | Message | Offline discussion 3 message | And I press "Post to forum" in the app Then I should find "Not sent" in the app And I should find "Offline discussion 1" in the app diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html index 613c62a6e..fbfbe55aa 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -28,8 +28,7 @@ + [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index e0d559580..36595994c 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -72,8 +72,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity loadMoreError = false; loadingMessage: string; promisedEntries: CorePromisedValue; - hasOfflineRatings = false; + protected hasOfflineEntries = false; + protected hasOfflineRatings = false; protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; protected addEntryObserver?: CoreEventObserver; protected fetchedEntriesCanLoadMore = false; @@ -128,7 +129,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(source, this)); this.sourceUnsubscribe = source.addListener({ - onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)), + onItemsUpdated: (items) => { + this.hasOfflineEntries = !!items.find(item => source.isOfflineEntry(item)); + this.hasOffline = this.hasOfflineEntries || this.hasOfflineRatings; + }, }); // When an entry is added, we reload the data. @@ -146,12 +150,14 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module' && data.instanceId == this.glossary.coursemodule) { this.hasOfflineRatings = true; + this.hasOffline = true; } }); this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => { if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module' && data.instanceId == this.glossary.coursemodule) { this.hasOfflineRatings = false; + this.hasOffline = this.hasOfflineEntries; } }); } @@ -198,6 +204,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity ]); this.hasOfflineRatings = hasOfflineRatings; + this.hasOffline = this.hasOfflineEntries || this.hasOfflineRatings; } /** diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature new file mode 100644 index 000000000..c3103b0a3 --- /dev/null +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -0,0 +1,233 @@ +@mod @mod_glossary @app @javascript +Feature: Test basic usage of glossary in app + In order to participate in the glossaries while using the mobile app + As a student + I need basic glossary functionality to work + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | teacher | teacher1@example.com | + | teacher2 | Teacher2 | teacher2 | teacher2@example.com | + | student1 | Student | student | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | mainglossary | allowcomments | assessed | scale | + | glossary | Test glossary | glossary description | C1 | gloss1 | 1 | 1 | 1 | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | + | forum | Test forum name | Test forum | C1 | forum | 0 | + And the following "mod_glossary > categories" exist: + | glossary | name | + | gloss1 | The ones I like | + | gloss1 | All for you | + And the following "mod_glossary > entries" exist: + | glossary | concept | definition | user | categories | usedynalink | + | gloss1 | Eggplant | Sour eggplants | teacher1 | All for you | 0 | + | gloss1 | Cucumber | Sweet cucumber | student1 | The ones I like | 0 | + | gloss1 | Potato | To make chips | student1 | The ones I like | 1 | + | gloss1 | Raddish | Raphanus sativus | student1 | All for you | 1 | + + Scenario: View a glossary and its terms + Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + Then the header should be "Test glossary" in the app + And I should find "Eggplant" in the app + And I should find "Cucumber" in the app + And I should find "Potato" in the app + + When I press "Potato" in the app + Then I should find "Potato" in the app + And I should find "To make chips" in the app + + Scenario: Navigate to glossary terms by link (auto-linking) + Given the "glossary" filter is "on" + And I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + Then the header should be "Test glossary" in the app + And I should find "Eggplant" in the app + And I should find "Cucumber" in the app + And I should find "Potato" in the app + And I should find "Raddish" in the app + + When I press the back button in the app + And I press "Test forum name" in the app + And I press "Add discussion topic" in the app + And I set the field "Subject" to "Testing auto-link glossary" + And I set the field "Message" to "Glossary terms auto-linked: Raddish Potato" in the app + And I press "Post to forum" in the app + And I press "Testing auto-link glossary" in the app + Then I should find "Raddish" in the app + + When I press "Raddish" in the app + Then the header should be "Raddish" in the app + And I should find "Raphanus sativus" in the app + + When I press the back button in the app + And I press "Potato" in the app + Then the header should be "Potato" in the app + And I should find "To make chips" in the app + + Scenario: See comments + Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + Then the header should be "Test glossary" in the app + + When I press "Eggplant" in the app + Then I should find "Comments (0)" in the app + + # Write comments as a teacher + Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher1" in the app + And I press "Eggplant" in the app + Then I should find "Comments (0)" in the app + + When I press "Comments" in the app + Then I should find "No comments" in the app + + And I set the field "Add a comment..." to "teacher first comment" in the app + And I press "Send" in the app + Then I should find "teacher first comment" in the app + + And I set the field "Add a comment..." to "teacher second comment" in the app + And I press "Send" in the app + Then I should find "teacher first comment" in the app + And I should find "teacher second comment" in the 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 + Then I should find "Comments (2)" in the app + + When I press "Comments" in the app + And I should find "teacher first comment" in the app + And I should find "teacher second comment" in the app + + Scenario: Prefetch + Given I entered the course "Course 1" as "student1" in the 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 press "Test glossary" in the app + Then the header should be "Test glossary" in the app + And I should find "Cucumber" in the app + And I should find "Eggplant" in the app + And I should find "Potato" in the app + + When I press "Eggplant" in the app + Then I should find "Eggplant" in the app + And I should find "Sour eggplants" in the app + And I should not see "Comments cannot be retrieved" + And I should find "Comments (0)" in the app + + Scenario: Add entries (basic info) + 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 set the following fields to these values in the app: + | Concept | Broccoli | + | Definition | Brassica oleracea var. italica | + And I press "Save" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Cabbage | + | Definition | Brassica oleracea var. capitata | + And I press "Save" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Garlic | + | Definition | Allium sativum | + And I press "Save" in the app + Then the header should be "Test glossary" in the app + And I should find "Cucumber" in the app + And I should find "Eggplant" in the app + And I should find "Potato" in the app + And I should find "Broccoli" in the app + And I should find "Cabbage" in the app + And I should find "Garlic" in the app + + When I press "Garlic" in the app + Then I should find "Garlic" in the app + And I should find "Allium sativum" in the 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 set the following fields to these values in the app: + | Concept | Broccoli | + | Definition | Brassica oleracea var. italica | + And I press "Save" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Cabbage | + | Definition | Brassica oleracea var. capitata | + And I press "Save" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Garlic | + | Definition | Allium sativum | + And I press "Save" in the app + Then the header should be "Test glossary" in the app + And I should find "Cucumber" in the app + And I should find "Eggplant" in the app + And I should find "Potato" in the app + And I should find "Broccoli" in the app + And I should find "Cabbage" in the app + And I should find "Garlic" in the app + And I should find "Entries to be synced" in the app + And I should find "This Glossary has offline data to be synchronised." in the app + + When I switch offline mode to "false" + 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 + And I should find "Cucumber" in the app + And I should find "Eggplant" in the app + And I should find "Potato" in the app + And I should find "Broccoli" in the app + And I should find "Cabbage" in the app + And I should find "Garlic" in the app + But I should not see "Entries to be synced" + And I should not see "This Glossary has offline data to be synchronised." + + When I press "Garlic" in the app + Then I should find "Garlic" in the app + And I should find "Allium sativum" in the app + + Scenario: Add/view ratings + # Rate entries as teacher1 + Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher1" in the app + And I press "Cucumber" in the app + Then I should find "Average of ratings: -" in the app + + When I press "None" in the app + And I press "1" in the app + Then I should find "Average of ratings: 1" in the 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 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" + And I press the back button in the app + Then I should find "This Glossary has offline data to be synchronised." in the app + + When I press "Information" in the app + And I press "Synchronise now" in the app + And I press "Cucumber" in the app + Then I should find "Average of ratings: 0.5" in the app + + # View ratings as a student + Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + And I press "Cucumber" in the app + Then the header should be "Cucumber" in the app + But I should not see "Average of ratings: 0.5" diff --git a/src/addons/mod/glossary/tests/behat/navigation.feature b/src/addons/mod/glossary/tests/behat/navigation.feature index 77d1bd1b8..04b988ecf 100644 --- a/src/addons/mod/glossary/tests/behat/navigation.feature +++ b/src/addons/mod/glossary/tests/behat/navigation.feature @@ -146,8 +146,12 @@ Feature: Test glossary navigation When I press the back button in the app And I scroll to "Acerola" in the app And I press "Search" in the app - And I set the field "Search" to "melon" in the app - And I press "Search" "button" near "Clear search" in the app + And I set the field "Search" to "something" in the app + And I press enter + Then I should find "No entries were found." in the app + + When I set the field "Search" to "melon" in the app + And I press enter Then I should find "Honeydew Melon" in the app And I should find "Watermelon" in the app But I should not find "Acerola" in the app @@ -170,12 +174,14 @@ Feature: Test glossary navigation 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 set the field "Concept" to "Tomato" in the app - And I set the field "Definition" to "Tomato is a fruit" in the app + And I set the following fields to these values in the app: + | Concept | Tomato | + | Definition | Tomato is a fruit | And I press "Save" in the app And I press "Add a new entry" in the app - And I set the field "Concept" to "Cashew" in the app - And I set the field "Definition" to "Cashew is a fruit" in the app + And I set the following fields to these values in the app: + | Concept | Cashew | + | Definition | Cashew is a fruit | And I press "Save" in the app Then I should find "Entries to be synced" in the app And I should find "Tomato" in the app @@ -248,8 +254,12 @@ Feature: Test glossary navigation # Search When I press "Search" in the app - And I set the field "Search" to "melon" in the app - And I press "Search" "button" near "Clear search" in the app + And I set the field "Search" to "something" in the app + And I press enter + Then I should find "No entries were found." in the app + + When I set the field "Search" to "melon" in the app + And I press enter Then I should find "Honeydew Melon" in the app And I should find "Watermelon" in the app And "Honeydew Melon" near "Watermelon" should be selected in the app @@ -265,11 +275,13 @@ Feature: Test glossary navigation 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 set the field "Concept" to "Tomato" in the app - And I set the field "Definition" to "Tomato is a fruit" in the app + And I set the following fields to these values in the app: + | Concept | Tomato | + | Definition | Tomato is a fruit | And I press "Save" in the app - And I set the field "Concept" to "Cashew" in the app - And I set the field "Definition" to "Cashew is a fruit" in the app + And I set the following fields to these values in the app: + | Concept | Cashew | + | Definition | Cashew is a fruit | And I press "Save" in the app Then I should find "Entries to be synced" in the app And I should find "Tomato" in the app diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index e8ca58c9c..c13acb8fd 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -27,7 +27,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils, OpenFileAction } from '@services/utils/utils'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { Subscription } from 'rxjs'; import { AddonModResource, @@ -83,7 +83,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource this.isOnline = CoreNetwork.isOnline(); // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/addons/mod/survey/components/index/addon-mod-survey-index.html b/src/addons/mod/survey/components/index/addon-mod-survey-index.html index 6d04fab7c..a8aa106d4 100644 --- a/src/addons/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addons/mod/survey/components/index/addon-mod-survey-index.html @@ -50,14 +50,12 @@ - + - - - {{question.num}}. {{ question.text }} - - + + {{question.num}}. {{ question.text }} + @@ -69,8 +67,8 @@ + [attr.aria-labelledby]="'addon-mod_survey-'+question.name" interface="action-sheet" + [name]="question.name" [interfaceOptions]="{header: question.text}"> {{ 'core.choose' | translate }} {{option}} @@ -85,17 +83,18 @@ - - - {{question.num}}. {{ question.text }} - - + + {{question.num}}. {{ question.text }} + - + + {{ 'core.choose' | translate }} + + {{option}} @@ -103,13 +102,13 @@ - + {{question.num}}. {{ question.text }} + [attr.aria-labelledby]="'addon-mod_survey-'+question.name" [required]="question.required"> diff --git a/src/addons/mod/survey/services/survey-helper.ts b/src/addons/mod/survey/services/survey-helper.ts index 352ad6487..d1217a03e 100644 --- a/src/addons/mod/survey/services/survey-helper.ts +++ b/src/addons/mod/survey/services/survey-helper.ts @@ -68,7 +68,6 @@ export class AddonModSurveyHelperProvider { formatQuestions(questions: AddonModSurveyQuestion[]): AddonModSurveyQuestionFormatted[] { const strIPreferThat = Translate.instant('addon.mod_survey.ipreferthat'); const strIFoundThat = Translate.instant('addon.mod_survey.ifoundthat'); - const strChoose = Translate.instant('core.choose'); const formatted: AddonModSurveyQuestionFormatted[] = []; const parents = this.getParentQuestions(questions); @@ -112,9 +111,6 @@ export class AddonModSurveyHelperProvider { // It's a single question. q1.name = 'q' + q1.id; q1.num = num++; - if (q1.type > 0) { // Add "choose" option since this question is not required. - q1.optionsArray.unshift(strChoose); - } } formatted.push(q1); diff --git a/src/addons/mod/survey/tests/behat/basic_usage.feature b/src/addons/mod/survey/tests/behat/basic_usage.feature new file mode 100755 index 000000000..b6e0755c4 --- /dev/null +++ b/src/addons/mod/survey/tests/behat/basic_usage.feature @@ -0,0 +1,272 @@ +@mod @mod_survey @app @javascript +Feature: Test basic usage of survey activity in app + In order to participate in surveys while using the mobile app + As a student + I need basic survey functionality to work + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | + | student1 | + | teacher1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | + | survey | Test survey name | Test survey | C1 | survey | 0 | + + Scenario: Answer a survey & View results (ATTLS) + Given I entered the survey activity "Test survey name" on course "Course 1" as "student1" in the app + And I set the following fields to these values in the app: + | 1. In evaluating what someone says, I focus on the quality of their argument, not on the person who's presenting it. | Strongly agree | + | 2. I like playing devil's advocate - arguing the opposite of what someone is saying. | Strongly disagree | + | 3. I like to understand where other people are 'coming from', what experiences have led them to feel the way they do. | Somewhat agree | + | 4. The most important part of my education has been learning to understand people who are very different to me. | Somewhat disagree | + | 5. I feel that the best way for me to achieve my own identity is to interact with a variety of other people. | Somewhat agree | + | 6. I enjoy hearing the opinions of people who come from backgrounds different to mine - it helps me to understand how the same things can be seen in such different ways. | Somewhat agree | + | 7. I find that I can strengthen my own position through arguing with someone who disagrees with me. | Somewhat agree | + | 8. I am always interested in knowing why people say and believe the things they do. | Somewhat agree | + | 9. I often find myself arguing with the authors of books that I read, trying to logically figure out why they're wrong. | Somewhat agree | + | 10. It's important for me to remain as objective as possible when I analyze something. | Somewhat agree | + | 11. I try to think with people instead of against them. | Somewhat agree | + | 12. I have certain criteria I use in evaluating arguments. | Somewhat agree | + | 13. I'm more likely to try to understand someone else's opinion than to try to evaluate it. | Somewhat agree | + | 14. I try to point out weaknesses in other people's thinking to help them clarify their arguments. | Somewhat agree | + | 15. I tend to put myself in other people's shoes when discussing controversial issues, to see why they think the way they do. | Somewhat agree | + | 16. One could call my way of analysing things 'putting them on trial' because I am careful to consider all the evidence. | Somewhat agree | + | 17. I value the use of logic and reason over the incorporation of my own concerns when solving problems. | Somewhat agree | + | 18. I can obtain insight into opinions that differ from mine through empathy. | Somewhat agree | + | 19. When I encounter people whose opinions seem alien to me, I make a deliberate effort to 'extend' myself into that person, to try to see how they could have those opinions. | Somewhat agree | + | 20. I spend time figuring out what's 'wrong' with things. For example, I'll look for something in a literary interpretation that isn't argued well enough. | Somewhat agree | + And I press "Submit" in the app + And I press "OK" in the app + And I press "Results" in the app + And I press "OK" in the app + And I switch to the browser tab opened by the app + And I log in as "student1" + Then I should see "You've completed this survey. The graph below shows a summary of your results compared to the class averages." + And I should see "1 people have completed this survey so far" + + Scenario: Answer a survey & View results (Critical incidents) + Given the following "activities" exist: + | activity | name | intro | template |course | idnumber | groupmode | + | survey | Test survey critical incidents | Test survey1 | 5 | C1 | survey1 | 0 | + Given I entered the survey activity "Test survey critical incidents" on course "Course 1" as "student1" in the app + And I set the following fields to these values in the app: + | At what moment in class were you most engaged as a learner? | 1st answer | + | At what moment in class were you most distanced as a learner? | 2nd answer | + | What action from anyone in the forums did you find most affirming or helpful? | 3rd answer | + | What action from anyone in the forums did you find most puzzling or confusing? | 4th answer | + | What event surprised you most? | 5th answer | + And I press "Submit" in the app + And I press "OK" in the app + Then I should see "Results" + + When I press "Results" in the app + And I press "OK" in the app + And I switch to the browser tab opened by the app + And I log in as "student1" + Then I should see "Test survey critical incidents" + And I should see "1st answer" + And I should see "2nd answer" + And I should see "3rd answer" + And I should see "4th answer" + And I should see "5th answer" + + Scenario: Answer a survey & View results (Colles actual) + Given the following "activities" exist: + | activity | name | intro | template |course | idnumber | groupmode | + | survey | Test survey Colles (actual) | Test survey1 | 1 | C1 | survey1 | 0 | + Given I entered the survey activity "Test survey Colles (actual)" on course "Course 1" as "student1" in the app + And I set the following fields to these values in the app: + | 1. my learning focuses on issues that interest me. | Sometimes | + | 2. what I learn is important for my professional practice. | Sometimes | + | 3. I learn how to improve my professional practice. | Sometimes | + | 4. what I learn connects well with my professional practice. | Sometimes | + | 5. I think critically about how I learn. | Sometimes | + | 6. I think critically about my own ideas. | Sometimes | + | 7. I think critically about other students' ideas. | Sometimes | + | 8. I think critically about ideas in the readings. | Sometimes | + | 9. I explain my ideas to other students. | Sometimes | + | 10. I ask other students to explain their ideas. | Sometimes | + | 11. other students ask me to explain my ideas. | Sometimes | + | 12. other students respond to my ideas. | Sometimes | + | 13. the tutor stimulates my thinking. | Sometimes | + | 14. the tutor encourages me to participate. | Sometimes | + | 15. the tutor models good discourse. | Sometimes | + | 16. the tutor models critical self-reflection. | Sometimes | + | 17. other students encourage my participation. | Sometimes | + | 18. other students praise my contribution. | Sometimes | + | 19. other students value my contribution. | Sometimes | + | 20. other students empathise with my struggle to learn. | Sometimes | + | 21. I make good sense of other students' messages. | Sometimes | + | 22. other students make good sense of my messages. | Sometimes | + | 23. I make good sense of the tutor's messages. | Sometimes | + | 24. the tutor makes good sense of my messages. | Sometimes | + | 25. How long did this survey take you to complete? | under 1 min | + And I press "Submit" in the app + And I press "OK" in the app + Then I should see "You have completed this survey" + + When I press "Results" in the app + And I press "OK" in the app + And I switch to the browser tab opened by the app + And I log in as "student1" + Then I should see "You've completed this survey. The graph below shows a summary of your results compared to the class averages." + And I should see "1 people have completed this survey so far" + + Scenario: Answer a survey & View results (Colles preferred) + Given the following "activities" exist: + | activity | name | intro | template | course | idnumber | groupmode | + | survey | Test survey Colles (preferred) | Test survey1 | 2 | C1 | survey1 | 0 | + Given I entered the survey activity "Test survey Colles (preferred)" on course "Course 1" as "student1" in the app + And I set the following fields to these values in the app: + | 1. my learning focuses on issues that interest me. | Sometimes | + | 2. what I learn is important for my professional practice. | Sometimes | + | 3. I learn how to improve my professional practice. | Sometimes | + | 4. what I learn connects well with my professional practice. | Sometimes | + | 5. I think critically about how I learn. | Sometimes | + | 6. I think critically about my own ideas. | Sometimes | + | 7. I think critically about other students' ideas. | Sometimes | + | 8. I think critically about ideas in the readings. | Sometimes | + | 9. I explain my ideas to other students. | Sometimes | + | 10. I ask other students to explain their ideas. | Sometimes | + | 11. other students ask me to explain my ideas. | Sometimes | + | 12. other students respond to my ideas. | Sometimes | + | 13. the tutor stimulates my thinking. | Sometimes | + | 14. the tutor encourages me to participate. | Sometimes | + | 15. the tutor models good discourse. | Sometimes | + | 16. the tutor models critical self-reflection. | Sometimes | + | 17. other students encourage my participation. | Sometimes | + | 18. other students praise my contribution. | Sometimes | + | 19. other students value my contribution. | Sometimes | + | 20. other students empathise with my struggle to learn. | Sometimes | + | 21. I make good sense of other students' messages. | Sometimes | + | 22. other students make good sense of my messages. | Sometimes | + | 23. I make good sense of the tutor's messages. | Sometimes | + | 24. the tutor makes good sense of my messages. | Sometimes | + | 25. How long did this survey take you to complete? | under 1 min | + And I press "Submit" in the app + And I press "OK" in the app + Then I should see "You have completed this survey" + + When I press "Results" in the app + And I press "OK" in the app + And I switch to the browser tab opened by the app + And I log in as "student1" + Then I should see "You've completed this survey. The graph below shows a summary of your results compared to the class averages." + And I should see "1 people have completed this survey so far" + + Scenario: Answer a survey & View results (Colles preferred and actual) + Given the following "activities" exist: + | activity | name | intro | template | course | idnumber | groupmode | + | survey | Test survey Colles (preferred and actual) | Test survey1 | 3 | C1 | survey1 | 0 | + Given I entered the survey activity "Test survey Colles (preferred and actual)" on course "Course 1" as "student1" in the app + And I set the following fields to these values in the app: + | 1. I prefer that my learning focuses on issues that interest me. | Sometimes | + | 2. I found that my learning focuses on issues that interest me. | Sometimes | + | 3. I prefer that what I learn is important for my professional practice. | Sometimes | + | 4. I found that what I learn is important for my professional practice. | Sometimes | + | 5. I prefer that I learn how to improve my professional practice. | Sometimes | + | 6. I found that I learn how to improve my professional practice. | Sometimes | + | 7. I prefer that what I learn connects well with my professional practice. | Sometimes | + | 8. I found that what I learn connects well with my professional practice. | Sometimes | + | 9. I prefer that I think critically about how I learn. | Sometimes | + | 10. I found that I think critically about how I learn. | Sometimes | + | 11. I prefer that I think critically about my own ideas. | Sometimes | + | 12. I found that I think critically about my own ideas. | Sometimes | + | 13. I prefer that I think critically about other students' ideas. | Sometimes | + | 14. I found that I think critically about other students' ideas. | Sometimes | + | 15. I prefer that I think critically about ideas in the readings. | Sometimes | + | 16. I found that I think critically about ideas in the readings. | Sometimes | + | 17. I prefer that I explain my ideas to other students. | Sometimes | + | 18. I found that I explain my ideas to other students. | Sometimes | + | 19. I prefer that I ask other students to explain their ideas. | Sometimes | + | 20. I found that I ask other students to explain their ideas. | Sometimes | + | 21. I prefer that other students ask me to explain my ideas. | Sometimes | + | 22. I found that other students ask me to explain my ideas. | Sometimes | + | 23. I prefer that other students respond to my ideas. | Sometimes | + | 24. I found that other students respond to my ideas. | Sometimes | + | 25. I prefer that the tutor stimulates my thinking. | Sometimes | + | 26. I found that the tutor stimulates my thinking. | Sometimes | + | 27. I prefer that the tutor encourages me to participate. | Sometimes | + | 28. I found that the tutor encourages me to participate. | Sometimes | + | 29. I prefer that the tutor models good discourse. | Sometimes | + | 30. I found that the tutor models good discourse. | Sometimes | + | 31. I prefer that the tutor models critical self-reflection. | Sometimes | + | 32. I found that the tutor models critical self-reflection. | Sometimes | + | 33. I prefer that other students encourage my participation. | Sometimes | + | 34. I found that other students encourage my participation. | Sometimes | + | 35. I prefer that other students praise my contribution. | Sometimes | + | 36. I found that other students praise my contribution. | Sometimes | + | 37. I prefer that other students value my contribution. | Sometimes | + | 38. I found that other students value my contribution. | Sometimes | + | 39. I prefer that other students empathise with my struggle to learn. | Sometimes | + | 40. I found that other students empathise with my struggle to learn. | Sometimes | + | 41. I prefer that I make good sense of other students' messages. | Sometimes | + | 42. I found that I make good sense of other students' messages. | Sometimes | + | 43. I prefer that other students make good sense of my messages. | Sometimes | + | 44. I found that other students make good sense of my messages. | Sometimes | + | 45. I prefer that I make good sense of the tutor's messages. | Sometimes | + | 46. I found that I make good sense of the tutor's messages. | Sometimes | + | 47. I prefer that the tutor makes good sense of my messages. | Sometimes | + | 48. I found that the tutor makes good sense of my messages. | Sometimes | + | 49. How long did this survey take you to complete? | 1-2 min | + And I press "Submit" in the app + And I press "OK" in the app + Then I should see "You have completed this survey" + + When I press "Results" in the app + And I press "OK" in the app + And I switch to the browser tab opened by the app + And I log in as "student1" + Then I should see "You've completed this survey. The graph below shows a summary of your results compared to the class averages." + And I should see "1 people have completed this survey so far" + + Scenario: Answer survey offline & Sync survey + Given the following "activities" exist: + | activity | name | intro | template | course | idnumber | groupmode | + | survey | Test survey critical incidents | Test survey1 | 5 | C1 | survey1 | 0 | + Given I entered the survey activity "Test survey critical incidents" on course "Course 1" as "student1" in the app + And I switch offline mode to "true" + 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" + 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 + And I press "Refresh" in the app + Then I should see "Results" + And I should see "You have completed this survey." + But I should not see "This Survey has offline data to be synchronised." + + Scenario: Prefetch & Auto-sync survey + Given the following "activities" exist: + | activity | name | intro | template | course | idnumber | groupmode | + | survey | Test survey critical incidents | Test survey1 | 5 | C1 | survey1 | 0 | + Given I entered the course "Course 1" as "student1" in the app + And I press "Course downloads" in the app + And I press "Download" within "Test survey critical incidents" "ion-item" in the app + And I press the back button in the app + And I switch offline mode to "true" + 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." + + When I press "OK" in the app + And I press the back button in the app + And I press "Test survey critical incidents" in the app + And I press "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" + 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." diff --git a/src/addons/mod/wiki/components/index/index.ts b/src/addons/mod/wiki/components/index/index.ts index 0a88f7cdb..73444595e 100644 --- a/src/addons/mod/wiki/components/index/index.ts +++ b/src/addons/mod/wiki/components/index/index.ts @@ -27,7 +27,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { Network, Translate, NgZone } from '@singletons'; +import { Translate, NgZone } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreText } from '@singletons/text'; import { Subscription } from 'rxjs'; @@ -119,7 +119,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp this.isOnline = CoreNetwork.isOnline(); // Refresh online status when changes. - this.onlineSubscription = Network.onChange().subscribe(() => { + this.onlineSubscription = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 594733fe0..25dbce527 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,7 +19,8 @@ import { BackButtonEvent, ScrollDetail } from '@ionic/core'; import { CoreLang } from '@services/lang'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreEvents } from '@singletons/events'; -import { Network, NgZone, Platform, SplashScreen, Translate } from '@singletons'; +import { NgZone, Platform, SplashScreen, Translate } from '@singletons'; +import { CoreNetwork } from '@services/network'; import { CoreApp, CoreAppProvider } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreNavigator } from '@services/navigator'; @@ -32,7 +33,6 @@ import { CoreConstants } from '@/core/constants'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreDom } from '@singletons/dom'; -import { CoreNetwork } from '@services/network'; const MOODLE_VERSION_PREFIX = 'version-'; const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; @@ -308,7 +308,7 @@ export class AppComponent implements OnInit, AfterViewInit { await Platform.ready(); // Refresh online status when changes. - Network.onChange().subscribe(() => { + CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { const isOnline = CoreNetwork.isOnline(); diff --git a/src/core/components/animations.ts b/src/core/components/animations.ts index 0f5d36215..d149fe5e2 100644 --- a/src/core/components/animations.ts +++ b/src/core/components/animations.ts @@ -36,7 +36,7 @@ export class CoreAnimations { animate(300, keyframes([ style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }), style({ opacity: 1, transform: 'translateX(5%)', offset: 0.7 }), - style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }), + style({ opacity: 1, transform: 'translateX(0)', offset: 1 }), ])), ]), // Leave animation. @@ -44,7 +44,7 @@ export class CoreAnimations { animate(300, keyframes([ style({ opacity: 1, transform: 'translateX(0)', offset: 0 }), style({ opacity: 1, transform: 'translateX(5%)', offset: 0.3 }), - style({ opacity: 0, transform: 'translateX(-100%)', offset: 1.0 }), + style({ opacity: 0, transform: 'translateX(-100%)', offset: 1 }), ])), ]), // Enter animation. @@ -52,7 +52,7 @@ export class CoreAnimations { animate(300, keyframes([ style({ opacity: 0, transform: 'translateX(100%)', offset: 0 }), style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.7 }), - style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }), + style({ opacity: 1, transform: 'translateX(0)', offset: 1 }), ])), ]), // Leave animation. @@ -60,7 +60,7 @@ export class CoreAnimations { animate(300, keyframes([ style({ opacity: 1, transform: 'translateX(0)', offset: 0 }), style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.3 }), - style({ opacity: 0, transform: 'translateX(100%)', offset: 1.0 }), + style({ opacity: 0, transform: 'translateX(100%)', offset: 1 }), ])), ]), ]); diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index 2ab8a0a30..7ad2d0279 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -61,6 +61,7 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-with-spinner'; import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides'; import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour'; +import { CoreMessageComponent } from './message/message'; @NgModule({ declarations: [ @@ -84,6 +85,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe- CoreLoadingComponent, CoreLocalFileComponent, CoreMarkRequiredComponent, + CoreMessageComponent, CoreModIconComponent, CoreNavBarButtonsComponent, CoreNavigationBarComponent, @@ -134,6 +136,7 @@ import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe- CoreLoadingComponent, CoreLocalFileComponent, CoreMarkRequiredComponent, + CoreMessageComponent, CoreModIconComponent, CoreNavBarButtonsComponent, CoreNavigationBarComponent, diff --git a/src/core/components/message/message.html b/src/core/components/message/message.html new file mode 100644 index 000000000..80ee993ad --- /dev/null +++ b/src/core/components/message/message.html @@ -0,0 +1,45 @@ +
+
+ +
+ +
{{ userFullname }}
+
+
+ {{ isMine + ? ('addon.messages.you' | translate) + : userFullname }} +
+ + + +
+ +
+
+ + {{ time | coreFormatDate: 'strftimetime' }} + + + + + {{ 'core.deletedoffline' | translate }} + + + +
+ + + + + + + +
+ +
+
diff --git a/src/core/components/message/message.scss b/src/core/components/message/message.scss new file mode 100644 index 000000000..64b90e61c --- /dev/null +++ b/src/core/components/message/message.scss @@ -0,0 +1,138 @@ +@import "~theme/globals"; + +:host { + --message-background: var(--core-messages-message-bg); + --message-activated-background: var(--core-messages-message-activated-bg); + --message-alignment: flex-start; + + display: flex; + justify-content: var(--message-alignment); + + + .message-box { + --background: var(--message-background); + --min-height: var(--a11y-min-target-size); + + display: flex; + flex-direction: row; + position: relative; + + border: 0; + border-radius: var(--medium-radius); + margin: 8px; + width: 90%; + max-width: var(--list-item-max-width); + min-height: 36px; + + font-size: var(--text-size); + color: var(--ion-text-color); + + background: var(--message-background); + @include core-transition(width); + + // This is needed to display bubble tails. + overflow: visible; + + &:hover { + -webkit-filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3)); + filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3)); + } + + &[tappable]:active { + --message-background: var(--message-activated-background); + } + + .main { + padding: 8px; + flex-grow: 1; + + .message-user { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: .5rem; + margin-top: 0; + color: var(--ion-text-color); + + core-user-avatar { + display: block; + --core-avatar-size: var(--core-messages-avatar-size); + margin: 0; + } + + div { + font-weight: 500; + flex-grow: 1; + padding-left: .5rem; + padding-right: .5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 16px; + } + } + + .message-text { + ::ng-deep > p:only-child { + display: inline; + margin: 0; + } + } + } + + .extra { + flex-shrink: 1; + display: flex; + flex-direction: row; + padding-left: 8px; + padding-right: 8px; + + .message-time { + padding-top: 8px; + color: var(--core-messages-message-note-text); + font-size: var(--core-messages-message-note-font-size); + } + + .delete-button { + min-height: initial; + line-height: initial; + margin: 0px; + align-self: flex-end; + + ::ng-deep ion-icon { + font-size: 1.2em; + } + } + } + + .tail { + content: ''; + width: 0; + height: 0; + border: 0.5rem solid transparent; + position: absolute; + touch-action: none; + bottom: 0; + border-bottom-color: var(--message-background); + @include position(null, null, null, -8px); + } + } + + &.no-user .message-box { + margin-top: 0px; + } + + &.is-mine { + // Defined when a message is the user's. + --message-background: var(--core-messages-message-mine-bg); + --message-activated-background: var(--core-messages-message-mine-activated-bg); + --message-alignment: flex-end; + + .message-box { + .tail { + @include position(null, -8px, null, unset); + } + } + } +} diff --git a/src/core/components/message/message.ts b/src/core/components/message/message.ts new file mode 100644 index 000000000..818638aef --- /dev/null +++ b/src/core/components/message/message.ts @@ -0,0 +1,121 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ContextLevel } from '@/core/constants'; +import { Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core'; +import { CoreAnimations } from '@components/animations'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar'; + +/** + * Component to handle a message in a conversation. + */ +@Component({ + selector: 'core-message', + templateUrl: 'message.html', + styleUrls: ['message.scss'], + animations: [CoreAnimations.SLIDE_IN_OUT], +}) +export class CoreMessageComponent implements OnInit { + + @Input() message?: CoreMessageData; // The message object. + @Input() user?: CoreUserWithAvatar; // The user object. + + @Input() text = ''; // Message text. + @Input() time = 0; // Message time. + @Input() instanceId = 0; + @Input() courseId?: number; + @Input() contextLevel: ContextLevel = ContextLevel.SYSTEM; + @Input() showDelete = false; + @Output() onDeleteMessage = new EventEmitter(); + @Output() onUndoDeleteMessage = new EventEmitter(); + @Output() afterRender = new EventEmitter(); + + protected deleted = false; // Needed to fix animation to void in Behat tests. + + // @TODO Recover the animation using native css or wait for Angular 13.1 + // where the bug https://github.com/angular/angular/issues/30693 is solved. + // @HostBinding('@coreSlideInOut') get animation(): string { + // return this.isMine ? '' : 'fromLeft'; + // } + + @HostBinding('class.is-mine') isMine = false; + + @HostBinding('class.no-user') get showUser(): boolean { + return !this.message?.showUserData; + }; + + get userId(): number | undefined { + return this.user && (this.user.userid || this.user.id); + } + + get userFullname(): string | undefined { + return this.user && (this.user.fullname || this.user.userfullname); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + const currentUserId = CoreSites.getCurrentSiteUserId(); + + this.isMine = this.userId === currentUserId; + } + + /** + * Emits the delete action. + * + * @param event Event. + */ + delete(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.onDeleteMessage.emit(); + } + + /** + * Emits the undo delete action. + * + * @param event Event. + */ + undoDelete(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.onUndoDeleteMessage.emit(); + + } + + /** + * Copy message to clipboard. + */ + copyMessage(): void { + CoreUtils.copyToClipboard(CoreTextUtils.decodeHTMLEntities(this.text)); + } + +} + +/** + * Conversation message with some calculated data. + */ +type CoreMessageData = { + pending?: boolean; // Whether the message is pending to be sent. + sending?: boolean; // Whether the message is being sent right now. + showDate?: boolean; // Whether to show the date before the message. + deleted?: boolean; // Whether the message has been deleted. + showUserData?: boolean; // Whether to show the user data in the message. + showTail?: boolean; // Whether to show a "tail" in the message. + delete?: boolean; // Permission to delete=true/false. +}; diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index 96f7afd80..85a087c19 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -156,7 +156,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { /** * Type with all possible formats of user. */ -type CoreUserWithAvatar = CoreUserBasicData & { +export type CoreUserWithAvatar = CoreUserBasicData & { userpictureurl?: string; userprofileimageurl?: string; profileimageurlsmall?: string; diff --git a/src/core/features/comments/pages/viewer/viewer.html b/src/core/features/comments/pages/viewer/viewer.html index 95127219c..0df89ba27 100644 --- a/src/core/features/comments/pages/viewer/viewer.html +++ b/src/core/features/comments/pages/viewer/viewer.html @@ -45,71 +45,20 @@ {{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}

- - - -

- - -
{{ comment.fullname }}
-

- -
- - -
-
- - - {{ comment.timecreated * 1000 | coreFormatDate: 'strftimetime' }} - - - - {{ 'core.deletedoffline' | translate }} - - - -
- - - - - - -
+ +
- - - -

- - {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} -

- -
- - -
-
- - {{ 'core.notsent' | translate }} - -
- - - -
+ + + {{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }} + + +
diff --git a/src/core/features/comments/pages/viewer/viewer.page.ts b/src/core/features/comments/pages/viewer/viewer.page.ts index 6c92436e0..4b79eedfe 100644 --- a/src/core/features/comments/pages/viewer/viewer.page.ts +++ b/src/core/features/comments/pages/viewer/viewer.page.ts @@ -14,7 +14,6 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreAnimations } from '@components/animations'; import { ActivatedRoute } from '@angular/router'; import { CoreSites } from '@services/sites'; import { @@ -30,7 +29,7 @@ import { import { IonContent, IonRefresher } from '@ionic/angular'; import { ContextLevel, CoreConstants } from '@/core/constants'; import { CoreNavigator } from '@services/navigator'; -import { Network, NgZone, Translate } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUser } from '@features/user/services/user'; @@ -43,6 +42,7 @@ import { CoreApp } from '@services/app'; import { CoreNetwork } from '@services/network'; import moment from 'moment'; import { Subscription } from 'rxjs'; +import { CoreAnimations } from '@components/animations'; /** * Page that displays comments. @@ -75,7 +75,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { hasOffline = false; refreshIcon = CoreConstants.ICON_LOADING; syncIcon = CoreConstants.ICON_LOADING; - offlineComment?: CoreCommentsOfflineWithUser; + offlineComment?: CoreCommentsOfflineWithUser & { pending?: boolean }; currentUserId: number; sending = false; newComment = ''; @@ -110,7 +110,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { }, CoreSites.getCurrentSiteId()); this.isOnline = CoreNetwork.isOnline(); - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); @@ -222,13 +222,15 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. * @return Resolved when done. */ - loadPrevious(infiniteComplete?: () => void): Promise { + async loadPrevious(infiniteComplete?: () => void): Promise { this.page++; this.canLoadMore = false; - return this.fetchComments(true).finally(() => { + try { + await this.fetchComments(true); + } finally { infiniteComplete && infiniteComplete(); - }); + } } /** @@ -359,13 +361,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { /** * Delete a comment. * - * @param e Click event. * @param comment Comment to delete. */ - async deleteComment(e: Event, comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise { - e.preventDefault(); - e.stopPropagation(); - + async deleteComment(comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise { const modified = 'lastmodified' in comment ? comment.lastmodified : comment.timecreated; @@ -527,15 +525,16 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { ).then(async (offlineComment) => { this.offlineComment = offlineComment; - if (!offlineComment) { + if (!this.offlineComment) { return; } if (this.newComment == '') { - this.newComment = this.offlineComment!.content; + this.newComment = this.offlineComment.content; } - this.offlineComment!.userid = this.currentUserId; + this.offlineComment.userid = this.currentUserId; + this.offlineComment.pending = true; return; })); @@ -571,13 +570,9 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { /** * Restore a comment. * - * @param e Click event. * @param comment Comment to delete. */ - async undoDeleteComment(e: Event, comment: CoreCommentsDataToDisplay): Promise { - e.preventDefault(); - e.stopPropagation(); - + async undoDeleteComment(comment: CoreCommentsDataToDisplay): Promise { await CoreCommentsOffline.undoDeleteComment(comment.id); comment.deleted = false; diff --git a/src/core/features/comments/pages/viewer/viewer.scss b/src/core/features/comments/pages/viewer/viewer.scss index 7fb2d0cc4..cf231da48 100644 --- a/src/core/features/comments/pages/viewer/viewer.scss +++ b/src/core/features/comments/pages/viewer/viewer.scss @@ -1 +1,5 @@ @import "~theme/components/discussion.scss"; + +ion-badge { + margin: 8px auto; +} diff --git a/src/core/features/comments/tests/behat/basic_usage.feature b/src/core/features/comments/tests/behat/basic_usage.feature new file mode 100644 index 000000000..86a424627 --- /dev/null +++ b/src/core/features/comments/tests/behat/basic_usage.feature @@ -0,0 +1,327 @@ +@core @core_comments @app @javascript +Feature: Test basic usage of comments in app + In order to participate in the comments while using the mobile app + As a student + I need basic comments functionality to work + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | teacher | teacher1@example.com | + | student1 | Student | student | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | mainglossary | allowcomments | assessed | scale | + | glossary | Test glossary | glossary description | C1 | gloss1 | 1 | 1 | 1 | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | comments | + | data | Data | Data info | C1 | data1 | 1 | + + Scenario: Add comments & Delete comments (database) + # Create database entry and comment as a teacher + Given I entered the data activity "Data" on course "Course 1" as "teacher1" in the app + And I press "Information" in the app + # TODO Create and use a generator for database fields. + And I press "Open in browser" in the app + And I switch to the browser tab opened by the app + And I log in as "teacher1" + And I add a "Text input" field to "Data" database and I fill the form with: + | Field name | Test field name | + | 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 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 set the field "Add a comment..." to "comment test teacher" in the app + And I press "Send" in the app + Then I should find "Comment created" in the app + And I should find "comment test teacher" in the app + + When I press the back button in the app + And I should find "Comments (1)" in the app + + # Create and delete comments as a student + Given I entered the data activity "Data" on course "Course 1" as "student1" in the app + And I press "More" in the app + And I press "Comments (1)" in the app + And I set the field "Add a comment..." to "comment test student" in the app + And I press "Send" in the app + Then I should find "Comment created" in the app + And I should find "comment test teacher" in the app + And I should find "comment test student" in the app + + When I press the back button in the app + And I press "Comments (2)" in the app + And I press "Toggle delete buttons" in the app + And I press "Delete" near "comment test student" in the app + And I press "Delete" near "Cancel" in the app + Then I should find "Comment deleted" in the app + And I should find "comment test teacher" in the app + But I should not see "comment test student" + + When I press the back button in the app + Then I should find "Comments (1)" in the 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 "Open in browser" in the app + And I switch to the browser tab opened by the app + And I log in as "teacher1" + And I add a "Text input" field to "Data" database and I fill the form with: + | Field name | Test field name | + | Field description | Test field description | + And I press "Save" + And I close the browser tab opened by 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 + 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 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 + And I should find "There are offline comments to be synchronised." in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should find "comment test" in the app + But I should not see "There are offline comments to be synchronised." + + 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 press "Toggle delete buttons" in the app + And I press "Delete" in the app + And I press "Delete" near "Cancel" in the app + Then I should find "Comment deleted" in the app + And I should find "There are offline comments to be synchronised." in the app + And I should find "Deleted offline" in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should not see "There are offline comments to be synchronised." + And I should not see "comment test" + + When I press the back button in the app + And I should find "Comments (0)" in the app + + Scenario: Add comments & delete comments (glossary) + # Create glossary entry and comment as a teacher + Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher1" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | potato | + | Definition | The potato is a root vegetable native to the Americas, a starchy tuber of the plant Solanum tuberosum, and the plant itself, a perennial in the family Solanaceae. | + And I press "Save" in the app + And I press "potato" in the app + And I press "Comments (0)" in the app + And I set the field "Add a comment..." to "comment test teacher" in the app + And I press "Send" in the app + Then I should find "Comment created" in the app + And I should find "comment test teacher" in the app + And I press the back button in the app + And I should find "Comments (1)" in the app + + # Create and delete comments as a student + When I entered the course "Course 1" as "student1" in the app + And I press "Test glossary" in the app + And I press "potato" in the app + And I press "Comments (1)" in the app + And I set the field "Add a comment..." to "comment test student" in the app + And I press "Send" in the app + Then I should find "Comment created" in the app + And I should find "comment test teacher" in the app + And I should find "comment test student" in the app + + When I press the back button in the app + And I press "Comments (2)" in the app + And I press "Toggle delete buttons" in the app + And I press "Delete" near "comment test student" in the app + And I press "Delete" near "Cancel" in the app + Then I should find "Comment deleted" in the app + And I should find "comment test teacher" in the app + But I should not see "comment test student" + + When I press the back button in the app + And I should find "Comments (1)" in the app + + Scenario: Add comments offline & Delete comments offline & Sync comments (glossary) + Given I entered the glossary activity "Test glossary" on course "Course 1" as "teacher1" in the app + And I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | potato | + | Definition | The potato is a root vegetable native to the Americas, a starchy tuber of the plant Solanum tuberosum, and the plant itself, a perennial in the family Solanaceae. | + 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 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 + And I should find "There are offline comments to be synchronised." in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should find "comment test" in the app + But I should not see "There are offline comments to be synchronised." + + 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 press "Toggle delete buttons" in the app + And I press "Delete" in the app + And I press "Delete" near "Cancel" in the app + Then I should find "Comment deleted" in the app + And I should find "There are offline comments to be synchronised." in the app + And I should find "Deleted offline" in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should not see "There are offline comments to be synchronised." + And I should not see "comment test" + + When I press the back button in the app + And I should find "Comments (0)" in the app + + Scenario: Add comments & Delete comments (blogs) + # Create blog as a teacher + Given the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata | + | blog_menu | Course | C1 | course-view-* | site-pre | | + And I entered the course "Course 1" as "teacher1" in the app + And I press "Course summary" in the app + # TODO Create and use a generator blog entries. + And I press "Open in browser" in the app + And I switch to the browser tab opened by the app + And I log in as "teacher1" + And I click on "Open block drawer" "button" + And I click on "Add an entry about this course" "link" in the "Blog menu" "block" + And I set the following fields to these values: + | Entry title | Blog test | + | Blog entry body | Blog body | + And I press "Save changes" + And I close the browser tab opened by the app + + # Create and delete comments as a student + When I entered the app as "student1" + And I press the more menu button in the app + And I press "Site blog" in the app + Then I should find "Blog test" in the app + And I should find "Blog body" in the app + + When I press "Comments (0)" in the app + 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 "Comment created" in the app + And I should find "comment test" in the app + + When I press the back button in the app + And I press "Comments (1)" in the app + 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 + Then I should find "Comment deleted" in the app + But I should not see "comment test" + + When I press the back button in the app + Then I should find "Comments (0)" in the app + + Scenario: Add comments offline & Delete comments offline & Sync comments (blogs) + # Create blog as a teacher + Given the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata | + | blog_menu | Course | C1 | course-view-* | site-pre | | + And I entered the course "Course 1" as "teacher1" in the app + And I press "Course summary" in the app + And I press "Open in browser" in the app + And I switch to the browser tab opened by the app + And I log in as "teacher1" + And I click on "Open block drawer" "button" + And I click on "Add an entry about this course" "link" in the "Blog menu" "block" + And I set the following fields to these values: + | Entry title | Blog test | + | Blog entry body | Blog body | + And I press "Save changes" + And I close the browser tab opened by the app + + # Create and delete comments as a student + When I entered the app as "student1" + And I press the more menu button in the app + And I press "Site blog" in the app + Then I should find "Blog test" in the 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 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 + And I should find "There are offline comments to be synchronised." in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should find "comment test" in the app + But I should not see "There are offline comments to be synchronised." + + 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 press "Toggle delete buttons" in the app + And I press "Delete" in the app + And I press "Delete" near "Cancel" in the app + Then I should find "Comment deleted" in the app + And I should find "There are offline comments to be synchronised." in the app + And I should find "Deleted offline" in the app + And I should find "comment test" in the 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 press "Display options" in the app + And I press "Synchronise now" in the app + And I close the popup in the app + Then I should not see "There are offline comments to be synchronised." + And I should not see "comment test" + + When I press the back button in the app + Then I should find "Comments (0)" in the app diff --git a/src/core/features/course/components/module-summary/module-summary.ts b/src/core/features/course/components/module-summary/module-summary.ts index 1cc0e43db..dd3c6cbb6 100644 --- a/src/core/features/course/components/module-summary/module-summary.ts +++ b/src/core/features/course/components/module-summary/module-summary.ts @@ -30,7 +30,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { ModalController, Network, NgZone } from '@singletons'; +import { ModalController, NgZone } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; @@ -82,7 +82,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy { this.isOnline = CoreNetwork.isOnline(); // Refresh online status when changes. - this.onlineSubscription = Network.onChange().subscribe(() => { + this.onlineSubscription = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); diff --git a/src/core/features/courses/tests/behat/basic_usage.feature b/src/core/features/courses/tests/behat/basic_usage.feature index 14b58707b..b61cbd65d 100755 --- a/src/core/features/courses/tests/behat/basic_usage.feature +++ b/src/core/features/courses/tests/behat/basic_usage.feature @@ -10,20 +10,23 @@ Feature: Test basic usage of courses in app | teacher1 | Teacher | teacher | teacher1@example.com | | student1 | Student | student | student1@example.com | And the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | - | Course 2 | C2 | 0 | - | Course 3 | C3 | 0 | - | Course 4 | C4 | 0 | + | fullname | shortname | category | visible | + | Course 1 | C1 | 0 | 1 | + | Course 2 | C2 | 0 | 1 | + | Course 3 | C3 | 0 | 1 | + | Course 4 | C4 | 0 | 1 | + | Hidden course | CH | 0 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | teacher1 | C3 | editingteacher | | teacher1 | C4 | editingteacher | + | teacher1 | CH | editingteacher | | student1 | C1 | student | | student1 | C2 | student | | student1 | C3 | student | + | student1 | CH | student | And the following "activities" exist: | activity | name | intro | course | idnumber | option | | choice | Choice course 1 | Test choice description | C1 | choice1 | Option 1, Option 2, Option 3 | @@ -49,6 +52,18 @@ Feature: Test basic usage of courses in app And I should find "Course 2" in the app And I should find "Course 3" in the app + @lms_from4.0 + Scenario: Hidden course is only accessible for teachers + Given I entered the app as "teacher1" + And I press "My courses" in the app + When I press "Hidden course" in the app + Then the header should be "Hidden course" in the app + + Given I entered the app as "student1" + And I press "My courses" in the app + And I should not find "Hidden course" in the app + + @lms_from4.0 Scenario: See my courses Given I entered the app as "student1" diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index 03d2e211f..17538020d 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -81,8 +81,8 @@ import { FileTransferMock } from './services/file-transfer'; import { GeolocationMock } from './services/geolocation'; import { InAppBrowserMock } from './services/inappbrowser'; import { MediaCaptureMock } from './services/media-capture'; -import { NetworkMock } from './services/network'; import { ZipMock } from './services/zip'; +import { CoreNetworkService } from '@services/network'; /** * This module handles the emulation of Cordova plugins in browser and desktop. @@ -152,11 +152,7 @@ import { ZipMock } from './services/zip'; deps: [Platform], useFactory: (platform: Platform): MediaCapture => platform.is('cordova') ? new MediaCapture() : new MediaCaptureMock(), }, - { - provide: Network, - deps: [Platform], - useFactory: (platform: Platform): Network => platform.is('cordova') ? new Network() : new NetworkMock(), - }, + CoreNetworkService, Push, QRScanner, SplashScreen, diff --git a/src/core/features/emulator/services/network.ts b/src/core/features/emulator/services/network.ts deleted file mode 100644 index cca7dab97..000000000 --- a/src/core/features/emulator/services/network.ts +++ /dev/null @@ -1,81 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Injectable } from '@angular/core'; -import { Network } from '@ionic-native/network/ngx'; -import { Observable, Subject, merge } from 'rxjs'; - -/** - * Emulates the Cordova Network plugin in browser. - */ -@Injectable() -export class NetworkMock extends Network { - - type!: string; - - protected connectObservable = new Subject<'connected'>(); - protected disconnectObservable = new Subject<'disconnected'>(); - - constructor() { - super(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-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 - }; - - window.addEventListener('online', () => { - this.connectObservable.next('connected'); - }, false); - - window.addEventListener('offline', () => { - this.disconnectObservable.next('disconnected'); - }, false); - } - - /** - * Returns an observable to watch connection changes. - * - * @return Observable. - */ - onChange(): Observable<'connected' | 'disconnected'> { - return merge(this.connectObservable, this.disconnectObservable); - } - - /** - * Returns an observable to notify when the app is connected. - * - * @return Observable. - */ - onConnect(): Observable<'connected'> { - return this.connectObservable; - } - - /** - * Returns an observable to notify when the app is disconnected. - * - * @return Observable. - */ - onDisconnect(): Observable<'disconnected'> { - return this.disconnectObservable; - } - -} diff --git a/src/core/features/login/tests/behat/basic_usage.feature b/src/core/features/login/tests/behat/basic_usage.feature index a558d1f74..d03a08844 100755 --- a/src/core/features/login/tests/behat/basic_usage.feature +++ b/src/core/features/login/tests/behat/basic_usage.feature @@ -32,8 +32,9 @@ Feature: Test basic usage of login in app And I press "Connect to your site" in the app Then I should find "Acceptance test site" in the app - When I set the field "Username" to "student1" in the app - And I set the field "Password" to "student1" in the app + When I set the following fields to these values in the app: + | Username | student1 | + | Password | student1 | And I press "Log in" near "Forgotten your username or password?" in the app Then I should find "Acceptance test site" in the app But I should not find "Log in" in the app diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index ddd86d114..cae2c2b49 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -431,7 +431,7 @@ export class CorePushNotificationsProvider { /** * Function called when a push notification is clicked. Redirect the user to the right state. * - * @param notification Notification. + * @param data Notification data. * @return Promise resolved when done. */ async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts index 397d8542c..cae87a52d 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts @@ -16,7 +16,7 @@ import { CoreApp } from '@services/app'; import { Component, OnDestroy } from '@angular/core'; import { CoreConstants } from '@/core/constants'; import { CoreLocalNotifications } from '@services/local-notifications'; -import { Device, Platform, Translate, Network, NgZone } from '@singletons'; +import { Device, Platform, Translate, NgZone } from '@singletons'; import { CoreLang } from '@services/lang'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; @@ -82,7 +82,6 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { protected onlineObserver?: Subscription; constructor() { - const appProvider = CoreApp.instance; const sitesProvider = CoreSites.instance; const device = Device.instance; const translate = Translate.instance; @@ -112,10 +111,10 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { if (CorePlatform.isMobile()) { this.deviceInfo.deviceType = Platform.is('tablet') ? 'tablet' : 'phone'; - if (appProvider.isAndroid()) { + if (CoreApp.isAndroid()) { this.deviceInfo.deviceOs = 'android'; this.deviceOsTranslated = 'Android'; - } else if (appProvider.isIOS()) { + } else if (CoreApp.isIOS()) { this.deviceInfo.deviceOs = 'ios'; this.deviceOsTranslated = 'iOS'; } else { @@ -177,7 +176,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy { this.deviceInfo.siteVersion = currentSite?.getInfo()?.release; // Refresh online status when changes. - this.onlineObserver = Network.onChange().subscribe(() => { + this.onlineObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.deviceInfo.networkStatus = CoreNetwork.isOnline() ? 'online' : 'offline'; diff --git a/src/core/initializers/prepare-automated-tests.ts b/src/core/initializers/prepare-automated-tests.ts index a0c9fdc44..ff773f9aa 100644 --- a/src/core/initializers/prepare-automated-tests.ts +++ b/src/core/initializers/prepare-automated-tests.ts @@ -12,35 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ApplicationRef, NgZone as NgZoneService } from '@angular/core'; -import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; -import { CoreApp, CoreAppProvider } from '@services/app'; -import { CoreConfig, CoreConfigProvider } from '@services/config'; -import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; +import { CoreAppProvider } from '@services/app'; import { CoreDB, CoreDbProvider } from '@services/db'; -import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; -import { Application, NgZone } from '@singletons'; type AutomatedTestsWindow = Window & { - appRef?: ApplicationRef; - appProvider?: CoreAppProvider; dbProvider?: CoreDbProvider; - configProvider?: CoreConfigProvider; - cronProvider?: CoreCronDelegateService; - ngZone?: NgZoneService; - pushNotifications?: CorePushNotificationsProvider; - urlSchemes?: CoreCustomURLSchemesProvider; }; function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) { - window.appRef = Application.instance; - window.appProvider = CoreApp.instance; window.dbProvider = CoreDB.instance; - window.configProvider = CoreConfig.instance; - window.cronProvider = CoreCronDelegate.instance; - window.ngZone = NgZone.instance; - window.pushNotifications = CorePushNotifications.instance; - window.urlSchemes = CoreCustomURLSchemes.instance; } export default function(): void { diff --git a/src/core/initializers/watch-network.ts b/src/core/initializers/watch-network.ts index ee867a42e..9d8aabf5f 100644 --- a/src/core/initializers/watch-network.ts +++ b/src/core/initializers/watch-network.ts @@ -13,11 +13,12 @@ // limitations under the License. import { CoreCronDelegate } from '@services/cron'; -import { Network, NgZone } from '@singletons'; +import { NgZone } from '@singletons'; +import { CoreNetwork } from '@services/network'; export default function(): void { // When the app is re-connected, start network handlers that were stopped. - Network.onConnect().subscribe(() => { + CoreNetwork.onConnect().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => CoreCronDelegate.startNetworkHandlers()); }); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 3de5be814..d954aff62 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -31,7 +31,7 @@ import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; -import { ApplicationInit, makeSingleton, Network, NgZone, Translate } from '@singletons'; +import { ApplicationInit, makeSingleton, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { APP_SCHEMA, @@ -150,7 +150,7 @@ export class CoreFilepoolProvider { this.checkQueueProcessing(); // Start queue when device goes online. - Network.onConnect().subscribe(() => { + CoreNetwork.onConnect().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => this.checkQueueProcessing()); }); diff --git a/src/core/services/network.ts b/src/core/services/network.ts index 7673e2007..d400f4fb0 100644 --- a/src/core/services/network.ts +++ b/src/core/services/network.ts @@ -14,15 +14,56 @@ import { Injectable } from '@angular/core'; import { CorePlatform } from '@services/platform'; -import { makeSingleton, Network } from '@singletons'; +import { Network as NetworkService } from '@ionic-native/network/ngx'; +import { makeSingleton } from '@singletons'; +import { Observable, Subject, merge } from 'rxjs'; + +const Network = makeSingleton(NetworkService); /** - * Service to manage network information. + * Service to manage network connections. */ @Injectable({ providedIn: 'root' }) -export class CoreNetworkService { +export class CoreNetworkService extends NetworkService { + type!: string; + + protected connectObservable = new Subject<'connected'>(); + protected disconnectObservable = new Subject<'disconnected'>(); protected forceOffline = false; + protected online = false; + + constructor() { + super(); + + this.checkOnline(); + + if (CorePlatform.isMobile()) { + Network.onChange().subscribe(() => { + this.fireObservable(); + }); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-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 + }; + + window.addEventListener('online', () => { + this.fireObservable(); + }, false); + + window.addEventListener('offline', () => { + this.fireObservable(); + }, false); + } + } /** * Set value of forceOffline flag. If true, the app will think the device is offline. @@ -31,6 +72,7 @@ export class CoreNetworkService { */ setForceOffline(value: boolean): void { this.forceOffline = !!value; + this.fireObservable(); } /** @@ -39,23 +81,77 @@ export class CoreNetworkService { * @return Whether the app is online. */ isOnline(): boolean { + return this.online; + } + + /** + * Returns whether we are online. + * + * @return Whether the app is online. + */ + checkOnline(): void { if (this.forceOffline) { - return false; + this.online = false; + + return; } if (!CorePlatform.isMobile()) { - return navigator.onLine; + this.online = navigator.onLine; + + return; } - let online = Network.type !== null && Network.type != Network.Connection.NONE && - Network.type != Network.Connection.UNKNOWN; + let online = this.type !== null && this.type != this.Connection.NONE && + this.type != this.Connection.UNKNOWN; // Double check we are not online because we cannot rely 100% in Cordova APIs. if (!online && navigator.onLine) { online = true; } - return online; + this.online = online; + } + + /** + * Returns an observable to watch connection changes. + * + * @return Observable. + */ + onChange(): Observable<'connected' | 'disconnected'> { + return merge(this.connectObservable, this.disconnectObservable); + } + + /** + * Returns an observable to notify when the app is connected. + * + * @return Observable. + */ + onConnect(): Observable<'connected'> { + return this.connectObservable; + } + + /** + * Returns an observable to notify when the app is disconnected. + * + * @return Observable. + */ + onDisconnect(): Observable<'disconnected'> { + return this.disconnectObservable; + } + + /** + * Fires the correct observable depending on the connection status. + */ + protected fireObservable(): void { + const previousOnline = this.online; + + this.checkOnline(); + if (this.online && !previousOnline) { + this.connectObservable.next('connected'); + } else if (!this.online && previousOnline) { + this.disconnectObservable.next('disconnected'); + } } /** diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 2cbd83068..5b46db3c9 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -25,7 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton, Network, NgZone, Translate, Diagnostic } from '@singletons'; +import { makeSingleton, NgZone, Translate, Diagnostic } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; @@ -76,7 +76,7 @@ export class CoreIframeUtilsProvider { this.addOfflineWarning(element, src, isSubframe); // If the network changes, check it again. - const subscription = Network.onConnect().subscribe(() => { + const subscription = CoreNetwork.onConnect().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { if (!this.checkOnlineFrameInOffline(element, isSubframe)) { diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 048dba0c6..695442589 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -187,6 +187,9 @@ export const LocalNotifications = makeSingleton(LocalNotificationsService); export const Media = makeSingleton(MediaService); export const MediaCapture = makeSingleton(MediaCaptureService); export const NativeHttp = makeSingleton(HTTP); +/** + * @deprecated on 4.1 use CoreNetwork instead. + */ export const Network = makeSingleton(NetworkService); export const Push = makeSingleton(PushService); export const QRScanner = makeSingleton(QRScannerService); diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index e3d892407..1292f231a 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CorePromisedValue } from '@classes/promised-value'; import { CoreUtils } from '@services/utils/utils'; import { NgZone } from '@singletons'; -import { TestsBehatBlocking } from './behat-blocking'; import { TestBehatElementLocator } from './behat-runtime'; // Containers that block containers behind them. @@ -82,7 +82,7 @@ export class TestsBehatDomUtils { * @return Elements containing the given text with exact boolean. */ protected static findElementsBasedOnTextWithinWithExact(container: HTMLElement, text: string): ElementsWithExact[] { - const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"]`; + const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"], [placeholder*="${text}"]`; const elements = Array.from(container.querySelectorAll(attributesSelector)) .filter((element => this.isElementVisible(element, container))) @@ -104,7 +104,9 @@ export class TestsBehatDomUtils { } if (node instanceof HTMLElement && - (node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none')) { + (node.getAttribute('aria-hidden') === 'true' || + node.getAttribute('aria-disabled') === 'true' || + getComputedStyle(node).display === 'none')) { return NodeFilter.FILTER_REJECT; } @@ -176,7 +178,8 @@ export class TestsBehatDomUtils { protected static checkElementLabel(element: HTMLElement, text: string): boolean { return element.title === text || element.getAttribute('alt') === text || - element.getAttribute('aria-label') === text; + element.getAttribute('aria-label') === text || + element.getAttribute('placeholder') === text; } /** @@ -219,7 +222,7 @@ export class TestsBehatDomUtils { } return Array.from(uniqueElements); - }; + } /** * Get parent element, including Shadow DOM parents. @@ -359,7 +362,7 @@ export class TestsBehatDomUtils { * Function to find elements based on their text or Aria label. * * @param locator Element locator. - * @param container Container to search in. + * @param topContainer Container to search in. * @return Found elements */ protected static findElementsBasedOnTextInContainer( @@ -377,7 +380,7 @@ export class TestsBehatDomUtils { const withinElementsAncestors = this.getTopAncestors(withinElements); if (withinElementsAncestors.length > 1) { - throw new Error('Too many matches for within text'); + throw new Error('Too many matches for within text ('+withinElementsAncestors.length+')'); } topContainer = container = withinElementsAncestors[0]; @@ -395,7 +398,7 @@ export class TestsBehatDomUtils { const nearElementsAncestors = this.getTopAncestors(nearElements); if (nearElementsAncestors.length > 1) { - throw new Error('Too many matches for near text'); + throw new Error('Too many matches for near text ('+nearElementsAncestors.length+')'); } container = this.getParentElement(nearElementsAncestors[0]); @@ -444,21 +447,23 @@ export class TestsBehatDomUtils { element.scrollIntoView(false); - return new Promise((resolve): void => { - requestAnimationFrame(() => { - const rect = element.getBoundingClientRect(); + const promise = new CorePromisedValue(); - if (initialRect.y !== rect.y) { - setTimeout(() => { - resolve(rect); - }, 300); + requestAnimationFrame(() => { + const rect = element.getBoundingClientRect(); - return; - } + if (initialRect.y !== rect.y) { + setTimeout(() => { + promise.resolve(rect); + }, 300); - resolve(rect); - }); + return; + } + + promise.resolve(rect); }); + + return promise; }; /** @@ -467,8 +472,8 @@ export class TestsBehatDomUtils { * @param element Element to press. */ static async pressElement(element: HTMLElement): Promise { - NgZone.run(async () => { - const blockKey = TestsBehatBlocking.block(); + await NgZone.run(async () => { + const promise = new CorePromisedValue(); // Events don't bubble up across Shadow DOM boundaries, and some buttons // may not work without doing this. @@ -498,8 +503,10 @@ export class TestsBehatDomUtils { element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); element.click(); - TestsBehatBlocking.unblock(blockKey); + promise.resolve(); }, 300); + + return promise; }); } @@ -509,22 +516,33 @@ export class TestsBehatDomUtils { * @param element HTML to set. * @param value Value to be set. */ - static async setElementValue(element: HTMLElement, value: string): Promise { - NgZone.run(async () => { - const blockKey = TestsBehatBlocking.block(); + static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise { + await NgZone.run(async () => { + const promise = new CorePromisedValue(); // Functions to get/set value depending on field type. - let setValue = (text: string) => { - element.innerHTML = text; - }; - let getValue = () => element.innerHTML; + const setValue = (text: string) => { + if (element.tagName === 'ION-SELECT' && 'value' in element) { + value = value.trim(); + const optionValue = Array.from(element.querySelectorAll('ion-select-option')) + .find((option) => option.innerHTML.trim() === value); - if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { - setValue = (text: string) => { + if (optionValue) { + element.value = optionValue.value; + } + } else if ('value' in element) { element.value = text; - }; - getValue = () => element.value; - } + } else { + element.innerHTML = text; + } + }; + const getValue = () => { + if ('value' in element) { + return element.value; + } else { + return element.innerHTML; + } + }; // Pretend we have cut and pasted the new text. let event: InputEvent; @@ -555,7 +573,9 @@ export class TestsBehatDomUtils { element.dispatchEvent(event); } - TestsBehatBlocking.unblock(blockKey); + promise.resolve(); + + return promise; }); } diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 97ba8a313..c99c0b996 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -18,6 +18,16 @@ import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreConfig } from '@services/config'; import { EnvironmentConfig } from '@/types/config'; +import { NgZone } from '@singletons'; +import { CoreNetwork } from '@services/network'; +import { + CorePushNotifications, + CorePushNotificationsNotificationBasicData, +} from '@features/pushnotifications/services/pushnotifications'; +import { CoreCronDelegate } from '@services/cron'; +import { CoreLoadingComponent } from '@components/loading/loading'; +import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CoreDom } from '@singletons/dom'; /** * Behat runtime servive with public API. @@ -45,6 +55,10 @@ export class TestsBehatRuntime { scrollTo: TestsBehatRuntime.scrollTo, setField: TestsBehatRuntime.setField, handleCustomURL: TestsBehatRuntime.handleCustomURL, + notificationClicked: TestsBehatRuntime.notificationClicked, + forceSyncExecution: TestsBehatRuntime.forceSyncExecution, + waitLoadingToFinish: TestsBehatRuntime.waitLoadingToFinish, + network: CoreNetwork.instance, }; if (!options) { @@ -69,26 +83,69 @@ export class TestsBehatRuntime { * @return OK if successful, or ERROR: followed by message. */ static async handleCustomURL(url: string): Promise { - const blockKey = TestsBehatBlocking.block(); - try { - await CoreCustomURLSchemes.handleCustomURL(url); + await NgZone.run(async () => { + await CoreCustomURLSchemes.handleCustomURL(url); + }); return 'OK'; } catch (error) { return 'ERROR: ' + error.message; + } + } + + /** + * Function called when a push notification is clicked. Redirect the user to the right state. + * + * @param data Notification data. + * @return Promise resolved when done. + */ + static async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { + const blockKey = TestsBehatBlocking.block(); + + try { + await NgZone.run(async () => { + await CorePushNotifications.notificationClicked(data); + }); } finally { TestsBehatBlocking.unblock(blockKey); } } + /** + * Force execution of synchronization cron tasks without waiting for the scheduled time. + * Please notice that some tasks may not be executed depending on the network connection and sync settings. + * + * @return Promise resolved if all handlers are executed successfully, rejected otherwise. + */ + static async forceSyncExecution(): Promise { + await NgZone.run(async () => { + await CoreCronDelegate.forceSyncExecution(); + }); + } + + /** + * Wait all controlled components to be rendered. + * + * @return Promise resolved when all components have been rendered. + */ + static async waitLoadingToFinish(): Promise { + await NgZone.run(async () => { + const elements = Array.from(document.body.querySelectorAll('core-loading')) + .filter((element) => CoreDom.isElementVisible(element)); + + await Promise.all(elements.map(element => + CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent))); + }); + } + /** * Function to find and click an app standard button. * * @param button Type of button to press. * @return OK if successful, or ERROR: followed by message. */ - static pressStandard(button: string): string { + static async pressStandard(button: string): Promise { this.log('Action - Click standard button: ' + button); // Find button @@ -120,7 +177,7 @@ export class TestsBehatRuntime { } // Click button - TestsBehatDomUtils.pressElement(foundButton); + await TestsBehatDomUtils.pressElement(foundButton); return 'OK'; } @@ -140,7 +197,7 @@ export class TestsBehatRuntime { return 'ERROR: Could not find backdrop'; } if (backdrops.length > 1) { - return 'ERROR: Found too many backdrops'; + return 'ERROR: Found too many backdrops ('+backdrops.length+')'; } const backdrop = backdrops[0]; backdrop.click(); @@ -274,7 +331,7 @@ export class TestsBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static press(locator: TestBehatElementLocator): string { + static async press(locator: TestBehatElementLocator): Promise { this.log('Action - Press', locator); try { @@ -284,7 +341,7 @@ export class TestsBehatRuntime { return 'ERROR: No element matches locator to press.'; } - TestsBehatDomUtils.pressElement(found); + await TestsBehatDomUtils.pressElement(found); return 'OK'; } catch (error) { @@ -304,7 +361,7 @@ export class TestsBehatRuntime { titles = titles.filter((title) => TestsBehatDomUtils.isElementVisible(title, document.body)); if (titles.length > 1) { - return 'ERROR: Too many possible titles.'; + return 'ERROR: Too many possible titles ('+titles.length+').'; } else if (!titles.length) { return 'ERROR: No title found.'; } else { @@ -323,18 +380,18 @@ export class TestsBehatRuntime { * @param value New value * @return OK or ERROR: followed by message */ - static setField(field: string, value: string): string { + static async setField(field: string, value: string): Promise { this.log('Action - Set field ' + field + ' to: ' + value); - const found: HTMLElement | HTMLInputElement | HTMLTextAreaElement =TestsBehatDomUtils.findElementBasedOnText( - { text: field, selector: 'input, textarea, [contenteditable="true"]' }, + const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText( + { text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' }, ); if (!found) { return 'ERROR: No element matches field to set.'; } - TestsBehatDomUtils.setElementValue(found, value); + await TestsBehatDomUtils.setElementValue(found, value); return 'OK'; } diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 4b9091735..3a097db93 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -19,11 +19,12 @@ import { Observable, Subject } from 'rxjs'; import { sep } from 'path'; import { CORE_SITE_SCHEMAS } from '@services/sites'; -import { CoreSingletonProxy, Network, Platform, Translate } from '@singletons'; +import { CoreSingletonProxy, Platform, Translate } from '@singletons'; import { CoreTextUtilsProvider } from '@services/utils/text'; import { TranslatePipeStub } from './stubs/pipes/translate'; import { CoreExternalContentDirectiveStub } from './stubs/directives/core-external-content'; +import { CoreNetwork } from '@services/network'; abstract class WrapperComponent { @@ -37,7 +38,7 @@ let testBedInitialized = false; const textUtils = new CoreTextUtilsProvider(); const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, Record][] = [ [Platform, mock({ is: () => false, ready: () => Promise.resolve(), resume: new Subject() })], - [Network, { onChange: () => new Observable() }], + [CoreNetwork, { onChange: () => new Observable() }], ]; async function renderAngularComponent(component: Type, config: RenderConfig): Promise> { diff --git a/src/theme/components/discussion.scss b/src/theme/components/discussion.scss index 540188a0e..fcb653a9d 100644 --- a/src/theme/components/discussion.scss +++ b/src/theme/components/discussion.scss @@ -27,148 +27,3 @@ ion-content { font-weight: normal; font-size: 0.9rem; } - -// Message item. -ion-item.addon-message { - --message-background: var(--addon-messages-message-bg); - --message-activated-background: var(--addon-messages-message-activated-bg); - --message-alignment: flex-start; - - border: 0; - border-radius: var(--medium-radius); - padding: 0 8px 0 8px; - margin: 8px; - --background: var(--message-background); - background: var(--message-background); - align-self: var(--message-alignment); - width: 90%; - max-width: var(--list-item-max-width); - --min-height: var(--a11y-min-target-size); - position: relative; - @include core-transition(width); - // This is needed to display bubble tails. - overflow: visible; - - &::part(native) { - --inner-border-width: 0px; - --inner-padding-end: 0px; - padding: 0; - margin: 0; - } - - &:hover { - -webkit-filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3)); - filter: drop-shadow(2px 2px 2px rgba(0,0,0,.3)); - } - - core-format-text > p:only-child { - display: inline; - } - - .addon-message-user { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - margin-bottom: .5rem; - margin-top: 0; - color: var(--ion-text-color); - - core-user-avatar { - display: block; - --core-avatar-size: var(--addon-messages-avatar-size); - margin: 0; - } - - div { - font-weight: 500; - flex-grow: 1; - padding-left: .5rem; - padding-right: .5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - ion-note { - color: var(--addon-messages-message-note-text); - font-size: var(--addon-messages-message-note-font-size); - margin: 0; - padding: 8px 0; - align-self: flex-start; - } - - &[tappable]:active { - --message-background: var(--message-activated-background); - } - - ion-label { - margin: 0; - padding: 8px 0; - } - - .addon-message-text { - display: inline-flex; - * { - color: var(--ion-text-color); - } - } - - .tail { - content: ''; - width: 0; - height: 0; - border: 0.5rem solid transparent; - position: absolute; - touch-action: none; - bottom: 0; - border-bottom-color: var(--message-background); - } - - // Defines when an item-message is the user's. - &.addon-message-mine { - --message-background: var(--addon-messages-message-mine-bg); - --message-activated-background: var(--addon-messages-message-mine-activated-bg); - --message-alignment: flex-end; - - .spinner { - @include float(end); - @include margin(2px, -3px, -2px, 5px); - - svg { - width: 16px; - height: 16px; - } - } - - .tail { - @include position(null, -8px, null, null); - @include margin-horizontal(null, -0.5rem); - } - } - - &.addon-message-not-mine .tail { - @include position(null, null, null, -8px); - @include margin-horizontal(-0.5rem, null); - } - - .addon-messages-delete-button { - min-height: initial; - line-height: initial; - margin-top: 0px; - margin-bottom: 0px; - height: var(--a11y-min-target-size) !important; - align-self: flex-end; - - ion-icon { - font-size: 1.4em; - line-height: initial; - color: var(--danger); - } - } - - &.addon-message-no-user { - margin-top: 0px; - } -} diff --git a/src/theme/theme.dark.scss b/src/theme/theme.dark.scss index ecd188b0c..a526f82ea 100644 --- a/src/theme/theme.dark.scss +++ b/src/theme/theme.dark.scss @@ -146,13 +146,13 @@ --core-collapsible-footer-background: var(--contrast-background); - --addon-messages-message-bg: var(--gray-800); - --addon-messages-message-activated-bg: var(--gray-700); - --addon-messages-message-note-text: var(--subdued-text-color); - --addon-messages-message-mine-bg: var(--gray-700); - --addon-messages-message-mine-activated-bg: var(--gray-600); - --addon-messages-discussion-badge: var(--primary); - --addon-messages-discussion-badge-text: var(--gray-100); + --core-messages-message-bg: var(--gray-800); + --core-messages-message-activated-bg: var(--gray-700); + --core-messages-message-note-text: var(--subdued-text-color); + --core-messages-message-mine-bg: var(--gray-700); + --core-messages-message-mine-activated-bg: var(--gray-600); + --core-messages-discussion-badge: var(--primary); + --core-messages-discussion-badge-text: var(--gray-100); --addon-forum-border-color: var(--gray-500); --addon-forum-highlight-color: var(--gray-200); diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index f13f3aee1..780297398 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -344,15 +344,15 @@ --addon-calendar-today-border-color: var(--primary); --addon-calendar-border-color: var(--stroke); - --addon-messages-message-bg: var(--white); - --addon-messages-message-activated-bg: var(--gray-200); - --addon-messages-message-note-text: var(--gray-500); - --addon-messages-message-note-font-size: 75%; - --addon-messages-message-mine-bg: var(--gray-300); - --addon-messages-message-mine-activated-bg: var(--gray-400); - --addon-messages-avatar-size: 30px; - --addon-messages-discussion-badge: var(--primary); - --addon-messages-discussion-badge-text: var(--white); + --core-messages-message-bg: var(--white); + --core-messages-message-activated-bg: var(--gray-200); + --core-messages-message-note-text: var(--gray-500); + --core-messages-message-note-font-size: 75%; + --core-messages-message-mine-bg: var(--gray-300); + --core-messages-message-mine-activated-bg: var(--gray-400); + --core-messages-avatar-size: 30px; + --core-messages-discussion-badge: var(--primary); + --core-messages-discussion-badge-text: var(--white); --addon-forum-avatar-size: var(--core-avatar-size); --addon-forum-border-color: var(--stroke); diff --git a/upgrade.txt b/upgrade.txt index ead37e419..7576f9804 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -1,9 +1,10 @@ This files describes API changes in the Moodle Mobile app, information provided here is intended especially for developers. -=== 4.0.1 === +=== 4.1.0 === - Zoom levels changed from "normal / low / high" to " none / medium / high". +- --addon-messages-* CSS3 variables have been renamed to --core-messages-* === 4.0.0 ===