From 44606242fd79ffb876d6b89036feba44f1ffe873 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 11 Jan 2024 15:25:49 +0100 Subject: [PATCH] MOBILE-3947 reminders: Fix tests --- .../tests/behat/behat_app.php | 9 +- .../tests/behat/activity_reminders.feature | 28 ++-- .../tests/behat/course_reminders.feature | 17 +-- src/testing/services/behat-dom.ts | 138 +++++++++++++----- src/testing/services/behat-runtime.ts | 21 ++- 5 files changed, 139 insertions(+), 74 deletions(-) diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index aeacba67a..c3a5d65ab 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -781,13 +781,10 @@ class behat_app extends behat_app_helper { /** * 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$/ - * @param string $field Text identifying field - * @param string $value Value for field - * @throws DriverException If the field set doesn't work + * @param string $field Text identifying the 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. */ public function i_set_the_field_in_the_app(string $field, string $value) { $field = addslashes_js($field); diff --git a/src/core/features/reminders/tests/behat/activity_reminders.feature b/src/core/features/reminders/tests/behat/activity_reminders.feature index 00c8224a2..10ec6f0f6 100644 --- a/src/core/features/reminders/tests/behat/activity_reminders.feature +++ b/src/core/features/reminders/tests/behat/activity_reminders.feature @@ -16,52 +16,48 @@ Feature: Set a new reminder on activity | assign | C1 | assign01 | Assignment 01 | ## yesterday ## | ## now +70 minutes ## | | assign | C1 | assign02 | Assignment 02 | ## yesterday ## | ## 1 January 2050 ## | - @ionic7_failure 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 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 "Set a reminder for \"Assignment 01\" (Due)" should not be selected in the app + And I should not find "Reminder set for" in the app + But I should find "Set a reminder for \"Assignment 01\" (Due)" in the app # Default set When I press "Set a reminder for \"Assignment 01\" (Due)" 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 + Then I should find "Reminder set for" in the app # Set from list When I press "Set a reminder for \"Assignment 01\" (Due)" 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 "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 - Then I should find "Reminder set for " in the app - And "Set a reminder for \"Assignment 01\" (Due)" should be selected in the app + Then I should find "Reminder set for" in the app # Custom set When I press "Set a reminder for \"Assignment 01\" (Due)" 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 + But "At the time of the event" should not be selected in the app When I press "Custom..." in the app Then I should find "Custom reminder" in the app When I set the following fields to these values in the app: | Value | 4 | | Units | minutes | And I press "Set reminder" 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 + Then I should find "Reminder set for" in the app # Remove When I press "Set a reminder for \"Assignment 01\" (Due)" in the app Then "4 minutes before" should be selected in the app When I press "Delete reminder" 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 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 When I press "Set a reminder for \"Assignment 01\" (Due)" in the app And I press "Custom..." in the app Then I should find "Custom reminder" in the app @@ -69,7 +65,7 @@ Feature: Set a new reminder on activity | Value | 69 | | Units | minutes | 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 When I wait "50" seconds Then a notification with title "Due: Assignment 01" is present in the app And I close a notification with title "Due: Assignment 01" in the app @@ -82,9 +78,9 @@ Feature: Set a new reminder on activity | Value | 68 | | Units | minutes | 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 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 When I press "Delete reminder" in the app Then I should find "Reminder deleted" in the app When I wait "50" seconds diff --git a/src/core/features/reminders/tests/behat/course_reminders.feature b/src/core/features/reminders/tests/behat/course_reminders.feature index 670d1d8c8..c5dc56e8a 100644 --- a/src/core/features/reminders/tests/behat/course_reminders.feature +++ b/src/core/features/reminders/tests/behat/course_reminders.feature @@ -12,34 +12,33 @@ Feature: Set a new reminder on course | user | course | role | | student1 | C1 | student | - @ionic7_failure Scenario: Add, delete and update reminder on course Given I entered the course "Course 1" as "student1" 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 - And I should find "Set a reminder for \"Course 1\" (Course end date)" in the app - And "Set a reminder for \"Course 1\" (Course end date)" should not be selected in the app + And I should not find "Reminder set for" in the app + But I should find "Set a reminder for \"Course 1\" (Course end date)" in the app # Default set 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 - 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 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 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 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 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 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 Then I should find "Custom reminder" 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 | And I press "Set reminder" 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 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 When I press "Delete reminder" 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 diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 76c812564..8d8fff76a 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -276,7 +276,7 @@ export class TestingBehatDomUtilsService { /** * 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. * @returns Top ancestors. @@ -480,6 +480,34 @@ export class TestingBehatDomUtilsService { 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( + selector: string, + timeout: number = 2000, + retryFrequency: number = 100, + ): Promise { + const element = document.querySelector(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(selector, timeout - retryFrequency, retryFrequency); + } + + return element; + } + /** * Function to find elements based on their text or Aria label. * @@ -515,7 +543,7 @@ export class TestingBehatDomUtilsService { protected findElementsBasedOnTextInContainer( locator: TestingBehatElementLocator, topContainer: HTMLElement, - options: TestingBehatFindOptions, + options: TestingBehatFindOptions = {}, ): HTMLElement[] { 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 value Value to be set. + * @param element Input element. + * @param value Value. */ - async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise { + async setInputValue(element: HTMLInputElement | HTMLElement, value: string): Promise { await NgZone.run(async () => { - const promise = new CorePromisedValue(); - // Functions to get/set value depending on field type. - const setValue = (text: string) => { - if (! ('value' in element)) { - element.innerHTML = text; - - return; - } - + const setValue = async (text: string) => { if (element.tagName === 'ION-SELECT') { - value = value.trim(); - const optionValue = Array.from(element.querySelectorAll('ion-select-option')) - .find((option) => option.innerHTML.trim() === value); - - if (optionValue) { - element.value = optionValue.value; - } - } else { + this.setIonSelectInputValue(element, value); + } else if ('value' in element) { element.value = text; + } else { + element.innerHTML = text; } element.dispatchEvent(new Event('ionChange')); }; + const getValue = () => { if ('value' in element) { return element.value; @@ -707,38 +724,79 @@ export class TestingBehatDomUtilsService { }; // Pretend we have cut and pasted the new text. - let event: InputEvent; - if (getValue() !== '') { - event = new InputEvent('input', { + if (element.tagName !== 'ION-SELECT' && getValue() !== '') { + await CoreUtils.nextTick(); + await setValue(''); + + element.dispatchEvent(new InputEvent('input', { bubbles: true, view: window, cancelable: true, inputType: 'deleteByCut', - }); - - await CoreUtils.nextTick(); - setValue(''); - element.dispatchEvent(event); + })); } if (value !== '') { - event = new InputEvent('input', { + await CoreUtils.nextTick(); + await setValue(value); + + element.dispatchEvent(new InputEvent('input', { bubbles: true, view: window, cancelable: true, inputType: 'insertFromPaste', data: value, - }); + })); + } + }); + } - await CoreUtils.nextTick(); - setValue(value); - element.dispatchEvent(event); + /** + * Select an option in an ion-select element. + * + * @param element IonSelect element. + * @param value Value. + */ + protected async setIonSelectInputValue(element: HTMLElement, value: string): Promise { + // Press select. + await TestingBehatDomUtils.pressElement(element); + + // 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('.alert-button-group button:last-child'); + + if (!submitButton) { + throw new Error('Couldn\'t find ion-select submit button.'); } - promise.resolve(); - - return promise; - }); + await TestingBehatDomUtils.pressElement(submitButton); + } } } diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 9a6e7d5fc..e7813e513 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TestingBehatDomUtils } from './behat-dom'; +import { TestingBehatDomUtils, TestingBehatDomUtilsService } from './behat-dom'; import { TestingBehatBlocking } from './behat-blocking'; import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; import { ONBOARDING_DONE } from '@features/login/constants'; @@ -63,6 +63,10 @@ export class TestingBehatRuntimeService { return CoreNavigator.instance; } + get domUtils(): TestingBehatDomUtilsService { + return TestingBehatDomUtils.instance; + } + /** * 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.includes(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'}`; + } } /**