commit
cbc6beae40
|
@ -10,7 +10,7 @@ on:
|
|||
moodle_branch:
|
||||
description: 'Moodle branch'
|
||||
required: true
|
||||
default: 'master'
|
||||
default: 'main'
|
||||
moodle_repository:
|
||||
description: 'Moodle repository'
|
||||
required: true
|
||||
|
@ -25,7 +25,7 @@ jobs:
|
|||
MOODLE_DOCKER_DB: pgsql
|
||||
MOODLE_DOCKER_BROWSER: chrome
|
||||
MOODLE_DOCKER_PHP_VERSION: '8.1'
|
||||
MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }}
|
||||
MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
|
||||
MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }}
|
||||
BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance&&~@ionic7_failure' }}
|
||||
|
||||
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Additional checkouts
|
||||
run: |
|
||||
git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle
|
||||
git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||
git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||
- name: Install npm packages
|
||||
run: npm ci --no-audit
|
||||
- name: Create Behat faildumps folder
|
||||
|
|
|
@ -16,8 +16,8 @@ jobs:
|
|||
node-version-file: '.nvmrc'
|
||||
- name: Additional checkouts
|
||||
run: |
|
||||
git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
|
||||
git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||
git clone --branch main --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
|
||||
git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||
- name: Install npm packages
|
||||
run: npm ci --no-audit
|
||||
- name: Generate Behat tests plugin
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
"addon.block_starredcourses.nocourses": "block_starredcourses",
|
||||
"addon.block_starredcourses.pluginname": "block_starredcourses",
|
||||
"addon.block_tags.pluginname": "block_tags",
|
||||
"addon.block_timeline.ariadayfilter": "block_timeline",
|
||||
"addon.block_timeline.duedate": "block_timeline",
|
||||
"addon.block_timeline.next30days": "block_timeline",
|
||||
"addon.block_timeline.next3months": "block_timeline",
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
</ion-row>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center addon-block-timeline-filter">
|
||||
<ion-col size="auto">
|
||||
<core-combobox [formControl]="filter" (onChange)="filterChanged($event)">
|
||||
<core-combobox [formControl]="filter" (onChange)="filterChanged($event)"
|
||||
[label]="'addon.block_timeline.ariadayfilter' | translate">
|
||||
<ion-select-option *ngFor="let option of statusFilterOptions; last as last"
|
||||
[attr.class]="'ion-text-wrap' + last ? ' core-select-option-border-bottom' : ''" [value]="option.value">
|
||||
{{ option.name | translate }}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"ariadayfilter": "Filter timeline by date",
|
||||
"duedate": "Due date",
|
||||
"next30days": "Next 30 days",
|
||||
"next3months": "Next 3 months",
|
||||
|
|
|
@ -46,7 +46,7 @@ Feature: Timeline block.
|
|||
| assign | C3 | assign24 | Assignment 24 | ##+1 year## |
|
||||
| assign | C3 | assign25 | Assignment 25 | ##+1 year## |
|
||||
|
||||
@lms_from4.0 @ionic7_failure
|
||||
@lms_from4.0
|
||||
Scenario: See courses inside block
|
||||
Given I entered the app as "student1"
|
||||
Then I should find "Assignment 00" within "Timeline" "ion-card" in the app
|
||||
|
@ -57,7 +57,7 @@ Feature: Timeline block.
|
|||
But I should not find "Assignment 01" within "Timeline" "ion-card" in the app
|
||||
And I should not find "Course 3" within "Timeline" "ion-card" in the app
|
||||
|
||||
When I press "Next 30 days" in the app
|
||||
When I press "Filter timeline by date" in the app
|
||||
And I press "Overdue" in the app
|
||||
Then I should find "Assignment 01" within "Timeline" "ion-card" in the app
|
||||
And I should find "Course 2" within "Timeline" "ion-card" in the app
|
||||
|
@ -66,7 +66,7 @@ Feature: Timeline block.
|
|||
And I should not find "Course 1" within "Timeline" "ion-card" in the app
|
||||
And I should not find "Course 3" within "Timeline" "ion-card" in the app
|
||||
|
||||
When I press "Overdue" in the app
|
||||
When I press "Filter timeline by date" in the app
|
||||
And I press "All" in the app
|
||||
Then I should find "Assignment 19" within "Timeline" "ion-card" in the app
|
||||
And I should find "Course 3" within "Timeline" "ion-card" in the app
|
||||
|
@ -76,7 +76,7 @@ Feature: Timeline block.
|
|||
Then I should find "Assignment 21" within "Timeline" "ion-card" in the app
|
||||
And I should find "Assignment 25" within "Timeline" "ion-card" in the app
|
||||
|
||||
When I press "All" in the app
|
||||
When I press "Filter timeline by date" in the app
|
||||
And I press "Next 7 days" in the app
|
||||
And I press "Sort by" in the app
|
||||
And I press "Sort by courses" in the app
|
||||
|
|
|
@ -142,7 +142,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||
|
@ -164,7 +164,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngDoCheck(): void {
|
||||
const items = this.manager?.getSource().getItems();
|
||||
|
@ -368,7 +368,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.undeleteEventObserver?.off();
|
||||
|
|
|
@ -65,15 +65,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
protected currentSiteId: string;
|
||||
|
||||
// Observers.
|
||||
protected newEventObserver: CoreEventObserver;
|
||||
protected discardedObserver: CoreEventObserver;
|
||||
protected editEventObserver: CoreEventObserver;
|
||||
protected deleteEventObserver: CoreEventObserver;
|
||||
protected undeleteEventObserver: CoreEventObserver;
|
||||
protected syncObserver: CoreEventObserver;
|
||||
protected manualSyncObserver: CoreEventObserver;
|
||||
protected eventObservers: CoreEventObserver[] = [];
|
||||
protected onlineObserver: Subscription;
|
||||
protected filterChangedObserver: CoreEventObserver;
|
||||
protected managerUnsubscribe?: () => void;
|
||||
protected logView: () => void;
|
||||
|
||||
|
@ -97,7 +90,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
this.currentSiteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
// Listen for events added. When an event is added, reload the data.
|
||||
this.newEventObserver = CoreEvents.on(
|
||||
this.eventObservers.push(CoreEvents.on(
|
||||
AddonCalendarProvider.NEW_EVENT_EVENT,
|
||||
(data) => {
|
||||
if (data && data.eventId) {
|
||||
|
@ -106,16 +99,16 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
}
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
));
|
||||
|
||||
// Listen for new event discarded event. When it does, reload the data.
|
||||
this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
||||
this.eventObservers.push(CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
|
||||
this.manager?.getSource().markAllItemsUnloaded();
|
||||
this.refreshData(true, true);
|
||||
}, this.currentSiteId);
|
||||
}, this.currentSiteId));
|
||||
|
||||
// Listen for events edited. When an event is edited, reload the data.
|
||||
this.editEventObserver = CoreEvents.on(
|
||||
this.eventObservers.push(CoreEvents.on(
|
||||
AddonCalendarProvider.EDIT_EVENT_EVENT,
|
||||
(data) => {
|
||||
if (data && data.eventId) {
|
||||
|
@ -124,25 +117,25 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
}
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
));
|
||||
|
||||
// Refresh data if calendar events are synchronized automatically.
|
||||
this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
||||
this.eventObservers.push(CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => {
|
||||
this.manager?.getSource().markAllItemsUnloaded();
|
||||
this.refreshData(false, true);
|
||||
}, this.currentSiteId);
|
||||
}, this.currentSiteId));
|
||||
|
||||
// Refresh data if calendar events are synchronized manually but not by this page.
|
||||
this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
|
||||
this.eventObservers.push(CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
|
||||
const selectedDay = this.manager?.getSelectedItem();
|
||||
if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) {
|
||||
this.manager?.getSource().markAllItemsUnloaded();
|
||||
this.refreshData(false, true);
|
||||
}
|
||||
}, this.currentSiteId);
|
||||
}, this.currentSiteId));
|
||||
|
||||
// Update the events when an event is deleted.
|
||||
this.deleteEventObserver = CoreEvents.on(
|
||||
this.eventObservers.push(CoreEvents.on(
|
||||
AddonCalendarProvider.DELETED_EVENT_EVENT,
|
||||
(data) => {
|
||||
if (data && !data.sent) {
|
||||
|
@ -154,10 +147,10 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
}
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
));
|
||||
|
||||
// Listen for events "undeleted" (offline).
|
||||
this.undeleteEventObserver = CoreEvents.on(
|
||||
this.eventObservers.push(CoreEvents.on(
|
||||
AddonCalendarProvider.UNDELETED_EVENT_EVENT,
|
||||
(data) => {
|
||||
if (!data || !data.eventId) {
|
||||
|
@ -168,9 +161,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
this.manager?.getSource().markAsDeleted(data.eventId, false);
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
));
|
||||
|
||||
this.filterChangedObserver = CoreEvents.on(
|
||||
this.eventObservers.push(CoreEvents.on(
|
||||
AddonCalendarProvider.FILTER_CHANGED_EVENT,
|
||||
async (data) => {
|
||||
this.filter = data;
|
||||
|
@ -180,7 +173,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
|
||||
this.manager?.getSource().filterAllDayEvents(this.filter);
|
||||
},
|
||||
);
|
||||
));
|
||||
|
||||
// Refresh online status when changes.
|
||||
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
|
||||
|
@ -214,7 +207,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const types: string[] = [];
|
||||
|
@ -470,18 +463,11 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.newEventObserver?.off();
|
||||
this.discardedObserver?.off();
|
||||
this.editEventObserver?.off();
|
||||
this.deleteEventObserver?.off();
|
||||
this.undeleteEventObserver?.off();
|
||||
this.syncObserver?.off();
|
||||
this.manualSyncObserver?.off();
|
||||
this.eventObservers.forEach((observer) => observer.off());
|
||||
this.onlineObserver?.unsubscribe();
|
||||
this.filterChangedObserver?.off();
|
||||
this.manager?.getSource().forgetRelatedSources();
|
||||
this.manager?.destroy();
|
||||
this.managerUnsubscribe?.();
|
||||
|
|
|
@ -20,13 +20,14 @@ Feature: Test creation of calendar events in app
|
|||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
|
||||
@ionic7_failure
|
||||
# This test is flaky due to timestamp.
|
||||
Scenario: Create user event as student from monthly view
|
||||
Given I entered the app as "student1"
|
||||
When I press "More" in the app
|
||||
And I press "Calendar" in the app
|
||||
And I press "New event" in the app
|
||||
Then the field "Date" matches value "## now ##%d/%m/%y, %H:%M##" in the app
|
||||
# Flaky step, sometimes it fails due to minute change when checking.
|
||||
Then the field "Date" matches value "## now ##%Y-%m-%dT%H:%M##" in the app
|
||||
And I should not be able to press "Save" in the app
|
||||
|
||||
# Check that student can only create User events.
|
||||
|
@ -36,7 +37,7 @@ Feature: Test creation of calendar events in app
|
|||
|
||||
# Create the event.
|
||||
When I set the field "Event title" to "User Event 01" in the app
|
||||
And I set the field "Date" to "2025-04-11T09:00+08:00" in the app
|
||||
And I set the field "Date" to "2025-04-11T09:00" in the app
|
||||
And I press "Without duration" in the app
|
||||
And I set the field "Description" to "This is User Event 01 description." in the app
|
||||
And I set the field "Location" to "Barcelona" in the app
|
||||
|
|
|
@ -297,7 +297,6 @@ Feature: Test competency navigation
|
|||
Then I should find "Desserts are important" in the app
|
||||
But I should not find "Cakes" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Tablet navigation (student)
|
||||
Given I entered the course "Course 1" as "student1" in the app
|
||||
And I change viewport size to "1200x640" in the app
|
||||
|
@ -383,7 +382,6 @@ Feature: Test competency navigation
|
|||
Then I should find "Desserts are important" in the app
|
||||
But I should not find "Cakes" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Tablet navigation (teacher)
|
||||
Given I entered the course "Course 1" as "teacher1" in the app
|
||||
And I change viewport size to "1200x640" in the app
|
||||
|
@ -391,7 +389,7 @@ Feature: Test competency navigation
|
|||
# Participant competencies
|
||||
When I press "Participants" in the app
|
||||
And I press "Student first" in the app
|
||||
And I press "Competencies" in the app
|
||||
And I press "Competencies" within "Student first" "page-core-user-participants" in the app
|
||||
Then I should find "Student first" in the app
|
||||
And I should find "Salads are important" in the app
|
||||
And I should find "Good" within "salads" "ion-item" in the app
|
||||
|
|
|
@ -19,7 +19,6 @@ Feature: Test basic usage of messages in app
|
|||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: View recent conversations and contacts
|
||||
Given I entered the app as "teacher1"
|
||||
When I press "Messages" in the app
|
||||
|
@ -217,7 +216,6 @@ Feature: Test basic usage of messages in app
|
|||
Then I should find "heeey student" in the app
|
||||
And I should find "byee" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Search for messages
|
||||
Given I entered the app as "teacher1"
|
||||
When I press "Messages" in the app
|
||||
|
@ -348,7 +346,6 @@ Feature: Test basic usage of messages in app
|
|||
Then I should find "test message" in the app
|
||||
And I should find "Muted conversation" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Self conversations
|
||||
Given I entered the app as "student1"
|
||||
When I press "Messages" in the app
|
||||
|
|
|
@ -146,11 +146,11 @@ 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 "something" in the app
|
||||
And I set the field "Search query" 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
|
||||
When I set the field "Search query" 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
|
||||
|
@ -266,11 +266,11 @@ Feature: Test glossary navigation
|
|||
|
||||
# Search
|
||||
When I press "Search" in the app
|
||||
And I set the field "Search" to "something" in the app
|
||||
And I set the field "Search query" 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
|
||||
When I set the field "Search query" 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
|
||||
|
|
|
@ -133,7 +133,6 @@ Feature: Attempt a quiz in app
|
|||
And I should find "Question 1" in the app
|
||||
And I should find "Question 2" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Attempt a quiz (all question types)
|
||||
Given I entered the quiz activity "Quiz 2" on course "Course 1" as "student1" in the app
|
||||
When I press "Attempt quiz now" in the app
|
||||
|
@ -156,12 +155,9 @@ Feature: Attempt a quiz in app
|
|||
And I press "Next" in the app
|
||||
And I press "True" in the app
|
||||
And I press "Next" in the app
|
||||
And I press "Choose... , frog" in the app
|
||||
And I press "amphibian" in the app
|
||||
And I press "Choose... , newt" in the app
|
||||
And I press "insect" in the app
|
||||
And I press "Choose... , cat" in the app
|
||||
And I press "mammal" in the app
|
||||
And I set the field "frog" to "amphibian" in the app
|
||||
And I set the field "newt" to "insect" in the app
|
||||
And I set the field "cat" to "mammal" in the app
|
||||
And I press "Next" in the app
|
||||
Then I should find "Text of the eighth question" in the app
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ Feature: Test basic usage of survey activity in app
|
|||
| activity | name | intro | course | idnumber | groupmode |
|
||||
| survey | Test survey name | Test survey | C1 | survey | 0 |
|
||||
|
||||
@ionic7_failure
|
||||
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:
|
||||
|
@ -79,7 +78,6 @@ Feature: Test basic usage of survey activity in app
|
|||
And I should see "4th answer"
|
||||
And I should see "5th answer"
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Answer a survey & View results (Colles actual)
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | template |course | idnumber | groupmode |
|
||||
|
@ -122,7 +120,6 @@ Feature: Test basic usage of survey activity in app
|
|||
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"
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Answer a survey & View results (Colles preferred)
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | template | course | idnumber | groupmode |
|
||||
|
@ -165,7 +162,6 @@ Feature: Test basic usage of survey activity in app
|
|||
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"
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Answer a survey & View results (Colles preferred and actual)
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | template | course | idnumber | groupmode |
|
||||
|
|
|
@ -19,11 +19,8 @@
|
|||
<core-loading [hideUntil]="notifications.loaded">
|
||||
|
||||
<ion-item *ngFor="let notification of notifications.items" class="ion-text-wrap"
|
||||
[attr.aria-current]="notifications.getItemAriaCurrent(notification)" [attr.aria-label]="
|
||||
notification.timeread
|
||||
? notification.subject
|
||||
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}"
|
||||
(click)="notifications.select(notification)" button [detail]="false" lines="full">
|
||||
[attr.aria-current]="notifications.getItemAriaCurrent(notification)" (click)="notifications.select(notification)" button
|
||||
[detail]="false" lines="full">
|
||||
|
||||
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
|
||||
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
|
||||
|
@ -43,7 +40,10 @@
|
|||
</ng-container>
|
||||
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<p class="item-heading" [attr.aria-label]="
|
||||
notification.timeread
|
||||
? notification.subject
|
||||
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}">
|
||||
<core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0"
|
||||
[wsNotFiltered]="true" />
|
||||
</p>
|
||||
|
|
|
@ -78,7 +78,6 @@ Feature: Notifications
|
|||
Then I should find "Test 10 description" in the app
|
||||
But I should not find "Test 09 description" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Tablet navigation
|
||||
Given I entered the app as "student1"
|
||||
And I change viewport size to "1200x640" in the app
|
||||
|
|
|
@ -22,7 +22,12 @@
|
|||
<ion-datetime-button datetime="datetime" />
|
||||
<ion-modal [keepContentsMounted]="true">
|
||||
<ng-template>
|
||||
<ion-datetime id="datetime" [formControlName]="modelName" [presentation]="ionDateTimePresentation" [max]="max" [min]="min" />
|
||||
<ion-datetime id="datetime" [formControlName]="modelName" [presentation]="ionDateTimePresentation" [max]="max" [min]="min">
|
||||
<span slot="title">
|
||||
<core-format-text [text]="field.name" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
|
||||
[courseId]="courseId" [wsNotFiltered]="true" />
|
||||
</span>
|
||||
</ion-datetime>
|
||||
</ng-template>
|
||||
</ion-modal>
|
||||
<core-input-errors [control]="form.controls[modelName]" />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
</ion-button>
|
||||
<ion-select *ngIf="interface !== 'modal'" class="ion-text-start" [(ngModel)]="selection" (ngModelChange)="onValueChanged(selection)"
|
||||
[interface]="interface" [attr.aria-label]="label + ': ' + selection" [disabled]="disabled" [hidden]="!!icon">
|
||||
[interface]="interface" [attr.aria-label]="label" [disabled]="disabled" [hidden]="!!icon">
|
||||
<ng-content></ng-content>
|
||||
</ion-select>
|
||||
|
||||
|
|
|
@ -7,17 +7,22 @@
|
|||
[href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden"
|
||||
[detail]="!!(item.href && !item.iconAction)" role="menuitem" [button]="!!(item.href && !item.iconAction)"
|
||||
[showBrowserWarning]="item.showBrowserWarning">
|
||||
<ion-label>
|
||||
<ion-toggle *ngIf="item.iconAction === 'toggle'" [(ngModel)]="item.toggle" (ionChange)="item.toggleChanged($event)">
|
||||
<p class="item-heading">
|
||||
<core-format-text [clean]="true" [text]="item.content" [filter]="false" />
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="(item.href || item.action) && item.iconAction">
|
||||
<ion-icon *ngIf="item.iconAction !== 'spinner' && item.iconAction !== 'toggle'" [name]="item.iconAction"
|
||||
[class.icon-slash]="item.iconSlash" slot="end" aria-hidden="true" />
|
||||
<ion-spinner *ngIf="item.iconAction === 'spinner'" slot="end" [attr.aria-label]="'core.loading' | translate" />
|
||||
<ion-toggle *ngIf="item.iconAction === 'toggle'" [(ngModel)]="item.toggle" (ionChange)="item.toggleChanged($event)"
|
||||
slot="end" />
|
||||
</ion-toggle>
|
||||
<ng-container *ngIf="item.iconAction !== 'toggle'">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [clean]="true" [text]="item.content" [filter]="false" />
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="(item.href || item.action) && item.iconAction">
|
||||
<ion-icon *ngIf="item.iconAction !== 'spinner'" [name]="item.iconAction" [class.icon-slash]="item.iconSlash" slot="end"
|
||||
aria-hidden="true" />
|
||||
<ion-spinner *ngIf="item.iconAction === 'spinner'" slot="end" [attr.aria-label]="'core.loading' | translate" />
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ion-badge class="{{item.badgeClass}}" slot="end" *ngIf="item.badge">
|
||||
<span [attr.ara-hidden]="!!item.badgeA11yText">{{item.badge}}</span>
|
||||
|
|
|
@ -64,17 +64,19 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges {
|
|||
* Initialize some common errors if they aren't set.
|
||||
*/
|
||||
protected initErrorMessages(): void {
|
||||
// Set default error messages.
|
||||
this.errorMessages = {
|
||||
required: this.errorMessages.required || 'core.required',
|
||||
email: this.errorMessages.email || 'core.login.invalidemail',
|
||||
date: this.errorMessages.date || 'core.login.invaliddate',
|
||||
datetime: this.errorMessages.datetime || 'core.login.invaliddate',
|
||||
datetimelocal: this.errorMessages.datetimelocal || 'core.login.invaliddate',
|
||||
time: this.errorMessages.time || 'core.login.invalidtime',
|
||||
url: this.errorMessages.url || 'core.login.invalidurl',
|
||||
required: 'core.required',
|
||||
email: 'core.login.invalidemail',
|
||||
date: 'core.login.invaliddate',
|
||||
datetime: 'core.login.invaliddate',
|
||||
datetimelocal: 'core.login.invaliddate',
|
||||
time: 'core.login.invalidtime',
|
||||
url: 'core.login.invalidurl',
|
||||
// Set empty values by default, the default error messages will be built in the template when needed.
|
||||
max: this.errorMessages.max || '',
|
||||
min: this.errorMessages.min || '',
|
||||
max: '',
|
||||
min: '',
|
||||
...this.errorMessages,
|
||||
};
|
||||
|
||||
this.errorMessages.requiredTrue = this.errorMessages.required;
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
import {
|
||||
Component, ContentChild, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChange, TemplateRef, ViewChild,
|
||||
} from '@angular/core';
|
||||
import { AsyncDirective } from '@classes/async-directive';
|
||||
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { CoreDomUtils, VerticalPoint } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
@ -32,7 +34,7 @@ import { SwiperOptions } from 'swiper/types';
|
|||
templateUrl: 'swipe-slides.html',
|
||||
styleUrls: ['swipe-slides.scss'],
|
||||
})
|
||||
export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDestroy {
|
||||
export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDestroy, AsyncDirective {
|
||||
|
||||
@Input() manager?: CoreSwipeSlidesItemsManager<Item>;
|
||||
@Input() options: CoreSwipeSlidesOptions = {};
|
||||
|
@ -46,22 +48,21 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
* This setTimeout waits for Ionic's async initialization to complete.
|
||||
* Otherwise, an outdated swiper reference will be used.
|
||||
*/
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
if (swiperRef?.nativeElement?.swiper) {
|
||||
this.swiper = swiperRef.nativeElement.swiper as Swiper;
|
||||
|
||||
await this.initialize();
|
||||
|
||||
if (this.options.initialSlide) {
|
||||
this.swiper.slideTo(this.options.initialSlide, 0, this.options.runCallbacksOnInit);
|
||||
}
|
||||
|
||||
this.updateOptions();
|
||||
|
||||
this.swiper.on('slideChangeTransitionStart', () => this.slideWillChange());
|
||||
this.swiper.on('slideChangeTransitionEnd', () => this.slideDidChange());
|
||||
|
||||
Object.keys(this.options).forEach((key) => {
|
||||
if (this.swiper) {
|
||||
this.swiper.params[key] = this.options[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
@ -72,6 +73,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
protected unsubscribe?: () => void;
|
||||
protected resizeListener: CoreEventObserver;
|
||||
protected activeSlideIndexes: number[] = [];
|
||||
protected onReadyPromise = new CorePromisedValue<void>();
|
||||
|
||||
constructor(
|
||||
elementRef: ElementRef<HTMLElement>,
|
||||
|
@ -87,17 +89,11 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (!this.unsubscribe && this.manager) {
|
||||
this.initialize(this.manager);
|
||||
}
|
||||
async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
|
||||
await this.initialize();
|
||||
|
||||
if (changes.options) {
|
||||
Object.keys(this.options).forEach((key) => {
|
||||
if (this.swiper) {
|
||||
this.swiper.params[key] = this.options[key];
|
||||
}
|
||||
});
|
||||
this.updateOptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,8 +118,12 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
/**
|
||||
* Initialize some properties based on the manager.
|
||||
*/
|
||||
protected async initialize(manager: CoreSwipeSlidesItemsManager<Item>): Promise<void> {
|
||||
this.unsubscribe = manager.getSource().addListener({
|
||||
protected async initialize(): Promise<void> {
|
||||
if (this.unsubscribe || !this.swiper || !this.manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unsubscribe = this.manager.getSource().addListener({
|
||||
onItemsUpdated: () => this.onItemsUpdated(),
|
||||
});
|
||||
|
||||
|
@ -131,16 +131,16 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
// This is because default callbacks aren't triggered for index 0, and to prevent auto scroll on init.
|
||||
this.options.runCallbacksOnInit = false;
|
||||
|
||||
await manager.getSource().waitForLoaded();
|
||||
await this.manager.getSource().waitForLoaded();
|
||||
|
||||
if (this.options.initialSlide === undefined) {
|
||||
// Calculate the initial slide.
|
||||
const index = manager.getSource().getInitialItemIndex();
|
||||
const index = this.manager.getSource().getInitialItemIndex();
|
||||
this.options.initialSlide = Math.max(index, 0);
|
||||
}
|
||||
|
||||
// Emit change events with the initial item.
|
||||
const items = manager.getSource().getItems();
|
||||
const items = this.manager.getSource().getItems();
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -155,9 +155,11 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
|
||||
this.activeSlideIndexes = [initialIndex];
|
||||
|
||||
manager.setSelectedItem(items[initialIndex]);
|
||||
this.manager.setSelectedItem(items[initialIndex]);
|
||||
this.onWillChange.emit(initialItemData);
|
||||
this.onDidChange.emit(initialItemData);
|
||||
|
||||
this.onReadyPromise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,19 +169,20 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
* @param speed Animation speed.
|
||||
* @param runCallbacks Whether to run callbacks.
|
||||
*/
|
||||
slideToIndex(index: number, speed?: number, runCallbacks?: boolean): void {
|
||||
async slideToIndex(index: number, speed?: number, runCallbacks?: boolean): Promise<void> {
|
||||
// If slides are being updated, wait for the update to finish.
|
||||
if (!this.swiper) {
|
||||
return;
|
||||
}
|
||||
await this.ready();
|
||||
|
||||
// Verify that the number of slides matches the number of items.
|
||||
const slidesLength = this.swiper.slides.length;
|
||||
const slidesLength = this.swiper?.slides?.length || 0;
|
||||
if (slidesLength !== this.items.length) {
|
||||
// Number doesn't match, do a new update to try to match them.
|
||||
this.updateSlidesComponent();
|
||||
await this.updateSlidesComponent();
|
||||
}
|
||||
|
||||
if (!this.swiper?.slides) {
|
||||
return;
|
||||
}
|
||||
this.swiper?.slideTo(index, speed, runCallbacks);
|
||||
}
|
||||
|
||||
|
@ -190,10 +193,10 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
* @param speed Animation speed.
|
||||
* @param runCallbacks Whether to run callbacks.
|
||||
*/
|
||||
slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void {
|
||||
async slideToItem(item: Item, speed?: number, runCallbacks?: boolean): Promise<void> {
|
||||
const index = this.manager?.getSource().getItemIndex(item) ?? -1;
|
||||
if (index != -1) {
|
||||
this.slideToIndex(index, speed, runCallbacks);
|
||||
await this.slideToIndex(index, speed, runCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,7 +228,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
await CoreUtils.nextTick();
|
||||
|
||||
// Update the slides component so the slides list reflects the new items.
|
||||
this.updateSlidesComponent();
|
||||
await this.updateSlidesComponent();
|
||||
|
||||
const currentItem = this.manager?.getSelectedItem();
|
||||
|
||||
|
@ -234,10 +237,26 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
}
|
||||
|
||||
// Keep the same slide in case the list has changed.
|
||||
const newIndex = this.manager.getSource().getItemIndex(currentItem) ?? -1;
|
||||
if (newIndex != -1) {
|
||||
this.swiper?.slideTo(newIndex, 0, false);
|
||||
this.slideToItem(currentItem, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Swiper params from options.
|
||||
*/
|
||||
protected updateOptions(): void {
|
||||
if (!this.swiper) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.swiper.params === undefined) {
|
||||
this.swiper.params = {};
|
||||
}
|
||||
|
||||
Object.keys(this.options).forEach((key) => {
|
||||
if (this.swiper) {
|
||||
this.swiper.params[key] = this.options[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -322,8 +341,24 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
|||
/**
|
||||
* Update slides component.
|
||||
*/
|
||||
updateSlidesComponent(): void {
|
||||
async updateSlidesComponent(): Promise<void> {
|
||||
await this.ready();
|
||||
|
||||
if (!this.swiper) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.swiper?.update();
|
||||
|
||||
// We need to ensure the slides are updated before continuing.
|
||||
await CoreUtils.nextTicks(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ready(): Promise<void> {
|
||||
return this.onReadyPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,6 @@ Feature: Test signup in app
|
|||
And I should find "These are the authentication instructions." in the app
|
||||
But I should not find "Create new account" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Basic signup
|
||||
When I launch the app
|
||||
|
||||
|
@ -80,7 +79,7 @@ Feature: Test signup in app
|
|||
Then I should find "Spain" in the app
|
||||
And I should find "u1@u1.com" in the app
|
||||
|
||||
@ionic7_failure @lms_from3.10
|
||||
@lms_from3.10
|
||||
Scenario: Check password policy in signup
|
||||
Given the following config values are set as admin:
|
||||
| passwordpolicy | 1 |
|
||||
|
@ -115,7 +114,6 @@ Feature: Test signup in app
|
|||
And I press "Create my new account" in the app
|
||||
Then I should find "An email should have been sent to your address" in the app
|
||||
|
||||
@ionic7_failure
|
||||
Scenario: Signup with custom profile fields
|
||||
# Use default options Yes/No for menu field because it's not possible to add new lines. See MDL-75788.
|
||||
Given the following "custom profile fields" exist:
|
||||
|
@ -163,7 +161,7 @@ Feature: Test signup in app
|
|||
And I set the field "Favourite food" to "Sushi" in the app
|
||||
And I press "Are you vegetarian?" in the app
|
||||
And I set the field "Birthday" to "1990-01-01" in the app
|
||||
And I set the field "Date and time" to "2010-01-01 11:45" in the app
|
||||
And I set the field "Date and time" to "2010-01-01T11:45" in the app
|
||||
And I set the field "Describe yourself" to "This is my description." in the app
|
||||
And I press "Create my new account" in the app
|
||||
Then I should find "An email should have been sent to your address" in the app
|
||||
|
|
|
@ -32,7 +32,7 @@ Feature: Main Menu opens the right page
|
|||
And "Dashboard" "text" should appear before "Site home" "text" in the ".core-tabs-bar" "css_element"
|
||||
And "Home" "text" should appear before "My courses" "text" in the ".mainmenu-tabs" "css_element"
|
||||
|
||||
@ionic7_failure @lms_from4.0
|
||||
@lms_from4.0
|
||||
Scenario: Opens My Courses when defaulthomepage is set to My Courses
|
||||
Given the following config values are set as admin:
|
||||
| defaulthomepage | 3 |
|
||||
|
|
|
@ -76,11 +76,10 @@ export class TestingBehatDomUtilsService {
|
|||
* Check if an element is selected.
|
||||
*
|
||||
* @param element Element.
|
||||
* @param container Container.
|
||||
* @param firstCall Whether this is the first call of the function.
|
||||
* @returns Whether the element is selected or not.
|
||||
*/
|
||||
isElementSelected(element: HTMLElement, container: HTMLElement, firstCall = true): boolean {
|
||||
isElementSelected(element: HTMLElement, firstCall = true): boolean {
|
||||
const ariaCurrent = element.getAttribute('aria-current');
|
||||
const ariaSelected = element.getAttribute('aria-selected');
|
||||
const ariaChecked = element.getAttribute('aria-checked');
|
||||
|
@ -96,14 +95,19 @@ export class TestingBehatDomUtilsService {
|
|||
if (inputElement) {
|
||||
return inputElement.value === 'on';
|
||||
}
|
||||
|
||||
const tabButtonElement = element.closest('ion-tab-button');
|
||||
if (tabButtonElement?.classList.contains('tab-selected')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const parentElement = this.getParentElement(element);
|
||||
if (!parentElement || parentElement === container) {
|
||||
if (!parentElement || parentElement.classList.contains('ion-page')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isElementSelected(parentElement, container, false);
|
||||
return this.isElementSelected(parentElement, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -286,7 +290,18 @@ export class TestingBehatDomUtilsService {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (element.contains(otherElement)) {
|
||||
let documentPosition = element.compareDocumentPosition(otherElement);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (documentPosition & Node.DOCUMENT_POSITION_DISCONNECTED) {
|
||||
// Check if they are inside shadow DOM so we can compare their hosts.
|
||||
const elementHost = this.getShadowDOMHost(element) || element;
|
||||
const otherElementHost = this.getShadowDOMHost(otherElement) || otherElement;
|
||||
|
||||
documentPosition = elementHost.compareDocumentPosition(otherElementHost);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
|
||||
uniqueElements.delete(otherElement);
|
||||
}
|
||||
}
|
||||
|
@ -302,9 +317,22 @@ export class TestingBehatDomUtilsService {
|
|||
* @returns Parent element.
|
||||
*/
|
||||
protected getParentElement(element: HTMLElement): HTMLElement | null {
|
||||
return element.parentElement ||
|
||||
(element.getRootNode() && (element.getRootNode() as ShadowRoot).host as HTMLElement) ||
|
||||
null;
|
||||
return element.parentElement || this.getShadowDOMHost(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shadow DOM host element.
|
||||
*
|
||||
* @param element Element.
|
||||
* @returns Shadow DOM host element.
|
||||
*/
|
||||
protected getShadowDOMHost(element: HTMLElement): HTMLElement | null {
|
||||
const node = element.getRootNode();
|
||||
if (node instanceof ShadowRoot) {
|
||||
return node.host as HTMLElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -400,11 +428,18 @@ export class TestingBehatDomUtilsService {
|
|||
* @returns Field element.
|
||||
*/
|
||||
findField(field: string): HTMLElement | HTMLInputElement | undefined {
|
||||
const input = this.findElementBasedOnText(
|
||||
{ text: field, selector: 'input, textarea, [contenteditable="true"], ion-select, ion-datetime' },
|
||||
const selector =
|
||||
'input, textarea, core-rich-text-editor, [contenteditable="true"], ion-select, ion-datetime-button, ion-datetime';
|
||||
|
||||
let input = this.findElementBasedOnText(
|
||||
{ text: field, selector },
|
||||
{ onlyClickable: false, containerName: '' },
|
||||
);
|
||||
|
||||
if (input?.tagName === 'CORE-RICH-TEXT-EDITOR') {
|
||||
input = input.querySelector<HTMLElement>('[contenteditable="true"]') || undefined;
|
||||
}
|
||||
|
||||
if (input) {
|
||||
return input;
|
||||
}
|
||||
|
@ -417,7 +452,17 @@ export class TestingBehatDomUtilsService {
|
|||
if (label) {
|
||||
const inputId = label.getAttribute('for');
|
||||
|
||||
return (inputId && document.getElementById(inputId)) || undefined;
|
||||
if (inputId) {
|
||||
return document.getElementById(inputId) || undefined;
|
||||
}
|
||||
|
||||
input = this.getShadowDOMHost(label) || undefined;
|
||||
|
||||
// Add support for other input types if required by adding them to the array.
|
||||
const ionicInputFields = ['ION-INPUT', 'ION-TEXTAREA', 'ION-SELECT', 'ION-DATETIME', 'ION-TOGGLE'];
|
||||
if (input && ionicInputFields.includes(input.tagName)) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -588,8 +633,10 @@ export class TestingBehatDomUtilsService {
|
|||
// may not work without doing this.
|
||||
const parentElement = this.getParentElement(element);
|
||||
|
||||
if (parentElement && parentElement.matches('ion-button, ion-back-button')) {
|
||||
if (parentElement?.matches('ion-button, ion-back-button')) {
|
||||
element = parentElement;
|
||||
} else if (parentElement?.tagName === 'ION-ITEM' && parentElement?.classList.contains('clickable')) {
|
||||
element = parentElement.querySelector<HTMLElement>('ion-toggle') || element;
|
||||
}
|
||||
|
||||
const rect = await this.ensureElementVisible(element);
|
||||
|
@ -631,7 +678,13 @@ export class TestingBehatDomUtilsService {
|
|||
|
||||
// Functions to get/set value depending on field type.
|
||||
const setValue = (text: string) => {
|
||||
if (element.tagName === 'ION-SELECT' && 'value' in element) {
|
||||
if (! ('value' in element)) {
|
||||
element.innerHTML = text;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.tagName === 'ION-SELECT') {
|
||||
value = value.trim();
|
||||
const optionValue = Array.from(element.querySelectorAll('ion-select-option'))
|
||||
.find((option) => option.innerHTML.trim() === value);
|
||||
|
@ -639,11 +692,11 @@ export class TestingBehatDomUtilsService {
|
|||
if (optionValue) {
|
||||
element.value = optionValue.value;
|
||||
}
|
||||
} else if ('value' in element) {
|
||||
element.value = text;
|
||||
} else {
|
||||
element.innerHTML = text;
|
||||
element.value = text;
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event('ionChange'));
|
||||
};
|
||||
const getValue = () => {
|
||||
if ('value' in element) {
|
||||
|
|
|
@ -326,7 +326,7 @@ export class TestingBehatRuntimeService {
|
|||
return 'ERROR: No element matches locator to find.';
|
||||
}
|
||||
|
||||
return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO';
|
||||
return TestingBehatDomUtils.isElementSelected(element) ? 'YES' : 'NO';
|
||||
} catch (error) {
|
||||
return 'ERROR: ' + error.message;
|
||||
}
|
||||
|
@ -508,12 +508,15 @@ export class TestingBehatRuntimeService {
|
|||
* @returns Value.
|
||||
*/
|
||||
protected getFieldValue(element: HTMLElement | HTMLInputElement): string {
|
||||
if (element.tagName === 'ION-DATETIME-BUTTON' && element.shadowRoot) {
|
||||
return Array.from(element.shadowRoot.querySelectorAll('button')).map(button => button.innerText).join(' ');
|
||||
}
|
||||
|
||||
if (element.tagName === 'ION-DATETIME') {
|
||||
// ion-datetime's value is a timestamp in ISO format. Use the text displayed to the user instead.
|
||||
const dateTimeTextElement = element.shadowRoot?.querySelector<HTMLElement>('.datetime-text');
|
||||
if (dateTimeTextElement) {
|
||||
return dateTimeTextElement.innerText;
|
||||
}
|
||||
const value = 'value' in element ? element.value : element.innerText;
|
||||
|
||||
// Remove seconds from the value to ensure stability on tests. It could be improved using moment parsing if needed.
|
||||
return value.substring(0, value.length - 3);
|
||||
}
|
||||
|
||||
return 'value' in element ? element.value : element.innerText;
|
||||
|
|
|
@ -1161,7 +1161,7 @@ ion-select-popover {
|
|||
|
||||
ion-item.core-select-option-title {
|
||||
cursor: pointer;
|
||||
ion-radio {
|
||||
ion-radio::part(container) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue