From c8b16035fea3fda2d9a4c22e07ced9fd2cc573d2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 30 Jun 2022 13:06:07 +0200 Subject: [PATCH] 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 {}