Merge pull request #3877 from NoelDeMartin/MOBILE-3947

MOBILE-3947: Fix application startup and behat gestures
main
Dani Palou 2023-12-11 08:05:11 +01:00 committed by GitHub
commit 3b1f1fb638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 114 additions and 64 deletions

View File

@ -16,7 +16,7 @@ on:
required: true
default: 'https://github.com/moodle/moodle'
pull_request:
branches: [ main, v*.x ]
branches: [ main, ionic7, v*.x ]
jobs:
behat:
@ -24,7 +24,7 @@ jobs:
env:
MOODLE_DOCKER_DB: pgsql
MOODLE_DOCKER_BROWSER: chrome
MOODLE_DOCKER_PHP_VERSION: '8.0'
MOODLE_DOCKER_PHP_VERSION: '8.1'
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' }}

View File

@ -214,10 +214,7 @@ class behat_app extends behat_app_helper {
return true;
});
$this->wait_for_pending_js();
// Wait scroll animation to finish.
$this->getSession()->wait(300);
$this->wait_animations_done();
}
/**
@ -263,10 +260,7 @@ class behat_app extends behat_app_helper {
throw new DriverException('Error when swiping - ' . $result);
}
$this->wait_for_pending_js();
// Wait swipe animation to finish.
$this->getSession()->wait(300);
$this->wait_animations_done();
}
/**
@ -689,10 +683,7 @@ class behat_app extends behat_app_helper {
return true;
});
$this->wait_for_pending_js();
// Wait for UI to settle after refreshing.
$this->getSession()->wait(300);
$this->wait_animations_done();
if (is_null($locator)) {
return;

View File

@ -641,4 +641,15 @@ EOF;
return $text;
}
}
/**
* Wait until animations have finished.
*/
protected function wait_animations_done() {
$this->wait_for_pending_js();
// Ideally, we wouldn't wait a fixed amount of time. But it is not straightforward to wait for animations
// to finish, so for now we'll just wait 300ms.
usleep(300000);
}
}

View File

@ -27,6 +27,8 @@ import {
import { CoreArray } from '@singletons/array';
const modulesRoutes: WeakMap<InjectionToken<unknown>, ModuleRoutes> = new WeakMap();
/**
* Build app routes.
*
@ -175,6 +177,10 @@ export function conditionalRoutes(routes: Routes, condition: () => boolean): Rou
* @returns Routes.
*/
export function resolveModuleRoutes(injector: Injector, token: InjectionToken<ModuleRoutesConfig[]>): ModuleRoutes {
if (modulesRoutes.has(token)) {
return modulesRoutes.get(token) as ModuleRoutes;
}
const configs = injector.get(token, []);
const routes = configs.map(config => {
if (Array.isArray(config)) {
@ -190,10 +196,14 @@ export function resolveModuleRoutes(injector: Injector, token: InjectionToken<Mo
};
});
return {
const moduleRoutes = {
children: CoreArray.flatten(routes.map(r => r.children)),
siblings: CoreArray.flatten(routes.map(r => r.siblings)),
};
modulesRoutes.set(token, moduleRoutes);
return moduleRoutes;
}
export const APP_ROUTES = new InjectionToken('APP_ROUTES');

View File

@ -1,5 +1,4 @@
<swiper-container #swiperRef *ngIf="loaded" (slidechangetransitionstart)="slideWillChange()" (slidechangetransitionend)="slideDidChange()"
[initialSlide]="options.initialSlide" [runCallbacksOnInit]="options.runCallbacksOnInit">
<swiper-container #swiperRef *ngIf="loaded">
<swiper-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}" />
</swiper-slide>

View File

@ -50,6 +50,13 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
if (swiperRef?.nativeElement?.swiper) {
this.swiper = swiperRef.nativeElement.swiper as Swiper;
if (this.options.initialSlide) {
this.swiper.slideTo(this.options.initialSlide, 0, this.options.runCallbacksOnInit);
}
this.swiper.on('slideChangeTransitionStart', () => this.slideWillChange());
this.swiper.on('slideChangeTransitionEnd', () => this.slideDidChange());
Object.keys(this.options).forEach((key) => {
if (this.swiper) {
this.swiper.params[key] = this.options[key];

View File

@ -14,8 +14,15 @@
import envJson from '@/assets/env.json';
import { EnvironmentConfig } from '@/types/config';
import { InjectionToken } from '@angular/core';
import { CoreBrowser } from '@singletons/browser';
/**
* Injection token used for dependencies marked as optional that will never
* be resolved by Angular injectors.
*/
export const NULL_INJECTION_TOKEN = new InjectionToken('null');
/**
* Context levels enumeration.
*/

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreConstants } from '@/core/constants';
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreSwipeNavigationTourComponent } from '@components/swipe-navigation-tour/swipe-navigation-tour';
@ -42,6 +43,11 @@ export class CoreSwipeNavigationDirective implements AfterViewInit, OnDestroy {
constructor(el: ElementRef) {
this.element = el.nativeElement;
if (CoreConstants.enableDevTools()) {
this.element['swipeNavigation'] = this;
this.element.classList.add('uses-swipe-navigation');
}
}
get enabled(): boolean {

View File

@ -36,10 +36,6 @@ function buildRoutes(injector: Injector): Routes {
path: '',
component: CoreMainMenuPage,
children: [
{
path: '',
pathMatch: 'full',
},
{
path: CoreMainMenuHomeHandlerService.PAGE_NAME,
loadChildren: () => import('./mainmenu-home-lazy.module').then(m => m.CoreMainMenuHomeLazyModule),

View File

@ -18,24 +18,64 @@ import { Route, Routes } from '@angular/router';
import { ModuleRoutesConfig, resolveModuleRoutes } from '@/app/app-routing.module';
const MAIN_MENU_TAB_ROUTES = new InjectionToken('MAIN_MENU_TAB_ROUTES');
const modulesPaths: Record<string, Set<string>> = {};
/**
* Get the name of the module the injector belongs to.
*
* @param injector Injector.
* @returns Injector module name.
*/
function getInjectorModule(injector: Injector): string | null {
if (!('source' in injector) || typeof injector.source !== 'string') {
return null;
}
// Get module name from R3Injector source.
// See https://github.com/angular/angular/blob/16.2.0/packages/core/src/di/r3_injector.ts#L161C8
return injector.source;
}
/**
* Get module paths.
*
* @param injector Injector.
* @returns Module paths.
*/
function getModulePaths(injector: Injector): Set<string> | null {
const module = getInjectorModule(injector);
if (!module) {
return null;
}
return modulesPaths[module] ??= new Set();
}
/**
* Build module routes.
*
* @param injector Injector.
* @param mainRoute Main route.
* @returns Routes.
*/
export function buildTabMainRoutes(injector: Injector, mainRoute: Route): Routes {
const path = mainRoute.path ?? '';
const modulePaths = getModulePaths(injector);
const isRootRoute = modulePaths && !modulePaths.has(path);
const routes = resolveModuleRoutes(injector, MAIN_MENU_TAB_ROUTES);
mainRoute.path = mainRoute.path || '';
mainRoute.children = mainRoute.children || [];
mainRoute.children = mainRoute.children.concat(routes.children);
mainRoute.path = path;
modulePaths?.add(path);
return [
mainRoute,
...routes.siblings,
];
if (isRootRoute && !('redirectTo' in mainRoute)) {
mainRoute.children = mainRoute.children || [];
mainRoute.children = mainRoute.children.concat(routes.children);
}
return isRootRoute
? [mainRoute, ...routes.siblings]
: [mainRoute];
}
@NgModule()

View File

@ -213,7 +213,7 @@ export class CoreSettingsDeviceInfoPage implements OnDestroy {
}
const showDevOptionsOnConfig = await CoreConfig.get('showDevOptions', 0);
this.devOptionsForced = CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting;
this.devOptionsForced = CoreConstants.enableDevTools();
this.showDevOptions = this.devOptionsForced || showDevOptionsOnConfig == 1;
const publicKey = this.deviceInfo.pushId ?

View File

@ -24,6 +24,7 @@ import { SQLiteDB } from '@classes/sqlitedb';
import { APP_SCHEMA, CoreStorageRecord, TABLE_NAME } from './database/storage';
import { CoreSites } from './sites';
import { CoreSite } from '@classes/sites/site';
import { NULL_INJECTION_TOKEN } from '@/core/constants';
/**
* Service to store data using key-value pairs.
@ -38,7 +39,7 @@ export class CoreStorageService {
table: AsyncInstance<CoreStorageTable>;
constructor(@Optional() @Inject(null) lazyTableConstructor?: () => Promise<CoreStorageTable>) {
constructor(@Optional() @Inject(NULL_INJECTION_TOKEN) lazyTableConstructor?: () => Promise<CoreStorageTable>) {
this.table = asyncInstance(lazyTableConstructor);
}

View File

@ -404,17 +404,13 @@ export class TestingBehatRuntimeService {
this.log('Action - pullToRefresh');
try {
// 'el' is protected, but there's no other way to trigger refresh programatically.
const ionRefresher = this.getAngularInstance<{ el: HTMLIonRefresherElement }>(
'ion-refresher',
'IonRefresher',
);
const ionRefresher = this.getElement('ion-refresher');
if (!ionRefresher) {
return 'ERROR: It\'s not possible to pull to refresh the current page.';
}
ionRefresher.el.dispatchEvent(new CustomEvent('ionRefresh'));
ionRefresher.dispatchEvent(new CustomEvent('ionRefresh'));
return 'OK';
} catch (error) {
@ -521,20 +517,13 @@ export class TestingBehatRuntimeService {
}
/**
* Get an Angular component instance.
* Get element instance.
*
* @param selector Element selector
* @param className Constructor class name
* @param selector Element selector.
* @param referenceLocator The locator to the reference element to start looking for. If not specified, document body.
* @returns Component instance
* @returns Element instance.
*/
getAngularInstance<T = unknown>(
selector: string,
className: string,
referenceLocator?: TestingBehatElementLocator,
): T | null {
this.log('Action - Get Angular instance ' + selector + ', ' + className, referenceLocator);
private getElement<T = Element>(selector: string, referenceLocator?: TestingBehatElementLocator): T | null {
let startingElement: HTMLElement | undefined = document.body;
let queryPrefix = '';
@ -552,15 +541,8 @@ export class TestingBehatRuntimeService {
queryPrefix = '.ion-page:not(.ion-page-hidden) ';
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activeElement = Array.from(startingElement.querySelectorAll<any>(`${queryPrefix}${selector}`)).pop() ??
startingElement.closest(selector);
if (!activeElement || !activeElement.__ngContext__) {
return null;
}
return activeElement.__ngContext__.find(node => node?.constructor?.name === className);
return Array.from(startingElement.querySelectorAll(`${queryPrefix}${selector}`)).pop() as T
?? startingElement.closest(selector) as T;
}
/**
@ -632,26 +614,26 @@ export class TestingBehatRuntimeService {
if (locator) {
// Locator specified, try to find swiper-container first.
const instance = this.getAngularInstance<Swiper>('swiper-container', 'Swiper', locator);
if (instance) {
direction === 'left' ? instance.slideNext() : instance.slidePrev();
const swiperContainer = this.getElement<{ swiper: Swiper }>('swiper-container', locator);
if (swiperContainer) {
direction === 'left' ? swiperContainer.swiper.slideNext() : swiperContainer.swiper.slidePrev();
return 'OK';
}
}
// No locator specified or swiper-container not found, search swipe navigation now.
const instance = this.getAngularInstance<CoreSwipeNavigationDirective>(
'ion-content',
'CoreSwipeNavigationDirective',
const ionContent = this.getElement<{ swipeNavigation: CoreSwipeNavigationDirective }>(
'ion-content.uses-swipe-navigation',
locator,
);
if (!instance) {
if (!ionContent) {
return 'ERROR: Element to swipe not found.';
}
direction === 'left' ? instance.swipeLeft() : instance.swipeRight();
direction === 'left' ? ionContent.swipeNavigation.swipeLeft() : ionContent.swipeNavigation.swipeRight();
return 'OK';
}