From e974912880593bfe500aa9b8bb39ac6b8dbb55ed Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:16:33 +0200 Subject: [PATCH 01/12] MOBILE-4110 behat: Fix namespace --- src/testing/behat-testing.module.ts | 8 ++-- src/testing/services/behat-blocking.ts | 20 ++++----- src/testing/services/behat-dom.ts | 10 ++--- src/testing/services/behat-runtime.ts | 60 +++++++++++++------------- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/testing/behat-testing.module.ts b/src/testing/behat-testing.module.ts index 11bfde709..a06853375 100644 --- a/src/testing/behat-testing.module.ts +++ b/src/testing/behat-testing.module.ts @@ -14,12 +14,12 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { CoreAppProvider } from '@services/app'; -import { TestsBehatBlockingService } from './services/behat-blocking'; -import { BehatTestsWindow, TestsBehatRuntime } from './services/behat-runtime'; +import { TestingBehatBlockingService } from './services/behat-blocking'; +import { BehatTestsWindow, TestingBehatRuntime } from './services/behat-runtime'; function initializeBehatTestsWindow(window: BehatTestsWindow) { // Make functions publicly available for Behat to call. - window.behatInit = TestsBehatRuntime.init; + window.behatInit = TestingBehatRuntime.init; } @NgModule({ @@ -27,7 +27,7 @@ function initializeBehatTestsWindow(window: BehatTestsWindow) { CoreAppProvider.isAutomated() ? [ { provide: APP_INITIALIZER, multi: true, useValue: () => initializeBehatTestsWindow(window) }, - TestsBehatBlockingService, + TestingBehatBlockingService, ] : [], }) diff --git a/src/testing/services/behat-blocking.ts b/src/testing/services/behat-blocking.ts index 913ba00ab..92cb315cf 100644 --- a/src/testing/services/behat-blocking.ts +++ b/src/testing/services/behat-blocking.ts @@ -15,13 +15,13 @@ import { Injectable } from '@angular/core'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, NgZone } from '@singletons'; -import { BehatTestsWindow, TestsBehatRuntime } from './behat-runtime'; +import { BehatTestsWindow, TestingBehatRuntime } from './behat-runtime'; /** * Behat block JS manager. */ @Injectable({ providedIn: 'root' }) -export class TestsBehatBlockingService { +export class TestingBehatBlockingService { protected waitingBlocked = false; protected recentMutation = false; @@ -48,7 +48,7 @@ export class TestsBehatBlockingService { win.M.util = win.M.util ?? {}; win.M.util.pending_js = win.M.util.pending_js ?? []; - TestsBehatRuntime.log('Initialized!'); + TestingBehatRuntime.log('Initialized!'); } /** @@ -90,7 +90,7 @@ export class TestsBehatBlockingService { } this.pendingList.push(key); - TestsBehatRuntime.log('PENDING+: ' + this.pendingList); + TestingBehatRuntime.log('PENDING+: ' + this.pendingList); return key; } @@ -105,7 +105,7 @@ export class TestsBehatBlockingService { // Remove the key immediately. this.pendingList = this.pendingList.filter((x) => x !== key); - TestsBehatRuntime.log('PENDING-: ' + this.pendingList); + TestingBehatRuntime.log('PENDING-: ' + this.pendingList); // If the only thing left is DELAY, then remove that as well, later... if (this.pendingList.length === 1) { @@ -124,7 +124,7 @@ export class TestsBehatBlockingService { // Only remove it if the pending array is STILL empty after all that. if (this.pendingList.length === 1) { this.pendingList = []; - TestsBehatRuntime.log('PENDING-: ' + this.pendingList); + TestingBehatRuntime.log('PENDING-: ' + this.pendingList); } } } @@ -221,16 +221,16 @@ export class TestsBehatBlockingService { try { // Add to the list of pending requests. - TestsBehatBlocking.block(key); + TestingBehatBlocking.block(key); // Detect when it finishes and remove it from the list. this.addEventListener('loadend', () => { - TestsBehatBlocking.unblock(key); + TestingBehatBlocking.unblock(key); }); return realOpen.apply(this, args); } catch (error) { - TestsBehatBlocking.unblock(key); + TestingBehatBlocking.unblock(key); throw error; } }); @@ -239,4 +239,4 @@ export class TestsBehatBlockingService { } -export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService); +export const TestingBehatBlocking = makeSingleton(TestingBehatBlockingService); diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 1292f231a..e175313f4 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -15,7 +15,7 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CoreUtils } from '@services/utils/utils'; import { NgZone } from '@singletons'; -import { TestBehatElementLocator } from './behat-runtime'; +import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime'; // Containers that block containers behind them. const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'CORE-USER-TOURS-USER-TOUR', 'ION-PAGE']; @@ -23,7 +23,7 @@ const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'COR /** * Behat Dom Utils helper functions. */ -export class TestsBehatDomUtils { +export class TestingBehatDomUtils { /** * Check if an element is visible. @@ -328,7 +328,7 @@ export class TestsBehatDomUtils { * @param containerName Whether to search only inside a specific container. * @return First found element. */ - static findElementBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement { + static findElementBasedOnText(locator: TestingBehatElementLocator, containerName = ''): HTMLElement { return this.findElementsBasedOnText(locator, containerName, true)[0]; } @@ -341,7 +341,7 @@ export class TestsBehatDomUtils { * @return Found elements */ protected static findElementsBasedOnText( - locator: TestBehatElementLocator, + locator: TestingBehatElementLocator, containerName = '', stopWhenFound = false, ): HTMLElement[] { @@ -366,7 +366,7 @@ export class TestsBehatDomUtils { * @return Found elements */ protected static findElementsBasedOnTextInContainer( - locator: TestBehatElementLocator, + locator: TestingBehatElementLocator, topContainer: HTMLElement, ): HTMLElement[] { let container: HTMLElement | null = topContainer; diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 4a6866900..17fe661f1 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TestsBehatDomUtils } from './behat-dom'; -import { TestsBehatBlocking } from './behat-blocking'; +import { TestingBehatDomUtils } from './behat-dom'; +import { TestingBehatBlocking } from './behat-blocking'; import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreConfig } from '@services/config'; @@ -34,15 +34,15 @@ import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dash /** * Behat runtime servive with public API. */ -export class TestsBehatRuntime { +export class TestingBehatRuntime { /** * Init behat functions and set options like skipping onboarding. * * @param options Options to set on the app. */ - static init(options?: TestsBehatInitOptions): void { - TestsBehatBlocking.init(); + static init(options?: TestingBehatInitOptions): void { + TestingBehatBlocking.init(); (window as BehatTestsWindow).behat = { closePopup: TestsBehatRuntime.closePopup, @@ -104,14 +104,14 @@ export class TestsBehatRuntime { * @return Promise resolved when done. */ static async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { - const blockKey = TestsBehatBlocking.block(); + const blockKey = TestingBehatBlocking.block(); try { await NgZone.run(async () => { await CorePushNotifications.notificationClicked(data); }); } finally { - TestsBehatBlocking.unblock(blockKey); + TestingBehatBlocking.unblock(blockKey); } } @@ -156,20 +156,20 @@ export class TestsBehatRuntime { switch (button) { case 'back': - foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Back' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }); break; case 'main menu': // Deprecated name. case 'more menu': - foundButton = TestsBehatDomUtils.findElementBasedOnText({ + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'More', selector: 'ion-tab-button', }); break; case 'user menu' : - foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'User account' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }); break; case 'page menu': - foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Display options' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }); break; default: return 'ERROR: Unsupported standard button type'; @@ -180,7 +180,7 @@ export class TestsBehatRuntime { } // Click button - await TestsBehatDomUtils.pressElement(foundButton); + await TestingBehatDomUtils.pressElement(foundButton); return 'OK'; } @@ -206,7 +206,7 @@ export class TestsBehatRuntime { backdrop.click(); // Mark busy until the click finishes processing. - TestsBehatBlocking.delay(); + TestingBehatBlocking.delay(); return 'OK'; } @@ -218,11 +218,11 @@ export class TestsBehatRuntime { * @param containerName Whether to search only inside a specific container content. * @return OK if successful, or ERROR: followed by message */ - static find(locator: TestBehatElementLocator, containerName: string): string { + static find(locator: TestingBehatElementLocator, containerName: string): string { this.log('Action - Find', { locator, containerName }); try { - const element = TestsBehatDomUtils.findElementBasedOnText(locator, containerName); + const element = TestingBehatDomUtils.findElementBasedOnText(locator, containerName); if (!element) { return 'ERROR: No element matches locator to find.'; @@ -242,11 +242,11 @@ export class TestsBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static scrollTo(locator: TestBehatElementLocator): string { + static scrollTo(locator: TestingBehatElementLocator): string { this.log('Action - scrollTo', { locator }); try { - let element = TestsBehatDomUtils.findElementBasedOnText(locator); + let element = TestingBehatDomUtils.findElementBasedOnText(locator); if (!element) { return 'ERROR: No element matches element to scroll to.'; @@ -316,13 +316,13 @@ export class TestsBehatRuntime { * @param locator Element locator. * @return YES or NO if successful, or ERROR: followed by message */ - static isSelected(locator: TestBehatElementLocator): string { + static isSelected(locator: TestingBehatElementLocator): string { this.log('Action - Is Selected', locator); try { - const element = TestsBehatDomUtils.findElementBasedOnText(locator); + const element = TestingBehatDomUtils.findElementBasedOnText(locator); - return TestsBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; + return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; } catch (error) { return 'ERROR: ' + error.message; } @@ -334,17 +334,17 @@ export class TestsBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static async press(locator: TestBehatElementLocator): Promise { + static async press(locator: TestingBehatElementLocator): Promise { this.log('Action - Press', locator); try { - const found = TestsBehatDomUtils.findElementBasedOnText(locator); + const found = TestingBehatDomUtils.findElementBasedOnText(locator); if (!found) { return 'ERROR: No element matches locator to press.'; } - await TestsBehatDomUtils.pressElement(found); + await TestingBehatDomUtils.pressElement(found); return 'OK'; } catch (error) { @@ -394,7 +394,7 @@ export class TestsBehatRuntime { this.log('Action - Get header'); let titles = Array.from(document.querySelectorAll('.ion-page:not(.ion-page-hidden) > ion-header h1')); - titles = titles.filter((title) => TestsBehatDomUtils.isElementVisible(title, document.body)); + titles = titles.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body)); if (titles.length > 1) { return 'ERROR: Too many possible titles ('+titles.length+').'; @@ -419,7 +419,7 @@ export class TestsBehatRuntime { static async setField(field: string, value: string): Promise { this.log('Action - Set field ' + field + ' to: ' + value); - const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText( + const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText( { text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' }, ); @@ -427,7 +427,7 @@ export class TestsBehatRuntime { return 'ERROR: No element matches field to set.'; } - await TestsBehatDomUtils.setElementValue(found, value); + await TestingBehatDomUtils.setElementValue(found, value); return 'OK'; } @@ -478,14 +478,14 @@ export type BehatTestsWindow = Window & { behat?: unknown; }; -export type TestBehatElementLocator = { +export type TestingBehatElementLocator = { text: string; - within?: TestBehatElementLocator; - near?: TestBehatElementLocator; + within?: TestingBehatElementLocator; + near?: TestingBehatElementLocator; selector?: string; }; -export type TestsBehatInitOptions = { +export type TestingBehatInitOptions = { skipOnBoarding?: boolean; configOverrides?: Partial; }; From a0363deb6a2d098a31a6ff8f1e5679f2d025dd13 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:17:29 +0200 Subject: [PATCH 02/12] MOBILE-4110 behat: Allow finding disabled elements --- .../tests/behat/behat_app.php | 47 ++++++++++--- .../mod/data/tests/behat/entries.feature | 26 ++++--- src/testing/services/behat-dom.ts | 69 ++++++++++++------- src/testing/services/behat-runtime.ts | 38 ++++++---- src/tests/behat/runtime.feature | 16 +++++ 5 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 src/tests/behat/runtime.feature diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local-moodleappbehat/tests/behat/behat_app.php index 2130ce540..72c389209 100644 --- a/local-moodleappbehat/tests/behat/behat_app.php +++ b/local-moodleappbehat/tests/behat/behat_app.php @@ -126,14 +126,14 @@ class behat_app extends behat_app_helper { $containerName = json_encode($containerName); $this->spin(function() use ($not, $locator, $containerName) { - $result = $this->js("return window.behat.find($locator, $containerName);"); + $result = $this->js("return window.behat.find($locator, { containerName: $containerName });"); if ($not && $result === 'OK') { - throw new DriverException('Error, found an item that should not be found'); + throw new DriverException('Error, found an element that should not be found'); } if (!$not && $result !== 'OK') { - throw new DriverException('Error finding item - ' . $result); + throw new DriverException('Error finding element - ' . $result); } return true; @@ -155,7 +155,7 @@ class behat_app extends behat_app_helper { $result = $this->js("return window.behat.scrollTo($locator);"); if ($result !== 'OK') { - throw new DriverException('Error finding item - ' . $result); + throw new DriverException('Error finding element - ' . $result); } return true; @@ -224,16 +224,16 @@ class behat_app extends behat_app_helper { switch ($result) { case 'YES': if ($not) { - throw new ExpectationException("Item was selected and shouldn't have", $this->getSession()->getDriver()); + throw new ExpectationException("Element was selected and shouldn't have", $this->getSession()->getDriver()); } break; case 'NO': if (!$not) { - throw new ExpectationException("Item wasn't selected and should have", $this->getSession()->getDriver()); + throw new ExpectationException("Element wasn't selected and should have", $this->getSession()->getDriver()); } break; default: - throw new DriverException('Error finding item - ' . $result); + throw new DriverException('Error finding element - ' . $result); } return true; @@ -536,7 +536,7 @@ class behat_app extends behat_app_helper { * Clicks on / touches something that is visible in the app. * * Note it is difficult to use the standard 'click on' or 'press' steps because those do not - * distinguish visible items and the app always has many non-visible items in the DOM. + * distinguish visible elements and the app always has many non-visible elements in the DOM. * * @When /^I press (".+") in the app$/ * @param string $locator Element locator @@ -578,6 +578,33 @@ class behat_app extends behat_app_helper { $this->wait_for_pending_js(); } + /** + * Checks if elements can be pressed in the app. + * + * @Then /^I should( not)? be able to press (".+") in the app$/ + * @param bool $not Whether to assert that the element cannot be pressed + * @param string $locator Element locator + */ + public function i_should_be_able_to_press_in_the_app(bool $not, string $locator) { + $locator = $this->parse_element_locator($locator); + + $this->spin(function() use ($not, $locator) { + $result = $this->js("return window.behat.find($locator, { onlyClickable: true });"); + + if ($not && $result === 'OK') { + throw new DriverException('Error, found a clickable element that should not be found'); + } + + if (!$not && $result !== 'OK') { + throw new DriverException('Error finding clickable element - ' . $result); + } + + return true; + }); + + $this->wait_for_pending_js(); + } + /** * Select an item from a list of options, such as a radio button. * @@ -602,11 +629,11 @@ class behat_app extends behat_app_helper { return true; } - // Press item. + // Press element. $result = $this->js("return await window.behat.press($locator);"); if ($result !== 'OK') { - throw new DriverException('Error pressing item - ' . $result); + throw new DriverException('Error pressing element - ' . $result); } // Check that it worked as expected. diff --git a/src/addons/mod/data/tests/behat/entries.feature b/src/addons/mod/data/tests/behat/entries.feature index 0551dc081..25f960d88 100644 --- a/src/addons/mod/data/tests/behat/entries.feature +++ b/src/addons/mod/data/tests/behat/entries.feature @@ -45,8 +45,9 @@ Feature: Users can manage entries in database activities Scenario: Browse entry Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app + # TODO Create and use a generator for database entries. - And I press "Add entries" in the app + When I press "Add entries" in the app And I set the following fields to these values in the app: | URL | https://moodle.org/ | | Description | Moodle community site | @@ -59,16 +60,19 @@ Feature: Users can manage entries in database activities And I press "Save" near "Web links" in the app And I press "More" near "Moodle community site" in the app Then I should find "Moodle community site" in the app - And I should not find "Next" in the app - And I should find "Previous" in the app - And I press "Previous" in the app - And I should find "Moodle Cloud" in the app - And I should find "Next" in the app - And I should not find "Previous" in the app - And I press "Next" in the app - And I should find "Moodle community site" in the app - And I should not find "Moodle Cloud" in the app - And I press the back button in the app + And I should be able to press "Previous" in the app + But I should not be able to press "Next" in the app + + When I press "Previous" in the app + Then I should find "Moodle Cloud" in the app + And I should be able to press "Next" in the app + But I should not be able to press "Previous" in the app + + When I press "Next" in the app + Then I should find "Moodle community site" in the app + But I should not find "Moodle Cloud" in the app + + When I press the back button in the app And I should find "Moodle community site" in the app And I should find "Moodle Cloud" in the app diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index e175313f4..a5e901c2a 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -79,9 +79,14 @@ export class TestingBehatDomUtils { * * @param container Parent element to search the element within * @param text Text to look for + * @param options Search options. * @return Elements containing the given text with exact boolean. */ - protected static findElementsBasedOnTextWithinWithExact(container: HTMLElement, text: string): ElementsWithExact[] { + protected static findElementsBasedOnTextWithinWithExact( + container: HTMLElement, + text: string, + options: TestingBehatFindOptions, + ): ElementsWithExact[] { const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"], [placeholder*="${text}"]`; const elements = Array.from(container.querySelectorAll(attributesSelector)) @@ -97,16 +102,23 @@ export class TestingBehatDomUtils { NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT | NodeFilter.SHOW_TEXT, // eslint-disable-line no-bitwise { acceptNode: node => { - if (node instanceof HTMLStyleElement || + if ( + node instanceof HTMLStyleElement || node instanceof HTMLLinkElement || - node instanceof HTMLScriptElement) { + node instanceof HTMLScriptElement + ) { return NodeFilter.FILTER_REJECT; } - if (node instanceof HTMLElement && - (node.getAttribute('aria-hidden') === 'true' || - node.getAttribute('aria-disabled') === 'true' || - getComputedStyle(node).display === 'none')) { + if (!(node instanceof HTMLElement)) { + return NodeFilter.FILTER_ACCEPT; + } + + if (options.onlyClickable && (node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled'))) { + return NodeFilter.FILTER_REJECT; + } + + if (node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none') { return NodeFilter.FILTER_REJECT; } @@ -160,7 +172,7 @@ export class TestingBehatDomUtils { continue; } - elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text)); + elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text, options)); } } } @@ -187,10 +199,15 @@ export class TestingBehatDomUtils { * * @param container Parent element to search the element within. * @param text Text to look for. + * @param options Search options. * @return Elements containing the given text. */ - protected static findElementsBasedOnTextWithin(container: HTMLElement, text: string): HTMLElement[] { - const elements = this.findElementsBasedOnTextWithinWithExact(container, text); + protected static findElementsBasedOnTextWithin( + container: HTMLElement, + text: string, + options: TestingBehatFindOptions, + ): HTMLElement[] { + const elements = this.findElementsBasedOnTextWithinWithExact(container, text, options); // Give more relevance to exact matches. elements.sort((a, b) => Number(b.exact) - Number(a.exact)); @@ -325,32 +342,33 @@ export class TestingBehatDomUtils { * Function to find element based on their text or Aria label. * * @param locator Element locator. - * @param containerName Whether to search only inside a specific container. + * @param options Search options. * @return First found element. */ - static findElementBasedOnText(locator: TestingBehatElementLocator, containerName = ''): HTMLElement { - return this.findElementsBasedOnText(locator, containerName, true)[0]; + static findElementBasedOnText( + locator: TestingBehatElementLocator, + options: TestingBehatFindOptions, + ): HTMLElement { + return this.findElementsBasedOnText(locator, options)[0]; } /** * Function to find elements based on their text or Aria label. * * @param locator Element locator. - * @param containerName Whether to search only inside a specific container. - * @param stopWhenFound Stop looking in containers once an element is found. + * @param options Search options. * @return Found elements */ protected static findElementsBasedOnText( locator: TestingBehatElementLocator, - containerName = '', - stopWhenFound = false, + options: TestingBehatFindOptions, ): HTMLElement[] { - const topContainers = this.getCurrentTopContainerElements(containerName); + const topContainers = this.getCurrentTopContainerElements(options.containerName); let elements: HTMLElement[] = []; for (let i = 0; i < topContainers.length; i++) { - elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i])); - if (stopWhenFound && elements.length) { + elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i], options)); + if (elements.length) { break; } } @@ -363,16 +381,18 @@ export class TestingBehatDomUtils { * * @param locator Element locator. * @param topContainer Container to search in. + * @param options Search options. * @return Found elements */ protected static findElementsBasedOnTextInContainer( locator: TestingBehatElementLocator, topContainer: HTMLElement, + options: TestingBehatFindOptions, ): HTMLElement[] { let container: HTMLElement | null = topContainer; if (locator.within) { - const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer); + const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options); if (withinElements.length === 0) { throw new Error('There was no match for within text'); @@ -390,7 +410,10 @@ export class TestingBehatDomUtils { } if (topContainer && locator.near) { - const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer); + const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer, { + ...options, + onlyClickable: false, + }); if (nearElements.length === 0) { throw new Error('There was no match for near text'); @@ -412,7 +435,7 @@ export class TestingBehatDomUtils { break; } - const elements = this.findElementsBasedOnTextWithin(container, locator.text); + const elements = this.findElementsBasedOnTextWithin(container, locator.text, options); let filteredElements: HTMLElement[] = elements; diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 17fe661f1..554ae6a7d 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -153,23 +153,27 @@ export class TestingBehatRuntime { // Find button let foundButton: HTMLElement | undefined; + const options: TestingBehatFindOptions = { + onlyClickable: true, + containerName: '', + }; switch (button) { case 'back': - foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }, options); break; case 'main menu': // Deprecated name. case 'more menu': foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'More', selector: 'ion-tab-button', - }); + }, options); break; case 'user menu' : - foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }, options); break; case 'page menu': - foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }); + foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }, options); break; default: return 'ERROR: Unsupported standard button type'; @@ -215,20 +219,24 @@ export class TestingBehatRuntime { * Function to find an arbitrary element based on its text or aria label. * * @param locator Element locator. - * @param containerName Whether to search only inside a specific container content. + * @param options Search options. * @return OK if successful, or ERROR: followed by message */ - static find(locator: TestingBehatElementLocator, containerName: string): string { - this.log('Action - Find', { locator, containerName }); + static find(locator: TestingBehatElementLocator, options: Partial = {}): string { + this.log('Action - Find', { locator, ...options }); try { - const element = TestingBehatDomUtils.findElementBasedOnText(locator, containerName); + const element = TestingBehatDomUtils.findElementBasedOnText(locator, { + onlyClickable: false, + containerName: '', + ...options, + }); if (!element) { return 'ERROR: No element matches locator to find.'; } - this.log('Action - Found', { locator, containerName, element }); + this.log('Action - Found', { locator, element, ...options }); return 'OK'; } catch (error) { @@ -246,7 +254,7 @@ export class TestingBehatRuntime { this.log('Action - scrollTo', { locator }); try { - let element = TestingBehatDomUtils.findElementBasedOnText(locator); + let element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' }); if (!element) { return 'ERROR: No element matches element to scroll to.'; @@ -320,7 +328,7 @@ export class TestingBehatRuntime { this.log('Action - Is Selected', locator); try { - const element = TestingBehatDomUtils.findElementBasedOnText(locator); + const element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' }); return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; } catch (error) { @@ -338,7 +346,7 @@ export class TestingBehatRuntime { this.log('Action - Press', locator); try { - const found = TestingBehatDomUtils.findElementBasedOnText(locator); + const found = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: true, containerName: '' }); if (!found) { return 'ERROR: No element matches locator to press.'; @@ -421,6 +429,7 @@ export class TestingBehatRuntime { const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText( { text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' }, + { onlyClickable: false, containerName: '' }, ); if (!found) { @@ -478,6 +487,11 @@ export type BehatTestsWindow = Window & { behat?: unknown; }; +export type TestingBehatFindOptions = { + containerName: string; + onlyClickable: boolean; +}; + export type TestingBehatElementLocator = { text: string; within?: TestingBehatElementLocator; diff --git a/src/tests/behat/runtime.feature b/src/tests/behat/runtime.feature new file mode 100644 index 000000000..2b9c03657 --- /dev/null +++ b/src/tests/behat/runtime.feature @@ -0,0 +1,16 @@ +@app @javascript +Feature: It has a Behat runtime with testing helpers. + + Background: + Given the following "users" exist: + | username | + | student1 | + + Scenario: Finds and presses elements + Given I entered the app as "student1" + When I set the following fields to these values in the app: + | Search by activity type or name | Foo bar | + Then I should find "Search" "button" in the app + And I should find "Clear search" in the app + And I should be able to press "Search" "button" in the app + But I should not be able to press "Clear search" in the app From 9d1d41348f4b98dd1248adb220618ca2dfa79644 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:17:57 +0200 Subject: [PATCH 03/12] MOBILE-4110 behat: Fix configuration overrides --- src/testing/services/behat-runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 554ae6a7d..369683109 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -75,7 +75,7 @@ export class TestingBehatRuntime { if (options.configOverrides) { // Set the cookie so it's maintained between reloads. document.cookie = 'MoodleAppConfig=' + JSON.stringify(options.configOverrides); - CoreConfig.patchEnvironment(options.configOverrides); + CoreConfig.patchEnvironment(options.configOverrides, { patchDefault: true }); } } From 97be254b725190a209c8c41e8b0d1e51f17d4a58 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:19:10 +0200 Subject: [PATCH 04/12] MOBILE-4110 comments: Use behat generators --- .../comments/tests/behat/basic_usage.feature | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/src/core/features/comments/tests/behat/basic_usage.feature b/src/core/features/comments/tests/behat/basic_usage.feature index 86a424627..9fef313be 100644 --- a/src/core/features/comments/tests/behat/basic_usage.feature +++ b/src/core/features/comments/tests/behat/basic_usage.feature @@ -220,22 +220,9 @@ Feature: Test basic usage of comments in app Scenario: Add comments & Delete comments (blogs) # Create blog as a teacher - Given the following "blocks" exist: - | blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata | - | blog_menu | Course | C1 | course-view-* | site-pre | | - And I entered the course "Course 1" as "teacher1" in the app - And I press "Course summary" in the app - # TODO Create and use a generator blog entries. - And I press "Open in browser" in the app - And I switch to the browser tab opened by the app - And I log in as "teacher1" - And I click on "Open block drawer" "button" - And I click on "Add an entry about this course" "link" in the "Blog menu" "block" - And I set the following fields to these values: - | Entry title | Blog test | - | Blog entry body | Blog body | - And I press "Save changes" - And I close the browser tab opened by the app + Given the following "core_blog > entries" exist: + | subject | body | user | + | Blog test | Blog body | teacher1 | # Create and delete comments as a student When I entered the app as "student1" @@ -263,21 +250,9 @@ Feature: Test basic usage of comments in app Scenario: Add comments offline & Delete comments offline & Sync comments (blogs) # Create blog as a teacher - Given the following "blocks" exist: - | blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata | - | blog_menu | Course | C1 | course-view-* | site-pre | | - And I entered the course "Course 1" as "teacher1" in the app - And I press "Course summary" in the app - And I press "Open in browser" in the app - And I switch to the browser tab opened by the app - And I log in as "teacher1" - And I click on "Open block drawer" "button" - And I click on "Add an entry about this course" "link" in the "Blog menu" "block" - And I set the following fields to these values: - | Entry title | Blog test | - | Blog entry body | Blog body | - And I press "Save changes" - And I close the browser tab opened by the app + Given the following "core_blog > entries" exist: + | subject | body | user | + | Blog test | Blog body | teacher1 | # Create and delete comments as a student When I entered the app as "student1" From 681b33fd5edbfcac9e35531cf89eab7c23f936a3 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:55:06 +0200 Subject: [PATCH 05/12] MOBILE-4110 e2e: Remove deprecated travis jobs --- .travis.yml | 18 ---------------- scripts/test_e2e.sh | 50 --------------------------------------------- 2 files changed, 68 deletions(-) delete mode 100755 scripts/test_e2e.sh diff --git a/.travis.yml b/.travis.yml index 591f20841..4232f486c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,21 +68,3 @@ jobs: homebrew: packages: - jq - - stage: test - name: "End to end tests (mod_forum and mod_messages)" - services: - - docker - if: type = cron - script: scripts/test_e2e.sh "@app&&@mod_forum" "@app&&@mod_messages" - - stage: test - name: "End to end tests (mod_course, core_course and mod_courses)" - services: - - docker - if: type = cron - script: scripts/test_e2e.sh "@app&&@mod_course" "@app&&@core_course" "@app&&@mod_courses" - - stage: test - name: "End to end tests (others)" - services: - - docker - if: type = cron - script: scripts/test_e2e.sh "@app&&~@mod_forum&&~@mod_messages&&~@mod_course&&~@core_course&&~@mod_courses" diff --git a/scripts/test_e2e.sh b/scripts/test_e2e.sh deleted file mode 100755 index 6e363aa24..000000000 --- a/scripts/test_e2e.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -source "scripts/functions.sh" - -# Prepare variables -basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../" && pwd )" -dockerscripts="$HOME/moodle-docker/bin/" -dockercompose="$dockerscripts/moodle-docker-compose" - -export MOODLE_DOCKER_DB=pgsql -export MOODLE_DOCKER_BROWSER=chrome -export MOODLE_DOCKER_WWWROOT="$HOME/moodle" -export MOODLE_DOCKER_PHP_VERSION=7.4 -export MOODLE_DOCKER_APP_PATH=$basedir - -# Prepare dependencies -print_title "Preparing dependencies" -git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle -git clone --branch ionic5 --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp - -# TODO replace for moodlehq/moodle-docker after merging https://github.com/moodlehq/moodle-docker/pull/156 -git clone --branch MOBILE-3738 --depth 1 git://github.com/NoelDeMartin/moodle-docker $HOME/moodle-docker - -cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php - -# Build app -print_title "Building app" -npm ci - -# Start containers -print_title "Starting containers" -$dockercompose pull -$dockercompose up -d -$dockerscripts/moodle-docker-wait-for-db -$dockerscripts/moodle-docker-wait-for-app - -$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/init.php" -notify_on_error_exit "e2e failed initializing behat" - -print_title "Running e2e tests" - -# Run tests -for tags in "$@" -do - $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\" --auto-rerun" - notify_on_error_exit "Some e2e tests are failing, please review" -done - -# Clean up -$dockercompose down From b0ccc7bf34682ff5d480e94bfd634cb059c37304 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Jun 2022 12:58:06 +0200 Subject: [PATCH 06/12] MOBILE-4110 behat: Configure for PRs --- .github/workflows/acceptance.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 035644cf3..2f45858cd 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -1,10 +1,10 @@ -name: Behat tests +name: Acceptance tests (Behat) on: workflow_dispatch: inputs: - tags: - description: 'Execute tags' + behat_tags: + description: 'Behat tags to execute' required: true default: '~@performance' moodle_branch: @@ -15,6 +15,10 @@ on: description: 'Moodle repository' required: true default: 'https://github.com/moodle/moodle' + pull_request: + branches: + - integration + - unscheduled jobs: behat: @@ -23,6 +27,9 @@ jobs: MOODLE_DOCKER_DB: pgsql MOODLE_DOCKER_BROWSER: chrome MOODLE_DOCKER_PHP_VERSION: 7.3 + MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }} + MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} + BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }} steps: - uses: actions/checkout@v2 - id: nvmrc @@ -32,7 +39,7 @@ jobs: node-version: '${{ steps.nvmrc.outputs.node_version }}' - name: Additional checkouts run: | - git clone --branch ${{ github.event.inputs.moodle_branch }} --depth 1 ${{ github.event.inputs.moodle_repository }} $GITHUB_WORKSPACE/moodle + 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 - name: Install npm packages run: npm ci --no-audit @@ -60,4 +67,4 @@ jobs: - name: Run Behat tests run: | export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle - $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&${{ github.event.inputs.tags }}' --auto-rerun" + $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&$BEHAT_TAGS' --auto-rerun" From c8b16035fea3fda2d9a4c22e07ced9fd2cc573d2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 30 Jun 2022 13:06:07 +0200 Subject: [PATCH 07/12] MOBILE-4110 behat: Clean up services --- angular.json | 6 ++ .../tests/behat/behat_app.php | 5 +- .../tests/behat/behat_app_helper.php | 2 +- src/app/app.component.ts | 14 +-- src/app/app.module.ts | 4 +- src/testing/behat-testing.module.ts | 34 ------- src/testing/services/behat-dom.ts | 38 ++++---- src/testing/services/behat-runtime.ts | 90 +++++++++---------- ....module.prod.ts => testing.module.prod.ts} | 2 +- .../testing.module.ts} | 18 ++-- 10 files changed, 90 insertions(+), 123 deletions(-) delete mode 100644 src/testing/behat-testing.module.ts rename src/testing/{behat-testing.module.prod.ts => testing.module.prod.ts} (95%) rename src/{core/initializers/prepare-automated-tests.ts => testing/testing.module.ts} (66%) diff --git a/angular.json b/angular.json index e27e74ea2..376a770fd 100644 --- a/angular.json +++ b/angular.json @@ -46,6 +46,12 @@ }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/testing/testing.module.ts", + "with": "src/testing/testing.module.prod.ts" + } + ], "optimization": { "scripts": false, "styles": true diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local-moodleappbehat/tests/behat/behat_app.php index 72c389209..05e89be8e 100644 --- a/local-moodleappbehat/tests/behat/behat_app.php +++ b/local-moodleappbehat/tests/behat/behat_app.php @@ -96,9 +96,8 @@ class behat_app extends behat_app_helper { public function i_wait_the_app_to_restart() { // Wait window to reload. $this->spin(function() { - $result = $this->js("return !window.behat;"); - - if (!$result) { + if ($this->js('window.behat.hasInitialized()')) { + // Behat runtime shouldn't be initialized after reload. throw new DriverException('Window is not reloading properly.'); } diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local-moodleappbehat/tests/behat/behat_app_helper.php index b929e3402..646b6e141 100644 --- a/local-moodleappbehat/tests/behat/behat_app_helper.php +++ b/local-moodleappbehat/tests/behat/behat_app_helper.php @@ -318,7 +318,7 @@ class behat_app_helper extends behat_base { $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; $initOptions->configOverrides = $this->appconfig; - $this->js('window.behatInit(' . json_encode($initOptions) . ');'); + $this->js('window.behat.init(' . json_encode($initOptions) . ');'); } catch (Exception $error) { throw new DriverException('Moodle App not running or not running on Automated mode.'); } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 912c8b785..ec1d66cc4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { IonRouterOutlet } from '@ionic/angular'; import { BackButtonEvent, ScrollDetail } from '@ionic/core'; @@ -21,7 +21,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreEvents } from '@singletons/events'; import { NgZone, SplashScreen, Translate } from '@singletons'; import { CoreNetwork } from '@services/network'; -import { CoreApp, CoreAppProvider } from '@services/app'; +import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreNavigator } from '@services/navigator'; import { CoreSubscriptions } from '@singletons/subscriptions'; @@ -38,10 +38,6 @@ import { CorePlatform } from '@services/platform'; const MOODLE_VERSION_PREFIX = 'version-'; const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; -type AutomatedTestsWindow = Window & { - changeDetector?: ChangeDetectorRef; -}; - @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -54,12 +50,6 @@ export class AppComponent implements OnInit, AfterViewInit { protected lastUrls: Record = {}; protected lastInAppUrl?: string; - constructor(changeDetector: ChangeDetectorRef) { - if (CoreAppProvider.isAutomated()) { - (window as AutomatedTestsWindow).changeDetector = changeDetector; - } - } - /** * Component being initialized. * diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 82928a691..99683c1f7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,7 +32,7 @@ import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; import { CoreCronDelegate } from '@services/cron'; import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; import { moodleTransitionAnimation } from '@classes/page-transition'; -import { BehatTestingModule } from '@/testing/behat-testing.module'; +import { TestingModule } from '@/testing/testing.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -60,7 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { AppRoutingModule, CoreModule, AddonsModule, - BehatTestingModule, + TestingModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/src/testing/behat-testing.module.ts b/src/testing/behat-testing.module.ts deleted file mode 100644 index a06853375..000000000 --- a/src/testing/behat-testing.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { APP_INITIALIZER, NgModule } from '@angular/core'; -import { CoreAppProvider } from '@services/app'; -import { TestingBehatBlockingService } from './services/behat-blocking'; -import { BehatTestsWindow, TestingBehatRuntime } from './services/behat-runtime'; - -function initializeBehatTestsWindow(window: BehatTestsWindow) { - // Make functions publicly available for Behat to call. - window.behatInit = TestingBehatRuntime.init; -} - -@NgModule({ - providers: - CoreAppProvider.isAutomated() - ? [ - { provide: APP_INITIALIZER, multi: true, useValue: () => initializeBehatTestsWindow(window) }, - TestingBehatBlockingService, - ] - : [], -}) -export class BehatTestingModule {} diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index a5e901c2a..101cb9657 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Injectable } from '@angular/core'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreUtils } from '@services/utils/utils'; -import { NgZone } from '@singletons'; +import { makeSingleton, NgZone } from '@singletons'; import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime'; // Containers that block containers behind them. @@ -23,7 +24,8 @@ const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'COR /** * Behat Dom Utils helper functions. */ -export class TestingBehatDomUtils { +@Injectable({ providedIn: 'root' }) +export class TestingBehatDomUtilsService { /** * Check if an element is visible. @@ -32,7 +34,7 @@ export class TestingBehatDomUtils { * @param container Container. * @return Whether the element is visible or not. */ - static isElementVisible(element: HTMLElement, container: HTMLElement): boolean { + isElementVisible(element: HTMLElement, container: HTMLElement): boolean { if (element.getAttribute('aria-hidden') === 'true' || getComputedStyle(element).display === 'none') { return false; } @@ -56,7 +58,7 @@ export class TestingBehatDomUtils { * @param container Container. * @return Whether the element is selected or not. */ - static isElementSelected(element: HTMLElement, container: HTMLElement): boolean { + isElementSelected(element: HTMLElement, container: HTMLElement): boolean { const ariaCurrent = element.getAttribute('aria-current'); if ( (ariaCurrent && ariaCurrent !== 'false') || @@ -82,7 +84,7 @@ export class TestingBehatDomUtils { * @param options Search options. * @return Elements containing the given text with exact boolean. */ - protected static findElementsBasedOnTextWithinWithExact( + protected findElementsBasedOnTextWithinWithExact( container: HTMLElement, text: string, options: TestingBehatFindOptions, @@ -187,7 +189,7 @@ export class TestingBehatDomUtils { * @param text Text to check. * @return If text matches any of the label attributes. */ - protected static checkElementLabel(element: HTMLElement, text: string): boolean { + protected checkElementLabel(element: HTMLElement, text: string): boolean { return element.title === text || element.getAttribute('alt') === text || element.getAttribute('aria-label') === text || @@ -202,7 +204,7 @@ export class TestingBehatDomUtils { * @param options Search options. * @return Elements containing the given text. */ - protected static findElementsBasedOnTextWithin( + protected findElementsBasedOnTextWithin( container: HTMLElement, text: string, options: TestingBehatFindOptions, @@ -223,7 +225,7 @@ export class TestingBehatDomUtils { * @param elements Elements list. * @return Top ancestors. */ - protected static getTopAncestors(elements: HTMLElement[]): HTMLElement[] { + protected getTopAncestors(elements: HTMLElement[]): HTMLElement[] { const uniqueElements = new Set(elements); for (const element of uniqueElements) { @@ -247,7 +249,7 @@ export class TestingBehatDomUtils { * @param element Element. * @return Parent element. */ - protected static getParentElement(element: HTMLElement): HTMLElement | null { + protected getParentElement(element: HTMLElement): HTMLElement | null { return element.parentElement || (element.getRootNode() && (element.getRootNode() as ShadowRoot).host as HTMLElement) || null; @@ -261,7 +263,7 @@ export class TestingBehatDomUtils { * @param container Topmost container to search within. * @return Closest matching element. */ - protected static getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null { + protected getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null { if (element.matches(selector)) { return element; } @@ -279,7 +281,7 @@ export class TestingBehatDomUtils { * @param containerName Whether to search inside the a container name. * @return Found top container elements. */ - protected static getCurrentTopContainerElements(containerName: string): HTMLElement[] { + protected getCurrentTopContainerElements(containerName: string): HTMLElement[] { const topContainers: HTMLElement[] = []; let containers = Array.from(document.querySelectorAll([ 'ion-alert.hydrated', @@ -345,7 +347,7 @@ export class TestingBehatDomUtils { * @param options Search options. * @return First found element. */ - static findElementBasedOnText( + findElementBasedOnText( locator: TestingBehatElementLocator, options: TestingBehatFindOptions, ): HTMLElement { @@ -359,7 +361,7 @@ export class TestingBehatDomUtils { * @param options Search options. * @return Found elements */ - protected static findElementsBasedOnText( + protected findElementsBasedOnText( locator: TestingBehatElementLocator, options: TestingBehatFindOptions, ): HTMLElement[] { @@ -384,7 +386,7 @@ export class TestingBehatDomUtils { * @param options Search options. * @return Found elements */ - protected static findElementsBasedOnTextInContainer( + protected findElementsBasedOnTextInContainer( locator: TestingBehatElementLocator, topContainer: HTMLElement, options: TestingBehatFindOptions, @@ -465,7 +467,7 @@ export class TestingBehatDomUtils { * * @param element Element. */ - protected static async ensureElementVisible(element: HTMLElement): Promise { + protected async ensureElementVisible(element: HTMLElement): Promise { const initialRect = element.getBoundingClientRect(); element.scrollIntoView(false); @@ -494,7 +496,7 @@ export class TestingBehatDomUtils { * * @param element Element to press. */ - static async pressElement(element: HTMLElement): Promise { + async pressElement(element: HTMLElement): Promise { await NgZone.run(async () => { const promise = new CorePromisedValue(); @@ -539,7 +541,7 @@ export class TestingBehatDomUtils { * @param element HTML to set. * @param value Value to be set. */ - static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise { + async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise { await NgZone.run(async () => { const promise = new CorePromisedValue(); @@ -604,6 +606,8 @@ export class TestingBehatDomUtils { } +export const TestingBehatDomUtils = makeSingleton(TestingBehatDomUtilsService); + type ElementsWithExact = { element: HTMLElement; exact: boolean; diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 369683109..8492d09a7 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -18,8 +18,8 @@ import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreConfig } from '@services/config'; import { EnvironmentConfig } from '@/types/config'; -import { NgZone } from '@singletons'; -import { CoreNetwork } from '@services/network'; +import { makeSingleton, NgZone } from '@singletons'; +import { CoreNetwork, CoreNetworkService } from '@services/network'; import { CorePushNotifications, CorePushNotificationsNotificationBasicData, @@ -30,45 +30,34 @@ import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreDom } from '@singletons/dom'; import { IonRefresher } from '@ionic/angular'; import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dashboard'; +import { Injectable } from '@angular/core'; /** * Behat runtime servive with public API. */ -export class TestingBehatRuntime { +@Injectable({ providedIn: 'root' }) +export class TestingBehatRuntimeService { + + protected initialized = false; + + get network(): CoreNetworkService { + return CoreNetwork.instance; + } /** * Init behat functions and set options like skipping onboarding. * * @param options Options to set on the app. */ - static init(options?: TestingBehatInitOptions): void { - TestingBehatBlocking.init(); - - (window as BehatTestsWindow).behat = { - closePopup: TestsBehatRuntime.closePopup, - find: TestsBehatRuntime.find, - getAngularInstance: TestsBehatRuntime.getAngularInstance, - getHeader: TestsBehatRuntime.getHeader, - isSelected: TestsBehatRuntime.isSelected, - loadMoreItems: TestsBehatRuntime.loadMoreItems, - log: TestsBehatRuntime.log, - press: TestsBehatRuntime.press, - pressStandard: TestsBehatRuntime.pressStandard, - pullToRefresh: TestsBehatRuntime.pullToRefresh, - scrollTo: TestsBehatRuntime.scrollTo, - setField: TestsBehatRuntime.setField, - handleCustomURL: TestsBehatRuntime.handleCustomURL, - notificationClicked: TestsBehatRuntime.notificationClicked, - forceSyncExecution: TestsBehatRuntime.forceSyncExecution, - waitLoadingToFinish: TestsBehatRuntime.waitLoadingToFinish, - network: CoreNetwork.instance, - }; - - if (!options) { + init(options: TestingBehatInitOptions = {}): void { + if (this.initialized) { return; } - if (options.skipOnBoarding === true) { + this.initialized = true; + TestingBehatBlocking.init(); + + if (options.skipOnBoarding) { CoreConfig.set(CoreLoginHelperProvider.ONBOARDING_DONE, 1); } @@ -79,13 +68,22 @@ export class TestingBehatRuntime { } } + /** + * Check whether the service has been initialized or not. + * + * @returns Whether the service has been initialized or not. + */ + hasInitialized(): boolean { + return this.initialized; + } + /** * Handles a custom URL. * * @param url Url to open. * @return OK if successful, or ERROR: followed by message. */ - static async handleCustomURL(url: string): Promise { + async handleCustomURL(url: string): Promise { try { await NgZone.run(async () => { await CoreCustomURLSchemes.handleCustomURL(url); @@ -103,7 +101,7 @@ export class TestingBehatRuntime { * @param data Notification data. * @return Promise resolved when done. */ - static async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { + async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { const blockKey = TestingBehatBlocking.block(); try { @@ -121,7 +119,7 @@ export class TestingBehatRuntime { * * @return Promise resolved if all handlers are executed successfully, rejected otherwise. */ - static async forceSyncExecution(): Promise { + async forceSyncExecution(): Promise { await NgZone.run(async () => { await CoreCronDelegate.forceSyncExecution(); }); @@ -132,7 +130,7 @@ export class TestingBehatRuntime { * * @return Promise resolved when all components have been rendered. */ - static async waitLoadingToFinish(): Promise { + async waitLoadingToFinish(): Promise { await NgZone.run(async () => { const elements = Array.from(document.body.querySelectorAll('core-loading')) .filter((element) => CoreDom.isElementVisible(element)); @@ -148,7 +146,7 @@ export class TestingBehatRuntime { * @param button Type of button to press. * @return OK if successful, or ERROR: followed by message. */ - static async pressStandard(button: string): Promise { + async pressStandard(button: string): Promise { this.log('Action - Click standard button: ' + button); // Find button @@ -194,7 +192,7 @@ export class TestingBehatRuntime { * * @return OK if successful, or ERROR: followed by message */ - static closePopup(): string { + closePopup(): string { this.log('Action - Close popup'); let backdrops = Array.from(document.querySelectorAll('ion-backdrop')); @@ -222,7 +220,7 @@ export class TestingBehatRuntime { * @param options Search options. * @return OK if successful, or ERROR: followed by message */ - static find(locator: TestingBehatElementLocator, options: Partial = {}): string { + find(locator: TestingBehatElementLocator, options: Partial = {}): string { this.log('Action - Find', { locator, ...options }); try { @@ -250,7 +248,7 @@ export class TestingBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static scrollTo(locator: TestingBehatElementLocator): string { + scrollTo(locator: TestingBehatElementLocator): string { this.log('Action - scrollTo', { locator }); try { @@ -277,7 +275,7 @@ export class TestingBehatRuntime { * * @return OK if successful, or ERROR: followed by message */ - static async loadMoreItems(): Promise { + async loadMoreItems(): Promise { this.log('Action - loadMoreItems'); try { @@ -324,7 +322,7 @@ export class TestingBehatRuntime { * @param locator Element locator. * @return YES or NO if successful, or ERROR: followed by message */ - static isSelected(locator: TestingBehatElementLocator): string { + isSelected(locator: TestingBehatElementLocator): string { this.log('Action - Is Selected', locator); try { @@ -342,7 +340,7 @@ export class TestingBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static async press(locator: TestingBehatElementLocator): Promise { + async press(locator: TestingBehatElementLocator): Promise { this.log('Action - Press', locator); try { @@ -365,7 +363,7 @@ export class TestingBehatRuntime { * * @return OK if successful, or ERROR: followed by message */ - static async pullToRefresh(): Promise { + async pullToRefresh(): Promise { this.log('Action - pullToRefresh'); try { @@ -398,7 +396,7 @@ export class TestingBehatRuntime { * * @return OK: followed by header text if successful, or ERROR: followed by message. */ - static getHeader(): string { + getHeader(): string { this.log('Action - Get header'); let titles = Array.from(document.querySelectorAll('.ion-page:not(.ion-page-hidden) > ion-header h1')); @@ -424,7 +422,7 @@ export class TestingBehatRuntime { * @param value New value * @return OK or ERROR: followed by message */ - static async setField(field: string, value: string): Promise { + async setField(field: string, value: string): Promise { this.log('Action - Set field ' + field + ' to: ' + value); const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText( @@ -448,7 +446,7 @@ export class TestingBehatRuntime { * @param className Constructor class name * @return Component instance */ - static getAngularInstance(selector: string, className: string): T | null { + getAngularInstance(selector: string, className: string): T | null { this.log('Action - Get Angular instance ' + selector + ', ' + className); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -465,7 +463,7 @@ export class TestingBehatRuntime { * Logs information from this Behat runtime JavaScript, including the time and the 'BEHAT' * keyword so we can easily filter for it if needed. */ - static log(...args: unknown[]): void { + log(...args: unknown[]): void { const now = new Date(); const nowFormatted = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + @@ -477,14 +475,14 @@ export class TestingBehatRuntime { } +export const TestingBehatRuntime = makeSingleton(TestingBehatRuntimeService); + export type BehatTestsWindow = Window & { M?: { // eslint-disable-line @typescript-eslint/naming-convention util?: { pending_js?: string[]; // eslint-disable-line @typescript-eslint/naming-convention }; }; - behatInit?: () => void; - behat?: unknown; }; export type TestingBehatFindOptions = { diff --git a/src/testing/behat-testing.module.prod.ts b/src/testing/testing.module.prod.ts similarity index 95% rename from src/testing/behat-testing.module.prod.ts rename to src/testing/testing.module.prod.ts index a95d24c81..e319eb9e6 100644 --- a/src/testing/behat-testing.module.prod.ts +++ b/src/testing/testing.module.prod.ts @@ -18,4 +18,4 @@ import { NgModule } from '@angular/core'; * Stub used in production to avoid including testing code in production bundles. */ @NgModule({}) -export class BehatTestingModule {} +export class TestingModule {} diff --git a/src/core/initializers/prepare-automated-tests.ts b/src/testing/testing.module.ts similarity index 66% rename from src/core/initializers/prepare-automated-tests.ts rename to src/testing/testing.module.ts index ff773f9aa..9d78a5aa1 100644 --- a/src/core/initializers/prepare-automated-tests.ts +++ b/src/testing/testing.module.ts @@ -12,21 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { CoreAppProvider } from '@services/app'; -import { CoreDB, CoreDbProvider } from '@services/db'; +import { TestingBehatRuntime, TestingBehatRuntimeService } from './services/behat-runtime'; type AutomatedTestsWindow = Window & { - dbProvider?: CoreDbProvider; + behat?: TestingBehatRuntimeService; }; function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) { - window.dbProvider = CoreDB.instance; -} - -export default function(): void { if (!CoreAppProvider.isAutomated()) { return; } - initializeAutomatedTestsWindow(window); + window.behat = TestingBehatRuntime.instance; } + +@NgModule({ + providers: [ + { provide: APP_INITIALIZER, multi: true, useValue: () => initializeAutomatedTestsWindow(window) }, + ], +}) +export class TestingModule {} From 52259b421f12b5dcd49fc2978f080de17b10cead Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 30 Jun 2022 13:48:43 +0200 Subject: [PATCH 08/12] MOBILE-4110 behat: Clean up js calls --- .../tests/behat/behat_app.php | 56 ++++++++--------- .../tests/behat/behat_app_helper.php | 48 ++++++++++---- .../behat/classes/performance_measure.php | 2 +- src/testing/services/behat-runtime.ts | 63 +++++++------------ 4 files changed, 88 insertions(+), 81 deletions(-) diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local-moodleappbehat/tests/behat/behat_app.php index 05e89be8e..4b7bbbf70 100644 --- a/local-moodleappbehat/tests/behat/behat_app.php +++ b/local-moodleappbehat/tests/behat/behat_app.php @@ -96,7 +96,7 @@ class behat_app extends behat_app_helper { public function i_wait_the_app_to_restart() { // Wait window to reload. $this->spin(function() { - if ($this->js('window.behat.hasInitialized()')) { + if ($this->runtime_js('hasInitialized()')) { // Behat runtime shouldn't be initialized after reload. throw new DriverException('Window is not reloading properly.'); } @@ -114,18 +114,18 @@ class behat_app extends behat_app_helper { * @Then /^I should( not)? find (".+")( inside the .+)? in the app$/ * @param bool $not Whether assert that the element was not found * @param string $locator Element locator - * @param string $containerName Container name + * @param string $container Container name */ - public function i_find_in_the_app(bool $not, string $locator, string $containerName = '') { + public function i_find_in_the_app(bool $not, string $locator, string $container = '') { $locator = $this->parse_element_locator($locator); - if (!empty($containerName)) { - preg_match('/^ inside the (.+)$/', $containerName, $matches); - $containerName = $matches[1]; + if (!empty($container)) { + preg_match('/^ inside the (.+)$/', $container, $matches); + $container = $matches[1]; } - $containerName = json_encode($containerName); + $options = json_encode(['containerName' => $container]); - $this->spin(function() use ($not, $locator, $containerName) { - $result = $this->js("return window.behat.find($locator, { containerName: $containerName });"); + $this->spin(function() use ($not, $locator, $options) { + $result = $this->runtime_js("find($locator, $options)"); if ($not && $result === 'OK') { throw new DriverException('Error, found an element that should not be found'); @@ -151,7 +151,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->js("return window.behat.scrollTo($locator);"); + $result = $this->runtime_js("scrollTo($locator)"); if ($result !== 'OK') { throw new DriverException('Error finding element - ' . $result); @@ -174,7 +174,7 @@ class behat_app extends behat_app_helper { */ public function i_load_more_items_in_the_app(bool $not = false) { $this->spin(function() use ($not) { - $result = $this->js('return await window.behat.loadMoreItems();'); + $result = $this->runtime_js('loadMoreItems()'); if ($not && $result !== 'ERROR: All items are already loaded.') { throw new DriverException('It should not have been possible to load more items'); @@ -199,7 +199,7 @@ class behat_app extends behat_app_helper { public function i_swipe_in_the_app(string $direction) { $method = 'swipe' . ucwords($direction); - $this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); + $this->runtime_js("getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); $this->wait_for_pending_js(); @@ -218,7 +218,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator, $not) { - $result = $this->js("return window.behat.isSelected($locator);"); + $result = $this->runtime_js("isSelected($locator)"); switch ($result) { case 'YES': @@ -325,7 +325,7 @@ class behat_app extends behat_app_helper { $this->login($username); } - $mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); + $mycoursesfound = $this->runtime_js("find({ text: 'My courses', selector: 'ion-tab-button'})"); if ($mycoursesfound !== 'OK') { // My courses not present enter from Dashboard. @@ -381,7 +381,7 @@ class behat_app extends behat_app_helper { */ public function i_press_the_standard_button_in_the_app(string $button) { $this->spin(function() use ($button) { - $result = $this->js("return await window.behat.pressStandard('$button');"); + $result = $this->runtime_js("pressStandard('$button')"); if ($result !== 'OK') { throw new DriverException('Error pressing standard button - ' . $result); @@ -419,7 +419,7 @@ class behat_app extends behat_app_helper { ], ]); - $this->js("window.behat.notificationClicked($notification)"); + $this->zone_js("pushNotifications.notificationClicked($notification)", true); $this->wait_for_pending_js(); } @@ -507,7 +507,7 @@ class behat_app extends behat_app_helper { */ public function i_close_the_popup_in_the_app() { $this->spin(function() { - $result = $this->js("return window.behat.closePopup();"); + $result = $this->runtime_js('closePopup()'); if ($result !== 'OK') { throw new DriverException('Error closing popup - ' . $result); @@ -545,7 +545,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->js("return await window.behat.press($locator);"); + $result = $this->runtime_js("press($locator)"); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -588,7 +588,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($not, $locator) { - $result = $this->js("return window.behat.find($locator, { onlyClickable: true });"); + $result = $this->runtime_js("find($locator, { onlyClickable: true })"); if ($not && $result === 'OK') { throw new DriverException('Error, found a clickable element that should not be found'); @@ -622,14 +622,14 @@ class behat_app extends behat_app_helper { $this->spin(function() use ($selectedtext, $selected, $locator) { // Don't do anything if the item is already in the expected state. - $result = $this->js("return window.behat.isSelected($locator);"); + $result = $this->runtime_js("isSelected($locator)"); if ($result === $selected) { return true; } // Press element. - $result = $this->js("return await window.behat.press($locator);"); + $result = $this->runtime_js("press($locator)"); if ($result !== 'OK') { throw new DriverException('Error pressing element - ' . $result); @@ -638,7 +638,7 @@ class behat_app extends behat_app_helper { // Check that it worked as expected. $this->wait_for_pending_js(); - $result = $this->js("return window.behat.isSelected($locator);"); + $result = $this->runtime_js("isSelected($locator)"); switch ($result) { case 'YES': @@ -672,7 +672,7 @@ class behat_app extends behat_app_helper { $value = addslashes_js($value); $this->spin(function() use ($field, $value) { - $result = $this->js("return await window.behat.setField(\"$field\", \"$value\");"); + $result = $this->runtime_js("setField('$field', '$value')"); if ($result !== 'OK') { throw new DriverException('Error setting field - ' . $result); @@ -711,7 +711,7 @@ class behat_app extends behat_app_helper { */ public function the_header_should_be_in_the_app(string $text) { $this->spin(function() use ($text) { - $result = $this->js('return window.behat.getHeader();'); + $result = $this->runtime_js('getHeader()'); if (substr($result, 0, 3) !== 'OK:') { throw new DriverException('Error getting header - ' . $result); @@ -792,7 +792,7 @@ class behat_app extends behat_app_helper { * @When I run cron tasks in the app */ public function i_run_cron_tasks_in_the_app() { - $this->js('await window.behat.forceSyncExecution()'); + $this->zone_js('cronDelegate.forceSyncExecution()'); $this->wait_for_pending_js(); } @@ -802,7 +802,7 @@ class behat_app extends behat_app_helper { * @When I wait loading to finish in the app */ public function i_wait_loading_to_finish_in_the_app() { - $this->js('await window.behat.waitLoadingToFinish()'); + $this->runtime_js('waitLoadingToFinish()'); $this->wait_for_pending_js(); } @@ -824,7 +824,7 @@ class behat_app extends behat_app_helper { $this->getSession()->switchToWindow($names[1]); } - $this->js('window.close();'); + $this->js('window.close()'); $this->getSession()->switchToWindow($names[0]); } @@ -836,7 +836,7 @@ class behat_app extends behat_app_helper { * @throws DriverException If the navigator.online mode is not available */ public function i_switch_offline_mode(string $offline) { - $this->js("window.behat.network.setForceOffline($offline);"); + $this->runtime_js("network.setForceOffline($offline)"); } } diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local-moodleappbehat/tests/behat/behat_app_helper.php index 646b6e141..a9e469535 100644 --- a/local-moodleappbehat/tests/behat/behat_app_helper.php +++ b/local-moodleappbehat/tests/behat/behat_app_helper.php @@ -313,12 +313,12 @@ class behat_app_helper extends behat_base { try { // Init Behat JavaScript runtime. + $initoptions = json_encode([ + 'skipOnBoarding' => $options['skiponboarding'] ?? true, + 'configOverrides' => $this->appconfig, + ]); - $initOptions = new StdClass(); - $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; - $initOptions->configOverrides = $this->appconfig; - - $this->js('window.behat.init(' . json_encode($initOptions) . ');'); + $this->runtime_js("init($initoptions)"); } catch (Exception $error) { throw new DriverException('Moodle App not running or not running on Automated mode.'); } @@ -456,7 +456,7 @@ class behat_app_helper extends behat_base { $res = $this->evaluate_script("Promise.resolve($script) .then(result => window.$promisevariable = result) - .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);"); + .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message)"); do { if (microtime(true) - $start > $timeout) { @@ -465,15 +465,42 @@ class behat_app_helper extends behat_base { // 0.1 seconds. usleep(100000); - } while (!$this->evaluate_script("return '$promisevariable' in window;")); + } while (!$this->evaluate_script("'$promisevariable' in window")); - $result = $this->evaluate_script("return window.$promisevariable;"); + $result = $this->evaluate_script("window.$promisevariable"); - $this->evaluate_script("delete window.$promisevariable;"); + $this->evaluate_script("delete window.$promisevariable"); + + if (is_string($result) && strrpos($result, 'Async code rejected:') === 0) { + throw new DriverException($result); + } return $result; } + /** + * Evaluate and execute methods from the Behat runtime. + * + * @param string $script + * @return mixed Result. + */ + protected function runtime_js(string $script) { + return $this->js("window.behat.$script"); + } + + /** + * Evaluate and execute methods from the Behat runtime inside the Angular zone. + * + * @param string $script + * @param bool $blocking + * @return mixed Result. + */ + protected function zone_js(string $script, bool $blocking = false) { + $blockingjson = json_encode($blocking); + + return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)"); + } + /** * Opens a custom URL for automatic login and redirect from the Moodle App (and waits to finish.) * @@ -548,8 +575,7 @@ class behat_app_helper extends behat_base { * @param string $successXPath The XPath of the element to lookat after navigation. */ protected function handle_url(string $customurl, string $successXPath = '') { - // Instead of using evaluate_async_script, we wait for the path to load. - $result = $this->js("return await window.behat.handleCustomURL('$customurl');"); + $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); if ($result !== 'OK') { throw new DriverException('Error handling url - ' . $result); diff --git a/local-moodleappbehat/tests/behat/classes/performance_measure.php b/local-moodleappbehat/tests/behat/classes/performance_measure.php index f56125ae5..73e37287c 100644 --- a/local-moodleappbehat/tests/behat/classes/performance_measure.php +++ b/local-moodleappbehat/tests/behat/classes/performance_measure.php @@ -178,7 +178,7 @@ class performance_measure implements behat_app_listener { * @return int Current time in milliseconds. */ private function now(): int { - return $this->driver->evaluateScript('Date.now();'); + return $this->driver->evaluateScript('Date.now()'); } /** diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 8492d09a7..d8448e035 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -14,17 +14,14 @@ import { TestingBehatDomUtils } from './behat-dom'; import { TestingBehatBlocking } from './behat-blocking'; -import { CoreCustomURLSchemes } from '@services/urlschemes'; +import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreConfig } from '@services/config'; import { EnvironmentConfig } from '@/types/config'; import { makeSingleton, NgZone } from '@singletons'; import { CoreNetwork, CoreNetworkService } from '@services/network'; -import { - CorePushNotifications, - CorePushNotificationsNotificationBasicData, -} from '@features/pushnotifications/services/pushnotifications'; -import { CoreCronDelegate } from '@services/cron'; +import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; +import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreDom } from '@singletons/dom'; @@ -40,10 +37,22 @@ export class TestingBehatRuntimeService { protected initialized = false; + get cronDelegate(): CoreCronDelegateService { + return CoreCronDelegate.instance; + } + + get customUrlSchemes(): CoreCustomURLSchemesProvider { + return CoreCustomURLSchemes.instance; + } + get network(): CoreNetworkService { return CoreNetwork.instance; } + get pushNotifications(): CorePushNotificationsProvider { + return CorePushNotifications.instance; + } + /** * Init behat functions and set options like skipping onboarding. * @@ -78,53 +87,25 @@ export class TestingBehatRuntimeService { } /** - * Handles a custom URL. + * Run an operation inside the angular zone and return result. * - * @param url Url to open. + * @param operation Operation callback. * @return OK if successful, or ERROR: followed by message. */ - async handleCustomURL(url: string): Promise { + async runInZone(operation: () => unknown, blocking: boolean = false): Promise { + const blockKey = blocking && TestingBehatBlocking.block(); + try { - await NgZone.run(async () => { - await CoreCustomURLSchemes.handleCustomURL(url); - }); + await NgZone.run(operation); return 'OK'; } catch (error) { return 'ERROR: ' + error.message; - } - } - - /** - * Function called when a push notification is clicked. Redirect the user to the right state. - * - * @param data Notification data. - * @return Promise resolved when done. - */ - async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { - const blockKey = TestingBehatBlocking.block(); - - try { - await NgZone.run(async () => { - await CorePushNotifications.notificationClicked(data); - }); } finally { - TestingBehatBlocking.unblock(blockKey); + blockKey && TestingBehatBlocking.unblock(blockKey); } } - /** - * Force execution of synchronization cron tasks without waiting for the scheduled time. - * Please notice that some tasks may not be executed depending on the network connection and sync settings. - * - * @return Promise resolved if all handlers are executed successfully, rejected otherwise. - */ - async forceSyncExecution(): Promise { - await NgZone.run(async () => { - await CoreCronDelegate.forceSyncExecution(); - }); - } - /** * Wait all controlled components to be rendered. * From 185b9fc9c68d7b69bf0781ef1b8b86dac173a30b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 5 Jul 2022 09:22:32 +0200 Subject: [PATCH 09/12] MOBILE-4110 behat: Fix custom url generation --- local-moodleappbehat/tests/behat/behat_app_helper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local-moodleappbehat/tests/behat/behat_app_helper.php index a9e469535..6b9fa3a9e 100644 --- a/local-moodleappbehat/tests/behat/behat_app_helper.php +++ b/local-moodleappbehat/tests/behat/behat_app_helper.php @@ -540,9 +540,10 @@ class behat_app_helper extends behat_base { // Generate custom URL. $parsed_url = parse_url($CFG->behat_wwwroot); - $domain = $parsed_url['host'] ?? ''; - $rootpath = $parsed_url['path'] ?? ''; - $url = $this->get_mobile_url_scheme() . "://$username@$domain$rootpath?token=$token&privatetoken=$privatetoken"; + $site = $parsed_url['host'] ?? ''; + $site .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $site .= $parsed_url['path'] ?? ''; + $url = $this->get_mobile_url_scheme() . "://$username@$site?token=$token&privatetoken=$privatetoken"; if (!empty($path)) { $url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path); From 7de3698e7e625f2a2c8648e2b787d6531114bfbd Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 5 Jul 2022 09:35:17 +0200 Subject: [PATCH 10/12] MOBILE-4110 ci: Use test build --- .github/workflows/acceptance.yml | 4 ++-- .github/workflows/performance.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 2f45858cd..2972bbf1d 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -55,9 +55,9 @@ jobs: $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db - - name: Compile & launch production app with Docker + - name: Compile & launch app with Docker run: | - docker build -t moodlehq/moodleapp:behat . + docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:behat . docker run -d --rm --name moodleapp moodlehq/moodleapp:behat docker network connect moodle-docker_default moodleapp --alias moodleapp - name: Init Behat diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 1f9321dbe..b4fe6a50c 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -40,9 +40,9 @@ jobs: $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db - - name: Compile & launch production app with Docker + - name: Compile & launch app with Docker run: | - docker build -t moodlehq/moodleapp:performance . + docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:performance . docker run -d --rm --name moodleapp moodlehq/moodleapp:performance docker network connect moodle-docker_default moodleapp --alias moodleapp - name: Init Behat From 30633664a185a8a03b67fed363221d33f04f5e3a Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 5 Jul 2022 16:07:15 +0200 Subject: [PATCH 11/12] MOBILE-4110 behat: Configure auto-generated plugin --- .github/scripts/functions.sh | 21 ++ .github/scripts/update_behat_plugin.sh | 68 ++++++ .github/workflows/update_behat_plugin.yml | 18 ++ package-lock.json | 255 +++++++++++++++++++++- package.json | 1 + scripts/build-behat-plugin.js | 63 ++++-- 6 files changed, 395 insertions(+), 31 deletions(-) create mode 100755 .github/scripts/update_behat_plugin.sh create mode 100644 .github/workflows/update_behat_plugin.yml diff --git a/.github/scripts/functions.sh b/.github/scripts/functions.sh index f36fba1fc..33ee4febb 100644 --- a/.github/scripts/functions.sh +++ b/.github/scripts/functions.sh @@ -63,3 +63,24 @@ function notify_on_error_exit { exit 1 fi } + +function get_behat_plugin_changes_diff { + i=0 + previoushash="" + currenthash=`git rev-parse HEAD` + initialhash=`git rev-list HEAD | tail -n 1` + totalcommits=`git log --oneline | wc -l` + repositoryname=`echo $GITHUB_REPOSITORY | sed "s/\\//\\\\\\\\\\//"` + + ((totalcommits--)) + while [ $i -lt $totalcommits ] && [[ -z $previoushash ]]; do + previoushash=`git rev-list --format=%B --max-count=1 HEAD~$i | grep -o "https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.[^.]\+" | sed "s/https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.//"` + ((i++)) + done + + if [[ -z $previoushash ]]; then + previoushash=$initialhash + fi + + echo "$previoushash...$currenthash" +} diff --git a/.github/scripts/update_behat_plugin.sh b/.github/scripts/update_behat_plugin.sh new file mode 100755 index 000000000..5419d831a --- /dev/null +++ b/.github/scripts/update_behat_plugin.sh @@ -0,0 +1,68 @@ +#!/bin/bash +source "./.github/scripts/functions.sh" + +if [ -z $GIT_TOKEN ] || [ -z $BEHAT_PLUGIN_GITHUB_REPOSITORY ] || [ -z $BEHAT_PLUGIN_BRANCH ]; then + print_error "Env vars not correctly defined" + exit 1 +fi + +if [[ $BEHAT_PLUGIN_BRANCH != $GITHUB_REF_NAME ]]; then + echo "Script disabled for this branch" + exit 0 +fi + +# Clone plugin repository. +print_title "Cloning Behat plugin repository..." + +git clone https://$GIT_TOKEN@github.com/$BEHAT_PLUGIN_GITHUB_REPOSITORY.git tmp/local_moodleappbehat -b integration +pluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+") + +# Auto-generate plugin. +print_title "Building Behat plugin..." + +if [ -z $BEHAT_PLUGIN_EXCLUDE_FEATURES ]; then + scripts/build-behat-plugin.js tmp/local_moodleappbehat +else + scripts/build-behat-plugin.js tmp/local_moodleappbehat --exclude-features +fi +notify_on_error_exit "Unsuccessful build, stopping..." + +# Check if there are any changes (ignoring plugin version). +print_title "Checking changes..." + +newpluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+") +sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $pluginversion\;/ tmp/local_moodleappbehat/version.php + +if [[ -z `git -C tmp/local_moodleappbehat/ status --short` ]]; then + echo "There weren't any changes to apply!" + exit +fi + +if [[ $pluginversion -eq $newpluginversion ]]; then + ((newpluginversion++)) +fi + +sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $newpluginversion\;/ tmp/local_moodleappbehat/version.php + +# Apply new changes +print_title "Applying changes to repository..." + +cd tmp/local_moodleappbehat + +diff=`get_behat_plugin_changes_diff` + +# Set up Github Actions bot user +# See https://github.community/t/github-actions-bot-email-address/17204/6 +git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" +git config --local user.name "github-actions[bot]" +git add . +git commit -m "[auto-generated] Update plugin files +Check out the commits that caused these changes: https://github.com/$GITHUB_REPOSITORY/compare/$diff +" +notify_on_error_exit "Unsuccessful commit, stopping..." + +echo "Pushing changes..." +git push +notify_on_error_exit "Unsuccessful push, stopping..." + +echo "Behat plugin updated!" diff --git a/.github/workflows/update_behat_plugin.yml b/.github/workflows/update_behat_plugin.yml new file mode 100644 index 000000000..1f6430a6a --- /dev/null +++ b/.github/workflows/update_behat_plugin.yml @@ -0,0 +1,18 @@ +name: Update Behat plugin + +on: ['push'] + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + - name: Install npm packages + run: npm ci --no-audit + - name: Update Behat plugin + env: + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} + BEHAT_PLUGIN_GITHUB_REPOSITORY: ${{ secrets.BEHAT_PLUGIN_GITHUB_REPOSITORY }} + BEHAT_PLUGIN_BRANCH: ${{ secrets.BEHAT_PLUGIN_BRANCH }} + run: ./.github/scripts/update_behat_plugin.sh diff --git a/package-lock.json b/package-lock.json index 58bff4929..635045583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -229,12 +229,31 @@ "uri-js": "^4.2.2" } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "core-js": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", "dev": true }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "open": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", @@ -3889,6 +3908,16 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -3913,6 +3942,15 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11772,12 +11810,12 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { @@ -12327,6 +12365,16 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -12447,6 +12495,17 @@ "table": "^5.2.3", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "eslint-scope": { @@ -13433,7 +13492,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "concat-stream": { "version": "1.6.2", @@ -16449,6 +16508,16 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -16493,6 +16562,15 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16625,6 +16703,16 @@ "tsconfig-paths": "^3.9.0" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "doctrine": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", @@ -16634,6 +16722,15 @@ "esutils": "^2.0.2", "isarray": "^1.0.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -17635,6 +17732,16 @@ "color-convert": "^1.9.0" } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", @@ -17766,6 +17873,15 @@ "to-regex": "^3.0.2" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -18326,6 +18442,25 @@ "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "glob-base": { @@ -19962,6 +20097,25 @@ "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "requires": { "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "image-size": { @@ -20959,6 +21113,16 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -20968,6 +21132,15 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -23349,11 +23522,12 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "minimist": { @@ -25138,6 +25312,16 @@ "which": "^1.3.1" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "cacache": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", @@ -25200,6 +25384,15 @@ "yallist": "^3.0.2" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -28127,6 +28320,27 @@ "dev": true, "requires": { "minimatch": "3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "reflect-metadata": { @@ -31856,6 +32070,27 @@ "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "text-table": { diff --git a/package.json b/package.json index 31cd28163..ea93e1d63 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,7 @@ "jest": "^26.5.2", "jest-preset-angular": "^8.3.1", "jsonc-parser": "^2.3.1", + "minimatch": "^5.1.0", "native-run": "^1.4.0", "patch-package": "^6.4.7", "terser-webpack-plugin": "^4.2.3", diff --git a/scripts/build-behat-plugin.js b/scripts/build-behat-plugin.js index 1d0570d7b..dad19ff1a 100755 --- a/scripts/build-behat-plugin.js +++ b/scripts/build-behat-plugin.js @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +const minimatch = require('minimatch'); const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs'); const { readdir } = require('fs').promises; const { mkdirSync, copySync } = require('fs-extra'); @@ -21,12 +22,22 @@ const { resolve, extname, dirname, basename, relative } = require('path'); async function main() { const pluginPath = process.argv[2] || guessPluginPath() || fail('Folder argument missing!'); + const excludeFeatures = process.argv.some(arg => arg === '--exclude-features'); + const exclusions = excludeFeatures + ? [ + '*.feature', + '**/js/mobile/index.js', + '**/db/mobile.php', + '**/classes/output/mobile.php', + ] + : []; if (!existsSync(pluginPath)) { mkdirSync(pluginPath); } else { // Empty directory, except the excluding list. const excludeFromErase = [ + ...exclusions, '.git', '.gitignore', 'README.md', @@ -34,7 +45,7 @@ async function main() { const files = await readdir(pluginPath, { withFileTypes: true }); for (const file of files) { - if (excludeFromErase.indexOf(file.name) >= 0) { + if (isExcluded(file.name, excludeFromErase)) { continue; } @@ -43,13 +54,17 @@ async function main() { } } - // Copy plugin template. const { version: appVersion } = require(projectPath('package.json')); const templatePath = projectPath('local-moodleappbehat'); + for await (const file of getDirectoryFiles(templatePath)) { + if (isExcluded(file, exclusions)) { + continue; + } - copySync(templatePath, pluginPath); + copySync(file, file.replace(templatePath, pluginPath)); + } // Update version.php const pluginFilePath = pluginPath + '/version.php'; @@ -62,28 +77,30 @@ async function main() { writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); // Copy feature files. - const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; - copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory }); + if (!excludeFeatures) { + const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; + copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory }); - const behatFeaturesPath = `${pluginPath}/tests/behat`; - if (!existsSync(behatFeaturesPath)) { - mkdirSync(behatFeaturesPath, {recursive: true}); - } - - for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) { - const featurePath = dirname(featureFile); - if (!featurePath.endsWith('/tests/behat')) { - continue; + const behatFeaturesPath = `${pluginPath}/tests/behat`; + if (!existsSync(behatFeaturesPath)) { + mkdirSync(behatFeaturesPath, {recursive: true}); } - const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length)); - const searchRegExp = new RegExp('/', 'g'); - const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core'; - const featureFilename = prefix + '-' + basename(featureFile); - renameSync(featureFile, behatFeaturesPath + '/' + featureFilename); - } + for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) { + const featurePath = dirname(featureFile); + if (!featurePath.endsWith('/tests/behat')) { + continue; + } - rmSync(behatTempFeaturesPath, {recursive: true}); + const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length)); + const searchRegExp = new RegExp('/', 'g'); + const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core'; + const featureFilename = prefix + '-' + basename(featureFile); + renameSync(featureFile, behatFeaturesPath + '/' + featureFilename); + } + + rmSync(behatTempFeaturesPath, {recursive: true}); + } } function isFeatureFileOrDirectory(src) { @@ -92,6 +109,10 @@ function isFeatureFileOrDirectory(src) { return stats.isDirectory() || extname(src) === '.feature'; } +function isExcluded(file, exclusions) { + return exclusions.some(exclusion => minimatch(file, exclusion)); +} + function fail(message) { console.error(message); process.exit(1); From 474e6df3504e519585d576338b4d2060aef8328b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 5 Jul 2022 17:50:16 +0200 Subject: [PATCH 12/12] MOBILE-4110 behat: Rename plugin to frankenstyle --- gulpfile.js | 2 +- .../classes/output/mobile.php | 0 .../classes/privacy/provider.php | 0 {local-moodleappbehat => local_moodleappbehat}/db/mobile.php | 0 .../js/mobile/index.js | 0 .../lang/en/local_moodleappbehat.php | 0 .../tests/behat/behat_app.php | 0 .../tests/behat/behat_app_helper.php | 0 .../tests/behat/behat_performance.php | 0 .../tests/behat/classes/performance_measure.php | 0 {local-moodleappbehat => local_moodleappbehat}/version.php | 0 scripts/build-behat-plugin.js | 2 +- 12 files changed, 2 insertions(+), 2 deletions(-) rename {local-moodleappbehat => local_moodleappbehat}/classes/output/mobile.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/classes/privacy/provider.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/db/mobile.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/js/mobile/index.js (100%) rename {local-moodleappbehat => local_moodleappbehat}/lang/en/local_moodleappbehat.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/tests/behat/behat_app.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/tests/behat/behat_app_helper.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/tests/behat/behat_performance.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/tests/behat/classes/performance_measure.php (100%) rename {local-moodleappbehat => local_moodleappbehat}/version.php (100%) diff --git a/gulpfile.js b/gulpfile.js index 7c7c5bb96..60451192f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -71,5 +71,5 @@ gulp.task('watch', () => { }); gulp.task('watch-behat', () => { - gulp.watch(['./src/**/*.feature', './local-moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); + gulp.watch(['./src/**/*.feature', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); }); diff --git a/local-moodleappbehat/classes/output/mobile.php b/local_moodleappbehat/classes/output/mobile.php similarity index 100% rename from local-moodleappbehat/classes/output/mobile.php rename to local_moodleappbehat/classes/output/mobile.php diff --git a/local-moodleappbehat/classes/privacy/provider.php b/local_moodleappbehat/classes/privacy/provider.php similarity index 100% rename from local-moodleappbehat/classes/privacy/provider.php rename to local_moodleappbehat/classes/privacy/provider.php diff --git a/local-moodleappbehat/db/mobile.php b/local_moodleappbehat/db/mobile.php similarity index 100% rename from local-moodleappbehat/db/mobile.php rename to local_moodleappbehat/db/mobile.php diff --git a/local-moodleappbehat/js/mobile/index.js b/local_moodleappbehat/js/mobile/index.js similarity index 100% rename from local-moodleappbehat/js/mobile/index.js rename to local_moodleappbehat/js/mobile/index.js diff --git a/local-moodleappbehat/lang/en/local_moodleappbehat.php b/local_moodleappbehat/lang/en/local_moodleappbehat.php similarity index 100% rename from local-moodleappbehat/lang/en/local_moodleappbehat.php rename to local_moodleappbehat/lang/en/local_moodleappbehat.php diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php similarity index 100% rename from local-moodleappbehat/tests/behat/behat_app.php rename to local_moodleappbehat/tests/behat/behat_app.php diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local_moodleappbehat/tests/behat/behat_app_helper.php similarity index 100% rename from local-moodleappbehat/tests/behat/behat_app_helper.php rename to local_moodleappbehat/tests/behat/behat_app_helper.php diff --git a/local-moodleappbehat/tests/behat/behat_performance.php b/local_moodleappbehat/tests/behat/behat_performance.php similarity index 100% rename from local-moodleappbehat/tests/behat/behat_performance.php rename to local_moodleappbehat/tests/behat/behat_performance.php diff --git a/local-moodleappbehat/tests/behat/classes/performance_measure.php b/local_moodleappbehat/tests/behat/classes/performance_measure.php similarity index 100% rename from local-moodleappbehat/tests/behat/classes/performance_measure.php rename to local_moodleappbehat/tests/behat/classes/performance_measure.php diff --git a/local-moodleappbehat/version.php b/local_moodleappbehat/version.php similarity index 100% rename from local-moodleappbehat/version.php rename to local_moodleappbehat/version.php diff --git a/scripts/build-behat-plugin.js b/scripts/build-behat-plugin.js index dad19ff1a..e5a7aebfb 100755 --- a/scripts/build-behat-plugin.js +++ b/scripts/build-behat-plugin.js @@ -56,7 +56,7 @@ async function main() { // Copy plugin template. const { version: appVersion } = require(projectPath('package.json')); - const templatePath = projectPath('local-moodleappbehat'); + const templatePath = projectPath('local_moodleappbehat'); for await (const file of getDirectoryFiles(templatePath)) { if (isExcluded(file, exclusions)) {