commit
7090f8d918
|
@ -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);");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -81,47 +81,16 @@
|
|||
<ion-icon name="fas-arrow-down" aria-hidden="true"></ion-icon>
|
||||
</ion-chip>
|
||||
|
||||
<ion-item class="ion-text-wrap addon-message" (longPress)="copyMessage(message)"
|
||||
[class.addon-message-mine]="message.useridfrom == currentUserId"
|
||||
[class.addon-message-not-mine]="message.useridfrom != currentUserId"
|
||||
[class.addon-message-no-user]="!message.showUserData"
|
||||
[@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
|
||||
<ion-label>
|
||||
<!-- User data. -->
|
||||
<div *ngIf="message.showUserData" class="item-heading addon-message-user">
|
||||
<core-user-avatar slot="start" [user]="members[message.useridfrom]" [linkProfile]="false" aria-hidden="true">
|
||||
</core-user-avatar>
|
||||
<div>{{ members[message.useridfrom].fullname }}</div>
|
||||
</div>
|
||||
<div *ngIf="!message.showUserData" class="sr-only">
|
||||
{{ message.useridfrom == currentUserId
|
||||
? ('addon.messages.you' | translate)
|
||||
: members[message.useridfrom].fullname }}
|
||||
</div>
|
||||
|
||||
<!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. -->
|
||||
<div class="addon-message-text">
|
||||
<core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text" contextLevel="system"
|
||||
[contextInstanceId]="0"></core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
<ion-note *ngIf="!message.pending" slot="end">{{ message.timecreated | coreFormatDate: "strftimetime" }}</ion-note>
|
||||
<ion-note *ngIf="message.pending" slot="end">
|
||||
<ion-icon name="fas-clock" [attr.aria-label]="'core.notsent' | translate" role="status"></ion-icon>
|
||||
</ion-note>
|
||||
<ion-button fill="clear" *ngIf="!message.sending && showDelete" (click)="deleteMessage(message, index)"
|
||||
class="addon-messages-delete-button" [@coreSlideInOut]="'fromRight'"
|
||||
[attr.aria-label]=" 'addon.messages.deletemessage' | translate" slot="end">
|
||||
<ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<div class="tail" *ngIf="message.showTail"></div>
|
||||
</ion-item>
|
||||
<core-message [message]="message" [user]="members[message.useridfrom]" (afterRender)="last && scrollToBottom()"
|
||||
[text]="message.text" (onDeleteMessage)="deleteMessage(message, index)" [showDelete]="showDelete"
|
||||
[time]="message.timecreated">
|
||||
</core-message>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="far-comments"
|
||||
[message]="'addon.messages.nomessagesfound' | translate"></core-empty-box>
|
||||
[message]="'addon.messages.nomessagesfound' | translate">
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
<!-- Scroll bottom. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && newMessages > 0">
|
||||
|
|
|
@ -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<void> {
|
||||
|
||||
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<HTMLElement>('.addon-message-not-mine'));
|
||||
const messages = Array.from(this.hostElement.querySelectorAll<HTMLElement>('core-message:not(.is-mine)'));
|
||||
|
||||
CoreDom.scrollToElement(messages[messages.length - this.newMessages]);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -81,27 +81,10 @@
|
|||
</ion-badge>
|
||||
</div>
|
||||
|
||||
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
|
||||
[class.addon-message-mine]="message.userid == currentUserId"
|
||||
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData"
|
||||
[@coreSlideInOut]="message.userid == currentUserId ? '' : 'fromLeft'">
|
||||
<ion-label>
|
||||
<!-- User data. -->
|
||||
<h2 class="addon-message-user" *ngIf="message.showUserData">
|
||||
<core-user-avatar slot="start" [user]="message" [linkProfile]="false">
|
||||
</core-user-avatar>
|
||||
<div>{{ message.userfullname }}</div>
|
||||
</h2>
|
||||
|
||||
<div class="addon-message-text">
|
||||
<core-format-text [text]="message.message" contextLevel="module" [contextInstanceId]="cmId"
|
||||
[courseId]="courseId" (afterRender)="last && scrollToBottom()">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
<ion-note slot="end">{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
|
||||
<div class="tail" *ngIf="message.showTail"></div>
|
||||
</ion-item>
|
||||
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
|
||||
[time]="message.timestamp * 1000" (afterRender)="last && scrollToBottom()" contextLevel="module" [instanceId]="cmId"
|
||||
[courseId]="courseId">
|
||||
</core-message>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -75,26 +75,9 @@
|
|||
</ion-badge>
|
||||
</div>
|
||||
|
||||
<ion-item *ngIf="!message.special" class="ion-text-wrap addon-message"
|
||||
[class.addon-message-mine]="message.userid == currentUserId"
|
||||
[class.addon-message-not-mine]="message.userid != currentUserId" [class.addon-message-no-user]="!message.showUserData">
|
||||
<ion-label>
|
||||
<!-- User data. -->
|
||||
<h2 class="addon-message-user">
|
||||
<core-user-avatar slot="start" [user]="message" [linkProfile]="false" *ngIf="message.showUserData">
|
||||
</core-user-avatar>
|
||||
<div *ngIf="message.showUserData">{{ message.userfullname }}</div>
|
||||
</h2>
|
||||
|
||||
<div class="addon-message-text">
|
||||
<core-format-text [text]="message.message" contextLevel="module" [contextInstanceId]="cmId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
<ion-note slot="end">{{ message.timestamp * 1000 | coreFormatDate: "strftimetime" }}</ion-note>
|
||||
<div class="tail" *ngIf="message.showTail"></div>
|
||||
</ion-item>
|
||||
<core-message *ngIf="!message.special" [message]="message" [user]="message" [text]="message.message"
|
||||
[time]="message.timestamp * 1000" contextLevel="module" [instanceId]="cmId" [courseId]="courseId">
|
||||
</core-message>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,8 +28,7 @@
|
|||
<core-loading [hideUntil]="!showLoading">
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info *ngIf="!isSearch" [module]="module" [description]="description" [component]="component"
|
||||
[componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings"
|
||||
(completionChanged)="onCompletionChange()">
|
||||
[componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">
|
||||
</core-course-module-info>
|
||||
|
||||
<ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0">
|
||||
|
|
|
@ -72,8 +72,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
loadMoreError = false;
|
||||
loadingMessage: string;
|
||||
promisedEntries: CorePromisedValue<AddonModGlossaryEntriesManager>;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -50,14 +50,12 @@
|
|||
<!-- Subquestion -->
|
||||
<ng-container *ngIf="question.parent !== 0">
|
||||
<ion-radio-group [(ngModel)]="answers[question.name]" [required]="question.required" [name]="question.name">
|
||||
<ion-row *ngIf="question.parent !== 0" class="ion-align-items-center ion-padding-horizontal" [class.even]="isEven">
|
||||
<ion-row class="ion-align-items-center ion-padding-horizontal" [class.even]="isEven">
|
||||
|
||||
<ion-col size="7">
|
||||
<ion-label id="addon-mod_survey-{{question.id}}">
|
||||
<span [core-mark-required]="question.required">
|
||||
<span id="addon-mod_survey-{{question.name}}" [core-mark-required]="question.required">
|
||||
<strong>{{question.num}}.</strong> {{ question.text }}
|
||||
</span>
|
||||
</ion-label>
|
||||
</ion-col>
|
||||
|
||||
<!-- Tablet view: radio buttons -->
|
||||
|
@ -69,8 +67,8 @@
|
|||
</ion-col>
|
||||
<ion-col class="ion-hide-md-up" size="5">
|
||||
<ion-select class="ion-padding" [(ngModel)]="answers[question.name]" [required]="question.required"
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.id" interface="action-sheet" [name]="question.name"
|
||||
[interfaceOptions]="{header: question.text}">
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.name" interface="action-sheet"
|
||||
[name]="question.name" [interfaceOptions]="{header: question.text}">
|
||||
<ion-select-option value="-1" selected disabled>{{ 'core.choose' | translate }}</ion-select-option>
|
||||
<ion-select-option *ngFor="let option of question.optionsArray; let value=index;" [value]="value +1">
|
||||
{{option}}
|
||||
|
@ -85,17 +83,18 @@
|
|||
<ng-container *ngIf="(!question.multiArray || question.multiArray.length == 0) && question.parent === 0">
|
||||
<ion-row class="ion-align-items-center ion-padding" *ngIf="question.type > 0" [class.even]="isEven">
|
||||
<ion-col size="7">
|
||||
<ion-label id="addon-mod_survey-{{question.id}}">
|
||||
<span [core-mark-required]="question.required">
|
||||
<span id="addon-mod_survey-{{question.name}}" [core-mark-required]="question.required">
|
||||
<strong>{{question.num}}.</strong> {{ question.text }}
|
||||
</span>
|
||||
</ion-label>
|
||||
</ion-col>
|
||||
<ion-col size="5">
|
||||
<ion-select class="ion-padding" [(ngModel)]="answers[question.name]" [required]="question.required"
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.id" interface="action-sheet" [name]="question.name"
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.name" interface="action-sheet" [name]="question.name"
|
||||
[interfaceOptions]="{header: question.text}">
|
||||
<ion-select-option *ngFor="let option of question.optionsArray; let value=index;" [value]="value">
|
||||
<ion-select-option [value]="question.required ? '-1' : '0'" selected [disabled]="question.required">
|
||||
{{ 'core.choose' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option *ngFor="let option of question.optionsArray; let value=index;" [value]="value +1">
|
||||
{{option}}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
|
@ -103,13 +102,13 @@
|
|||
</ion-row>
|
||||
|
||||
<ion-item *ngIf="question.type === 0" class="ion-text-wrap" [class.even]="isEven">
|
||||
<ion-label position="floating" id="addon-mod_survey-{{question.id}}">
|
||||
<ion-label position="floating" id="addon-mod_survey-{{question.name}}">
|
||||
<span [core-mark-required]="question.required">
|
||||
<strong>{{question.num}}.</strong> {{ question.text }}
|
||||
</span>
|
||||
</ion-label>
|
||||
<ion-textarea [(ngModel)]="answers[question.name]" [name]="question.name"
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.id" [required]="question.required">
|
||||
[attr.aria-labelledby]="'addon-mod_survey-'+question.name" [required]="question.required">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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."
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }),
|
||||
])),
|
||||
]),
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<div *ngIf="message" class="message-box" (longPress)="copyMessage()">
|
||||
<div class="main">
|
||||
<!-- User data. -->
|
||||
<div class="message-user" *ngIf="message.showUserData">
|
||||
<core-user-avatar slot="start" [user]="user" [courseId]="courseId" [linkProfile]="false" aria-hidden="true">
|
||||
</core-user-avatar>
|
||||
<div>{{ userFullname }}</div>
|
||||
</div>
|
||||
<div *ngIf="!message.showUserData" class="sr-only">
|
||||
{{ isMine
|
||||
? ('addon.messages.you' | translate)
|
||||
: userFullname }}
|
||||
</div>
|
||||
|
||||
<core-format-text class="message-text" [text]="text" (afterRender)="afterRender.emit()" [contextLevel]="contextLevel"
|
||||
[contextInstanceId]="instanceId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</div>
|
||||
|
||||
<div class="extra">
|
||||
<div class="message-time">
|
||||
<ng-container *ngIf="!message.pending && !message.deleted">
|
||||
{{ time | coreFormatDate: 'strftimetime' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!message.pending && message.deleted">
|
||||
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon>
|
||||
<span class="ion-text-wrap">
|
||||
{{ 'core.deletedoffline' | translate }}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ion-icon *ngIf="message.pending" name="fas-clock" [attr.aria-label]="'core.notsent' | translate" role="status"></ion-icon>
|
||||
</div>
|
||||
|
||||
<ion-button *ngIf="showDelete && !message.deleted && message.delete !== false" fill="clear" [@coreSlideInOut]="'fromRight'"
|
||||
color="danger" (click)="delete($event)" [attr.aria-label]="'addon.messages.deletemessage' | translate" class="delete-button">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="showDelete && message.deleted" fill="clear" color="danger" (click)="undoDelete($event)"
|
||||
[attr.aria-label]="'core.restore' | translate" class="delete-button">
|
||||
<ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="tail" *ngIf="message.showTail"></div>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<void>();
|
||||
@Output() onUndoDeleteMessage = new EventEmitter<void>();
|
||||
@Output() afterRender = new EventEmitter<void>();
|
||||
|
||||
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<void> {
|
||||
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.
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -45,71 +45,20 @@
|
|||
{{ comment.timecreated * 1000 | coreFormatDate: "strftimedayshort" }}
|
||||
</p>
|
||||
|
||||
<ion-item class="ion-text-wrap addon-message" [class.addon-message-mine]="comment.userid == currentUserId"
|
||||
[class.addon-message-not-mine]="comment.userid != currentUserId" [class.addon-message-no-user]="!comment.showUserData"
|
||||
[@coreSlideInOut]="comment.userid == currentUserId ? '' : 'fromLeft'">
|
||||
<ion-label>
|
||||
<!-- User data. -->
|
||||
<h2 class="addon-message-user" *ngIf="comment.showUserData">
|
||||
<core-user-avatar slot="start" [user]="comment" [linkProfile]="false">
|
||||
</core-user-avatar>
|
||||
<div>{{ comment.fullname }}</div>
|
||||
</h2>
|
||||
|
||||
<div class="addon-message-text">
|
||||
<core-format-text [text]="comment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
<ion-note slot="end">
|
||||
<ng-container *ngIf="!comment.deleted">
|
||||
{{ comment.timecreated * 1000 | coreFormatDate: 'strftimetime' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="comment.deleted">
|
||||
<ion-icon name="fas-trash" aria-hidden="true"></ion-icon> <span class="ion-text-wrap">
|
||||
{{ 'core.deletedoffline' | translate }}
|
||||
</span>
|
||||
</ng-container>
|
||||
</ion-note>
|
||||
<div class="tail" *ngIf="comment.showTail"></div>
|
||||
<ion-button *ngIf="showDelete && !comment.deleted && comment.delete" slot="end" fill="clear"
|
||||
[@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteComment($event, comment)"
|
||||
[attr.aria-label]="'core.delete' | translate" class="addon-messages-delete-button">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="showDelete && comment.deleted" slot="end" fill="clear" color="danger"
|
||||
(click)="undoDeleteComment($event, comment)" [attr.aria-label]="'core.restore' | translate"
|
||||
class="addon-messages-delete-button">
|
||||
<ion-icon name="fas-undo-alt" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<core-message [message]="comment" [text]="comment.content" [time]="comment.timecreated * 1000" [user]="comment"
|
||||
[showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId" [courseId]="courseId"
|
||||
(onDeleteMessage)="deleteComment(comment)" (onUndoDeleteMessage)="undoDeleteComment(comment)">
|
||||
</core-message>
|
||||
</ng-container>
|
||||
|
||||
<ion-item *ngIf="offlineComment" class="ion-text-wrap addon-message addon-message-mine">
|
||||
<ion-label>
|
||||
<!-- User data. -->
|
||||
<p class="ion-text-center">
|
||||
<ion-badge class="ion-text-wrap" color="info" *ngIf="hasOffline">
|
||||
<ion-icon name="fas-exclamation-triangle" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.thereisdatatosync' | translate:{$a: 'core.comments.comments' | translate | lowercase } }}
|
||||
</p>
|
||||
|
||||
<div class="addon-message-text">
|
||||
<core-format-text [text]="offlineComment.content" [contextLevel]="contextLevel" [contextInstanceId]="instanceId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
<ion-note>
|
||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'core.notsent' | translate }}
|
||||
</ion-note>
|
||||
<div class="tail"></div>
|
||||
<ion-button *ngIf="showDelete" slot="end" fill="clear" [@coreSlideInOut]="'fromRight'" color="danger"
|
||||
(click)="deleteComment($event, offlineComment)" [attr.aria-label]="'core.delete' | translate"
|
||||
class="addon-messages-delete-button">
|
||||
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-badge>
|
||||
<core-message *ngIf="hasOffline && offlineComment" [message]="offlineComment" [text]="offlineComment.content"
|
||||
[user]="offlineComment" [showDelete]="showDelete" [contextLevel]="contextLevel" [instanceId]="instanceId"
|
||||
[courseId]="courseId" (onDeleteMessage)="deleteComment(offlineComment)">
|
||||
</core-message>
|
||||
</ion-list>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -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<void> {
|
||||
async loadPrevious(infiniteComplete?: () => void): Promise<void> {
|
||||
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<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
async deleteComment(comment: CoreCommentsDataToDisplay | CoreCommentsOfflineWithUser): Promise<void> {
|
||||
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<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
async undoDeleteComment(comment: CoreCommentsDataToDisplay): Promise<void> {
|
||||
await CoreCommentsOffline.undoDeleteComment(comment.id);
|
||||
|
||||
comment.deleted = false;
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
@import "~theme/components/discussion.scss";
|
||||
|
||||
ion-badge {
|
||||
margin: 8px auto;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
(<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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<void> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
|
|
|
@ -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
|
||||
(<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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<HTMLElement>(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<DOMRect>((resolve): void => {
|
||||
const promise = new CorePromisedValue<DOMRect>();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
if (initialRect.y !== rect.y) {
|
||||
setTimeout(() => {
|
||||
resolve(rect);
|
||||
promise.resolve(rect);
|
||||
}, 300);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(rect);
|
||||
});
|
||||
promise.resolve(rect);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -467,8 +472,8 @@ export class TestsBehatDomUtils {
|
|||
* @param element Element to press.
|
||||
*/
|
||||
static async pressElement(element: HTMLElement): Promise<void> {
|
||||
NgZone.run(async () => {
|
||||
const blockKey = TestsBehatBlocking.block();
|
||||
await NgZone.run(async () => {
|
||||
const promise = new CorePromisedValue<void>();
|
||||
|
||||
// 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<void> {
|
||||
NgZone.run(async () => {
|
||||
const blockKey = TestsBehatBlocking.block();
|
||||
static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
|
||||
await NgZone.run(async () => {
|
||||
const promise = new CorePromisedValue<void>();
|
||||
|
||||
// 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) => {
|
||||
element.value = text;
|
||||
};
|
||||
getValue = () => element.value;
|
||||
if (optionValue) {
|
||||
element.value = optionValue.value;
|
||||
}
|
||||
} else if ('value' in element) {
|
||||
element.value = text;
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string> {
|
||||
const blockKey = TestsBehatBlocking.block();
|
||||
|
||||
try {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await NgZone.run(async () => {
|
||||
const elements = Array.from(document.body.querySelectorAll<HTMLElement>('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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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';
|
||||
}
|
||||
|
|
|
@ -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<U> {
|
||||
|
||||
|
@ -37,7 +38,7 @@ let testBedInitialized = false;
|
|||
const textUtils = new CoreTextUtilsProvider();
|
||||
const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, Record<string, unknown>][] = [
|
||||
[Platform, mock({ is: () => false, ready: () => Promise.resolve(), resume: new Subject<void>() })],
|
||||
[Network, { onChange: () => new Observable() }],
|
||||
[CoreNetwork, { onChange: () => new Observable() }],
|
||||
];
|
||||
|
||||
async function renderAngularComponent<T>(component: Type<T>, config: RenderConfig): Promise<ComponentFixture<T>> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue