Merge pull request #3877 from NoelDeMartin/MOBILE-3947
MOBILE-3947: Fix application startup and behat gesturesmain
commit
3b1f1fb638
|
@ -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' }}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue