Merge pull request #3886 from NoelDeMartin/MOBILE-3947

MOBILE-3947: Fix behat tests
main
Pau Ferrer Ocaña 2024-01-16 11:41:39 +01:00 committed by GitHub
commit 97df9fb152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 245 additions and 123 deletions

View File

@ -6,7 +6,7 @@ on:
behat_tags: behat_tags:
description: 'Behat tags to execute' description: 'Behat tags to execute'
required: true required: true
default: '~@performance&&~@ionic7_failure' default: '~@performance'
moodle_branch: moodle_branch:
description: 'Moodle branch' description: 'Moodle branch'
required: true required: true
@ -27,7 +27,7 @@ jobs:
MOODLE_DOCKER_PHP_VERSION: '8.1' MOODLE_DOCKER_PHP_VERSION: '8.1'
MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }} MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }}
BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance&&~@ionic7_failure' }} BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -781,13 +781,10 @@ class behat_app extends behat_app_helper {
/** /**
* Sets a field to the given text value in the app. * Sets a field to the given text value in the app.
* *
* Currently this only works for input fields which must be identified using a partial or
* exact match on the placeholder text.
*
* @Given /^I set the field "((?:[^"]|\\")+)" to "((?:[^"]|\\")*)" in the app$/ * @Given /^I set the field "((?:[^"]|\\")+)" to "((?:[^"]|\\")*)" in the app$/
* @param string $field Text identifying field * @param string $field Text identifying the field.
* @param string $value Value for field * @param string $value Value to set. In select fields, this can be either the value or text included in the select option.
* @throws DriverException If the field set doesn't work * @throws DriverException If the field set doesn't work.
*/ */
public function i_set_the_field_in_the_app(string $field, string $value) { public function i_set_the_field_in_the_app(string $field, string $value) {
$field = addslashes_js($field); $field = addslashes_js($field);

View File

@ -639,8 +639,10 @@ class AddonCalendarEventsSwipeItemsManager extends CoreSwipeNavigationItemsManag
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.id; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.id;
} }
} }

View File

@ -36,7 +36,7 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source';
import { ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source';
import { CoreTime } from '@singletons/time'; import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
@ -350,8 +350,10 @@ class AddonCompetencyCompetenciesSwipeManager
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.competencyId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.competencyId;
} }
} }

View File

@ -280,7 +280,6 @@ Feature: Test basic usage of messages in app
Then I should find "Teacher teacher" in the app Then I should find "Teacher teacher" in the app
And I should find "Student1 student1" in the app And I should find "Student1 student1" in the app
@ionic7_failure
Scenario: User blocking feature Scenario: User blocking feature
Given I entered the course "Course 1" as "student2" in the app Given I entered the course "Course 1" as "student2" in the app
When I press "Participants" in the app When I press "Participants" in the app
@ -318,7 +317,6 @@ Feature: Test basic usage of messages in app
Then I should find "test message" in the app Then I should find "test message" in the app
But I should not find "You are unable to message this user" in the app But I should not find "You are unable to message this user" in the app
@ionic7_failure
Scenario: Mute Unmute conversations Scenario: Mute Unmute conversations
Given I entered the course "Course 1" as "student1" in the app Given I entered the course "Course 1" as "student1" in the app
When I press "Participants" in the app When I press "Participants" in the app

View File

@ -245,8 +245,10 @@ class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeNavigationItems
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.submitId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.submitId;
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
@ -187,8 +187,10 @@ class AddonModFeedbackAttemptsSwipeManager extends CoreSwipeNavigationItemsManag
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.attemptId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.attemptId;
} }
} }

View File

@ -893,8 +893,10 @@ class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscus
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return this.getSource().DISCUSSIONS_PATH_PREFIX + route.params.discussionId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return this.getSource().DISCUSSIONS_PATH_PREFIX + snapshot.params.discussionId;
} }
} }

View File

@ -699,8 +699,10 @@ class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDis
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${route.params.timeCreated}`; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${snapshot.params.timeCreated}`;
} }
} }

View File

@ -367,8 +367,10 @@ class AddonModGlossaryEntryEntriesSwipeManager
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entrySlug}`; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${snapshot.params.entrySlug}`;
} }
} }

View File

@ -211,7 +211,6 @@ Feature: Test glossary navigation
And I should find "Cashew" in the app And I should find "Cashew" in the app
And I should find "Acerola" in the app And I should find "Acerola" in the app
@ci_jenkins_skip @ionic7_failure
Scenario: Tablet navigation on glossary Scenario: Tablet navigation on glossary
Given I entered the course "Course 1" as "student1" in the app Given I entered the course "Course 1" as "student1" in the app
And I change viewport size to "1200x640" in the app And I change viewport size to "1200x640" in the app

View File

@ -20,7 +20,7 @@ import {
AddonNotificationsHelper, AddonNotificationsHelper,
} from '@addons/notifications/services/notifications-helper'; } from '@addons/notifications/services/notifications-helper';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
@ -211,8 +211,10 @@ class AddonNotificationSwipeItemsManager extends CoreSwipeNavigationItemsManager
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.id; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.id;
} }
} }

View File

@ -169,6 +169,20 @@ export function conditionalRoutes(routes: Routes, condition: () => boolean): Rou
}); });
} }
/**
* Check whether a route does not have any content.
*
* @param route Route.
* @returns Whether the route doesn't have any content.
*/
export function isEmptyRoute(route: Route): boolean {
return !('component' in route)
&& !('loadComponent' in route)
&& !('children' in route)
&& !('loadChildren' in route)
&& !('redirectTo' in route);
}
/** /**
* Resolve module routes. * Resolve module routes.
* *

View File

@ -239,13 +239,15 @@ export class CoreListItemsManager<
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const segments: UrlSegment[] = []; const segments: UrlSegment[] = [];
while (route.firstChild) { while (route.firstChild) {
route = route.firstChild; route = route.firstChild;
segments.push(...route.url); const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
segments.push(...snapshot.url);
} }
return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null; return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null;

View File

@ -55,7 +55,7 @@ export abstract class CoreRoutedItemsManager<
* @param route Page route. * @param route Page route.
* @returns Path of the selected item in the given route. * @returns Path of the selected item in the given route.
*/ */
protected abstract getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null; protected abstract getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null;
/** /**
* Get the path of the selected item. * Get the path of the selected item.
@ -63,7 +63,7 @@ export abstract class CoreRoutedItemsManager<
* @param route Page route, if any. * @param route Page route, if any.
* @returns Path of the selected item. * @returns Path of the selected item.
*/ */
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null { protected getSelectedItemPath(route?: ActivatedRouteSnapshot | ActivatedRoute | null): string | null {
if (!route) { if (!route) {
return null; return null;
} }
@ -76,14 +76,12 @@ export abstract class CoreRoutedItemsManager<
* *
* @param route Current route. * @param route Current route.
*/ */
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void { protected updateSelectedItem(route: ActivatedRouteSnapshot | ActivatedRoute | null = null): void {
route = route ?? this.getCurrentPageRoute()?.snapshot ?? null; route = route ?? this.getCurrentPageRoute() ?? null;
const selectedItemPath = this.getSelectedItemPath(route); const selectedItemPath = this.getSelectedItemPath(route);
const selectedItem = selectedItemPath ? (this.itemsMap?.[selectedItemPath] ?? null) : null;
const selectedItem = selectedItemPath
? this.itemsMap?.[selectedItemPath] ?? null
: null;
this.setSelectedItem(selectedItem); this.setSelectedItem(selectedItem);
} }
@ -106,7 +104,7 @@ export abstract class CoreRoutedItemsManager<
// If this item is already selected, do nothing. // If this item is already selected, do nothing.
const itemPath = this.getSource().getItemPath(item); const itemPath = this.getSource().getItemPath(item);
const selectedItemPath = this.getSelectedItemPath(route.snapshot); const selectedItemPath = this.getSelectedItemPath(route);
if (selectedItemPath === itemPath) { if (selectedItemPath === itemPath) {
return; return;
@ -135,7 +133,7 @@ export abstract class CoreRoutedItemsManager<
} }
// If the current page is already the index, do nothing. // If the current page is already the index, do nothing.
const selectedItemPath = this.getSelectedItemPath(route.snapshot); const selectedItemPath = this.getSelectedItemPath(route);
if (selectedItemPath === null) { if (selectedItemPath === null) {
return; return;

View File

@ -81,11 +81,13 @@ export class CoreSwipeNavigationItemsManager<
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const segments: UrlSegment[] = []; const segments: UrlSegment[] = [];
while (route) { while (route) {
segments.push(...route.url); const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
segments.push(...snapshot.url);
if (!route.firstChild) { if (!route.firstChild) {
break; break;

View File

@ -330,8 +330,10 @@ class CoreGradesCourseParticipantsSwipeManager extends CoreSwipeNavigationItemsM
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.userId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.userId;
} }
} }

View File

@ -15,7 +15,7 @@
import { InjectionToken, Injector, ModuleWithProviders, NgModule } from '@angular/core'; import { InjectionToken, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { Route, Routes } from '@angular/router'; import { Route, Routes } from '@angular/router';
import { ModuleRoutesConfig, resolveModuleRoutes } from '@/app/app-routing.module'; import { ModuleRoutesConfig, isEmptyRoute, resolveModuleRoutes } from '@/app/app-routing.module';
const MAIN_MENU_TAB_ROUTES = new InjectionToken('MAIN_MENU_TAB_ROUTES'); const MAIN_MENU_TAB_ROUTES = new InjectionToken('MAIN_MENU_TAB_ROUTES');
const modulesPaths: Record<string, Set<string>> = {}; const modulesPaths: Record<string, Set<string>> = {};
@ -71,6 +71,8 @@ export function buildTabMainRoutes(injector: Injector, mainRoute: Route): Routes
if (isRootRoute && !('redirectTo' in mainRoute)) { if (isRootRoute && !('redirectTo' in mainRoute)) {
mainRoute.children = mainRoute.children || []; mainRoute.children = mainRoute.children || [];
mainRoute.children = mainRoute.children.concat(routes.children); mainRoute.children = mainRoute.children.concat(routes.children);
} else if (isEmptyRoute(mainRoute)) {
return [];
} }
return isRootRoute return isRootRoute

View File

@ -4,4 +4,4 @@
</div> </div>
<core-reminders-set-button *ngIf="showReminderButton" slot="end" [component]="component" [instanceId]="instanceId" [type]="type" <core-reminders-set-button *ngIf="showReminderButton" slot="end" [component]="component" [instanceId]="instanceId" [type]="type"
[label]="label" [timebefore]="timebefore" [time]="time" [title]="title" [url]="url" /> [label]="label" [initialTimebefore]="timebefore" [time]="time" [title]="title" [url]="url" />

View File

@ -1,6 +1,7 @@
<ion-button fill="clear" size="small" (click)="setReminder($event)" <ion-button fill="clear" size="small" (click)="setReminder($event)"
[attr.aria-label]="'core.reminders.setareminderfor' | translate : { title: title, label: labelClean }" [attr.aria-label]="'core.reminders.setareminderfor' | translate : { title: title, label: labelClean }">
[attr.aria-checked]="timebefore !== undefined">
<ion-icon name="fas-bell" slot="icon-only" *ngIf="timebefore !== undefined" aria-hidden="true" /> <ion-icon name="fas-bell" slot="icon-only" *ngIf="timebefore !== undefined" aria-hidden="true" />
<ion-icon name="far-bell-slash" slot="icon-only" *ngIf="timebefore === undefined" aria-hidden="true" /> <ion-icon name="far-bell-slash" slot="icon-only" *ngIf="timebefore === undefined" aria-hidden="true" />
</ion-button> </ion-button>
<span class="sr-only" role="status" *ngIf="reminderMessage">{{ reminderMessage }}</span>

View File

@ -32,18 +32,22 @@ export class CoreRemindersSetButtonComponent implements OnInit {
@Input() instanceId?: number; @Input() instanceId?: number;
@Input() type?: string; @Input() type?: string;
@Input() label = ''; @Input() label = '';
@Input() timebefore?: number; @Input() initialTimebefore?: number;
@Input() time = -1; @Input() time = -1;
@Input() title = ''; @Input() title = '';
@Input() url = ''; @Input() url = '';
labelClean = ''; labelClean = '';
timebefore?: number;
reminderMessage?: string;
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnInit(): void { ngOnInit(): void {
this.labelClean = this.label.replace(':', ''); this.labelClean = this.label.replace(':', '');
this.setTimebefore(this.initialTimebefore);
} }
/** /**
@ -86,6 +90,23 @@ export class CoreRemindersSetButtonComponent implements OnInit {
this.saveReminder(reminderTime.timeBefore); this.saveReminder(reminderTime.timeBefore);
} }
/**
* Update time before.
*/
setTimebefore(timebefore: number | undefined): void {
this.timebefore = timebefore;
if (this.timebefore !== undefined) {
const reminderTime = this.time - this.timebefore;
this.reminderMessage = Translate.instant('core.reminders.reminderset', {
$a: CoreTimeUtils.userDate(reminderTime * 1000),
});
} else {
this.reminderMessage = undefined;
}
}
/** /**
* Save reminder. * Save reminder.
* *
@ -105,18 +126,18 @@ export class CoreRemindersSetButtonComponent implements OnInit {
}); });
if (timebefore === undefined || timebefore === CoreRemindersService.DISABLED) { if (timebefore === undefined || timebefore === CoreRemindersService.DISABLED) {
this.timebefore = undefined; this.setTimebefore(undefined);
CoreDomUtils.showToast('core.reminders.reminderunset', true); CoreDomUtils.showToast('core.reminders.reminderunset', true);
return; return;
} }
this.timebefore = timebefore; this.setTimebefore(timebefore);
const reminder: CoreReminderData = { const reminder: CoreReminderData = {
timebefore,
component: this.component, component: this.component,
instanceId: this.instanceId, instanceId: this.instanceId,
timebefore: this.timebefore,
type: this.type, type: this.type,
title: this.label + ' ' + this.title, title: this.label + ' ' + this.title,
url: this.url, url: this.url,

View File

@ -16,33 +16,30 @@ Feature: Set a new reminder on activity
| assign | C1 | assign01 | Assignment 01 | ## yesterday ## | ## now +70 minutes ## | | assign | C1 | assign01 | Assignment 01 | ## yesterday ## | ## now +70 minutes ## |
| assign | C1 | assign02 | Assignment 02 | ## yesterday ## | ## 1 January 2050 ## | | assign | C1 | assign02 | Assignment 02 | ## yesterday ## | ## 1 January 2050 ## |
@ionic7_failure
Scenario: Add, delete and update reminder on activity Scenario: Add, delete and update reminder on activity
Given I entered the assign activity "Assignment 01" on course "Course 1" as "student1" in the app Given I entered the assign activity "Assignment 01" on course "Course 1" as "student1" in the app
Then I should not find "Set a reminder for \"Assignment 01\" (Opened)" in the app Then I should not find "Set a reminder for \"Assignment 01\" (Opened)" in the app
And I should find "Set a reminder for \"Assignment 01\" (Due)" in the app And I should not find "Reminder set for" in the app
And "Set a reminder for \"Assignment 01\" (Due)" should not be selected in the app But I should find "Set a reminder for \"Assignment 01\" (Due)" in the app
# Default set # Default set
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
# Set from list # Set from list
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
Then I should find "Set a reminder" in the app Then I should find "Set a reminder" in the app
And "At the time of the event" should be selected in the app And "At the time of the event" should be selected in the app
And "1 hour before" should not be selected in the app But "1 hour before" should not be selected in the app
When I press "1 hour before" in the app When I press "1 hour before" in the app
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
# Custom set # Custom set
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
Then I should find "Set a reminder" in the app Then I should find "Set a reminder" in the app
And "At the time of the event" should not be selected in the app
And "1 hour before" should be selected in the app And "1 hour before" should be selected in the app
But "At the time of the event" should not be selected in the app
When I press "Custom..." in the app When I press "Custom..." in the app
Then I should find "Custom reminder" in the app Then I should find "Custom reminder" in the app
When I set the following fields to these values in the app: When I set the following fields to these values in the app:
@ -50,14 +47,13 @@ Feature: Set a new reminder on activity
| Units | minutes | | Units | minutes |
And I press "Set reminder" in the app And I press "Set reminder" in the app
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app
# Remove # Remove
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
Then "4 minutes before" should be selected in the app Then "4 minutes before" should be selected in the app
When I press "Delete reminder" in the app When I press "Delete reminder" in the app
Then I should find "Reminder deleted" in the app Then I should find "Reminder deleted" in the app
And "Set a reminder for \"Assignment 01\" (Due)" should not be selected in the app But I should not find "Reminder set for" in the app
# Set and check reminder # Set and check reminder
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I press "Set a reminder for \"Assignment 01\" (Due)" in the app

View File

@ -12,34 +12,33 @@ Feature: Set a new reminder on course
| user | course | role | | user | course | role |
| student1 | C1 | student | | student1 | C1 | student |
@ionic7_failure
Scenario: Add, delete and update reminder on course Scenario: Add, delete and update reminder on course
Given I entered the course "Course 1" as "student1" in the app Given I entered the course "Course 1" as "student1" in the app
And I press "Course summary" in the app And I press "Course summary" in the app
Then I should not find "Set a reminder for \"Course 1\" (Course start date)" in the app Then I should not find "Set a reminder for \"Course 1\" (Course start date)" in the app
And I should find "Set a reminder for \"Course 1\" (Course end date)" in the app And I should not find "Reminder set for" in the app
And "Set a reminder for \"Course 1\" (Course end date)" should not be selected in the app But I should find "Set a reminder for \"Course 1\" (Course end date)" in the app
# Default set # Default set
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
Then I should find "Reminder set for " in the app Then I should find "Reminder set for " in the app
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app And I should find "Reminder set for" in the app
# Set from list # Set from list
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
Then I should find "Set a reminder" in the app Then I should find "Set a reminder" in the app
And "At the time of the event" should be selected in the app And "At the time of the event" should be selected in the app
And "12 hours before" should not be selected in the app But "12 hours before" should not be selected in the app
When I press "12 hours before" in the app When I press "12 hours before" in the app
Then I should find "Reminder set for " in the app Then I should find "Reminder set for " in the app
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app And I should find "Reminder set for" in the app
# Custom set # Custom set
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
Then I should find "Set a reminder" in the app Then I should find "Set a reminder" in the app
And "At the time of the event" should not be selected in the app And "At the time of the event" should not be selected in the app
And "12 hours before" should be selected in the app But "12 hours before" should be selected in the app
When I press "Custom..." in the app When I press "Custom..." in the app
Then I should find "Custom reminder" in the app Then I should find "Custom reminder" in the app
When I set the following fields to these values in the app: When I set the following fields to these values in the app:
@ -47,11 +46,11 @@ Feature: Set a new reminder on course
| Units | hours | | Units | hours |
And I press "Set reminder" in the app And I press "Set reminder" in the app
Then I should find "Reminder set for " in the app Then I should find "Reminder set for " in the app
And "Set a reminder for \"Course 1\" (Course end date)" should be selected in the app And I should find "Reminder set for" in the app
# Remove # Remove
When I press "Set a reminder for \"Course 1\" (Course end date)" in the app When I press "Set a reminder for \"Course 1\" (Course end date)" in the app
Then "2 hours before" should be selected in the app Then "2 hours before" should be selected in the app
When I press "Delete reminder" in the app When I press "Delete reminder" in the app
Then I should find "Reminder deleted" in the app Then I should find "Reminder deleted" in the app
And "Set a reminder for \"Course 1\" (Course end date)" should not be selected in the app But I should not find "Reminder set for" in the app

View File

@ -253,8 +253,10 @@ class CoreUserSwipeItemsManager extends CoreSwipeNavigationItemsManager {
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
return route.params.userId; const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.userId;
} }
} }

View File

@ -276,7 +276,7 @@ export class TestingBehatDomUtilsService {
/** /**
* Given a list of elements, get the top ancestors among all of them. * Given a list of elements, get the top ancestors among all of them.
* *
* This will remote duplicates and drop any elements nested within each other. * This will remove duplicates and drop any elements nested within each other.
* *
* @param elements Elements list. * @param elements Elements list.
* @returns Top ancestors. * @returns Top ancestors.
@ -480,6 +480,34 @@ export class TestingBehatDomUtilsService {
return this.findElementsBasedOnText(locator, options)[0]; return this.findElementsBasedOnText(locator, options)[0];
} }
/**
* Wait until an element with the given selector is found.
*
* @param selector Element selector.
* @param timeout Timeout after which an error is thrown.
* @param retryFrequency Frequency for retries when the element is not found.
* @returns Element.
*/
async waitForElement<T extends HTMLElement = HTMLElement>(
selector: string,
timeout: number = 2000,
retryFrequency: number = 100,
): Promise<T> {
const element = document.querySelector<T>(selector);
if (!element) {
if (timeout < retryFrequency) {
throw new Error(`Element with '${selector}' selector not found`);
}
await new Promise(resolve => setTimeout(resolve, retryFrequency));
return this.waitForElement<T>(selector, timeout - retryFrequency, retryFrequency);
}
return element;
}
/** /**
* Function to find elements based on their text or Aria label. * Function to find elements based on their text or Aria label.
* *
@ -515,7 +543,7 @@ export class TestingBehatDomUtilsService {
protected findElementsBasedOnTextInContainer( protected findElementsBasedOnTextInContainer(
locator: TestingBehatElementLocator, locator: TestingBehatElementLocator,
topContainer: HTMLElement, topContainer: HTMLElement,
options: TestingBehatFindOptions, options: TestingBehatFindOptions = {},
): HTMLElement[] { ): HTMLElement[] {
let container: HTMLElement | null = topContainer; let container: HTMLElement | null = topContainer;
@ -667,37 +695,26 @@ export class TestingBehatDomUtilsService {
} }
/** /**
* Set an element value. * Set an input element value.
* *
* @param element HTML to set. * @param element Input element.
* @param value Value to be set. * @param value Value.
*/ */
async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> { async setInputValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
await NgZone.run(async () => { await NgZone.run(async () => {
const promise = new CorePromisedValue<void>();
// Functions to get/set value depending on field type. // Functions to get/set value depending on field type.
const setValue = (text: string) => { const setValue = async (text: string) => {
if (! ('value' in element)) {
element.innerHTML = text;
return;
}
if (element.tagName === 'ION-SELECT') { if (element.tagName === 'ION-SELECT') {
value = value.trim(); this.setIonSelectInputValue(element, value);
const optionValue = Array.from(element.querySelectorAll('ion-select-option')) } else if ('value' in element) {
.find((option) => option.innerHTML.trim() === value);
if (optionValue) {
element.value = optionValue.value;
}
} else {
element.value = text; element.value = text;
} else {
element.innerHTML = text;
} }
element.dispatchEvent(new Event('ionChange')); element.dispatchEvent(new Event('ionChange'));
}; };
const getValue = () => { const getValue = () => {
if ('value' in element) { if ('value' in element) {
return element.value; return element.value;
@ -707,38 +724,79 @@ export class TestingBehatDomUtilsService {
}; };
// Pretend we have cut and pasted the new text. // Pretend we have cut and pasted the new text.
let event: InputEvent; if (element.tagName !== 'ION-SELECT' && getValue() !== '') {
if (getValue() !== '') { await CoreUtils.nextTick();
event = new InputEvent('input', { await setValue('');
element.dispatchEvent(new InputEvent('input', {
bubbles: true, bubbles: true,
view: window, view: window,
cancelable: true, cancelable: true,
inputType: 'deleteByCut', inputType: 'deleteByCut',
}); }));
await CoreUtils.nextTick();
setValue('');
element.dispatchEvent(event);
} }
if (value !== '') { if (value !== '') {
event = new InputEvent('input', { await CoreUtils.nextTick();
await setValue(value);
element.dispatchEvent(new InputEvent('input', {
bubbles: true, bubbles: true,
view: window, view: window,
cancelable: true, cancelable: true,
inputType: 'insertFromPaste', inputType: 'insertFromPaste',
data: value, data: value,
}));
}
}); });
await CoreUtils.nextTick();
setValue(value);
element.dispatchEvent(event);
} }
promise.resolve(); /**
* Select an option in an ion-select element.
*
* @param element IonSelect element.
* @param value Value.
*/
protected async setIonSelectInputValue(element: HTMLElement, value: string): Promise<void> {
// Press select.
await TestingBehatDomUtils.pressElement(element);
return promise; // Press option.
}); type IonSelectInterface = 'alert' | 'action-sheet' | 'popover';
const selectInterface = element.getAttribute('interface') as IonSelectInterface ?? 'alert';
const containerSelector = ({
'alert': 'ion-alert.select-alert',
'action-sheet': 'ion-action-sheet.select-action-sheet',
'popover': 'ion-popover.select-popover',
})[selectInterface];
const optionSelector = ({
'alert': 'button',
'action-sheet': 'button',
'popover': 'ion-radio',
})[selectInterface] ?? '';
const optionsContainer = await TestingBehatDomUtils.waitForElement(containerSelector);
const options = this.findElementsBasedOnTextInContainer(
{ text: value, selector: optionSelector },
optionsContainer,
{},
);
if (options.length === 0) {
throw new Error('Couldn\'t find ion-select option.');
}
await TestingBehatDomUtils.pressElement(options[0]);
// Press options submit.
if (selectInterface === 'alert') {
const submitButton = optionsContainer.querySelector<HTMLElement>('.alert-button-group button:last-child');
if (!submitButton) {
throw new Error('Couldn\'t find ion-select submit button.');
}
await TestingBehatDomUtils.pressElement(submitButton);
}
} }
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { TestingBehatDomUtils } from './behat-dom'; import { TestingBehatDomUtils, TestingBehatDomUtilsService } from './behat-dom';
import { TestingBehatBlocking } from './behat-blocking'; import { TestingBehatBlocking } from './behat-blocking';
import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes';
import { ONBOARDING_DONE } from '@features/login/constants'; import { ONBOARDING_DONE } from '@features/login/constants';
@ -63,6 +63,10 @@ export class TestingBehatRuntimeService {
return CoreNavigator.instance; return CoreNavigator.instance;
} }
get domUtils(): TestingBehatDomUtilsService {
return TestingBehatDomUtils.instance;
}
/** /**
* Init behat functions and set options like skipping onboarding. * Init behat functions and set options like skipping onboarding.
* *
@ -468,11 +472,22 @@ export class TestingBehatRuntimeService {
?? options.find(option => option.text === value)?.value ?? options.find(option => option.text === value)?.value
?? options.find(option => option.text.includes(value))?.value ?? options.find(option => option.text.includes(value))?.value
?? value; ?? value;
} else if (input.tagName === 'ION-SELECT') {
const options = Array.from(input.querySelectorAll('ion-select-option'));
value = options.find(option => option.value?.toString() === value)?.textContent?.trim()
?? options.find(option => option.textContent?.trim() === value)?.textContent?.trim()
?? options.find(option => option.textContent?.includes(value))?.textContent?.trim()
?? value;
} }
await TestingBehatDomUtils.setElementValue(input, value); try {
await TestingBehatDomUtils.setInputValue(input, value);
return 'OK'; return 'OK';
} catch (error) {
return `ERROR: ${error.message ?? 'Unknown error'}`;
}
} }
/** /**