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">
+
0">
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()">
0">
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 @@
0" [class.even]="isEven">
-
-
- {{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 }}
+
+
+
+
+
+
+
+
+
+
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 ===