MOBILE-4110 behat: Clean up services

main
Noel De Martin 2022-06-30 13:06:07 +02:00
parent b0ccc7bf34
commit c8b16035fe
10 changed files with 90 additions and 123 deletions

View File

@ -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

View File

@ -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.');
}

View File

@ -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.');
}

View File

@ -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<string, number> = {};
protected lastInAppUrl?: string;
constructor(changeDetector: ChangeDetectorRef) {
if (CoreAppProvider.isAutomated()) {
(window as AutomatedTestsWindow).changeDetector = changeDetector;
}
}
/**
* Component being initialized.
*

View File

@ -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 },

View File

@ -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 {}

View File

@ -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<HTMLElement>([
'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<DOMRect> {
protected async ensureElementVisible(element: HTMLElement): Promise<DOMRect> {
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<void> {
async pressElement(element: HTMLElement): Promise<void> {
await NgZone.run(async () => {
const promise = new CorePromisedValue<void>();
@ -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<void> {
async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
await NgZone.run(async () => {
const promise = new CorePromisedValue<void>();
@ -604,6 +606,8 @@ export class TestingBehatDomUtils {
}
export const TestingBehatDomUtils = makeSingleton(TestingBehatDomUtilsService);
type ElementsWithExact = {
element: HTMLElement;
exact: boolean;

View File

@ -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<string> {
async handleCustomURL(url: string): Promise<string> {
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<void> {
async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise<void> {
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<void> {
async forceSyncExecution(): Promise<void> {
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<void> {
async waitLoadingToFinish(): Promise<void> {
await NgZone.run(async () => {
const elements = Array.from(document.body.querySelectorAll<HTMLElement>('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<string> {
async pressStandard(button: string): Promise<string> {
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<TestingBehatFindOptions> = {}): string {
find(locator: TestingBehatElementLocator, options: Partial<TestingBehatFindOptions> = {}): 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<string> {
async loadMoreItems(): Promise<string> {
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<string> {
async press(locator: TestingBehatElementLocator): Promise<string> {
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<string> {
async pullToRefresh(): Promise<string> {
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<HTMLElement>('.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<string> {
async setField(field: string, value: string): Promise<string> {
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<T = unknown>(selector: string, className: string): T | null {
getAngularInstance<T = unknown>(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 = {

View File

@ -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 {}

View File

@ -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 {}