Acabo de empezar a escribir pruebas e2e para mi aplicación y tengo problemas de tiempo de espera con Protractor y ngrx/effects.
Tengo el siguiente efecto enviando una acción cada par de minutos:
@Effect() setSessionTimer$ = this.actions$ .ofType(Auth.ActionTypes.SET_SECONDS_LEFT) .map(toPayload) .switchMap(secondsLeft => Observable.concat( Observable.timer((secondsLeft - 60) * 1000).map(_ => new Auth.SessionExpiringAction(60)), Observable.timer(60 * 1000).map(_ => new Auth.SessionExpiredAction()) ));Intentar ejecutar una prueba de transportador hace que la prueba se agote con el siguiente error, ya que Angular no es estable.
Error: se agotó el tiempo de espera para que finalicen las tareas angulares asincrónicas después de 11 segundos. Esto puede deberse a que la página actual no es una aplicación de Angular. Consulte las preguntas frecuentes para obtener más detalles: https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular Mientras espera el elemento con el localizador - Localizador: Por (css selector, . barra de herramientas-título)
De acuerdo con este problema ( https://github.com/angular/protractor/issues/3349 ), necesito usar NgZone para ejecutar un intervalo Observable fuera de Angular. Probé diferentes combinaciones de this.ngZone.runOutsideAngular() pero nada funcionó y las pruebas siguen agotando el tiempo de espera.
Por ejemplo esto no funciona:
@Effect() setSessionTimer$ = this.actions$ .ofType(Auth.ActionTypes.SET_SECONDS_LEFT) .map(toPayload) .switchMap(secondsLeft => this.ngZone.runOutsideAngular(() => Observable.concat( Observable.timer((secondsLeft - 60) * 1000).map(_ => new Auth.SessionExpiringAction(60)), Observable.timer(60 * 1000).map(_ => new Auth.SessionExpiredAction()) )));No tengo idea de cómo ejecutar el efecto fuera de Angular. ¿Alguien ha probado con éxito e2e su aplicación ngrx?
La solución es programar el temporizador observable para que se ejecute fuera de NgZone y luego volver a ingresar a la zona cuando ocurra algo interesante.
Primero, necesitará dos funciones de utilidad que envuelvan cualquier planificador y hagan que el efecto entre o salga de la zona:
import { Subscription } from 'rxjs/Subscription'; import { Scheduler } from 'rxjs/Scheduler'; import { NgZone } from '@angular/core'; class LeaveZoneSchduler { constructor(private zone: NgZone, private scheduler: Scheduler) { } schedule(...args: any[]): Subscription { return this.zone.runOutsideAngular(() => this.scheduler.schedule.apply(this.scheduler, args) ); } } class EnterZoneScheduler { constructor(private zone: NgZone, private scheduler: Scheduler) { } schedule(...args: any[]): Subscription { return this.zone.run(() => this.scheduler.schedule.apply(this.scheduler, args) ); } } export function leaveZone(zone: NgZone, scheduler: Scheduler): Scheduler { return new LeaveZoneSchduler(zone, scheduler) as any; } export function enterZone(zone: NgZone, scheduler: Scheduler): Scheduler { return new EnterZoneScheduler(zone, scheduler) as any; } Luego, al usar un programador (como asap o async ), puede hacer que una transmisión ingrese o salga de la zona:
import { async } from 'rxjs/scheduler/async'; import { enterZone, leaveZone } from './util'; actions$.ofType('[Light] Turn On') .bufferTime(300, leaveZone(this.ngZone, async)) .filter(messages => messages.length > 0) .observeOn(enterZone(this.ngZone, async)) Tenga en cuenta que la mayoría de los operadores basados en el tiempo (como bufferTime , debounceTime , Observable.timer , etc.) ya aceptan un planificador alternativo. Solo necesita observeOn para volver a ingresar a la zona cuando sucede algo interesante.
Para Angular 6 y RxJS 6 use el siguiente código:
import { SchedulerLike, Subscription } from 'rxjs' import { NgZone } from '@angular/core' class LeaveZoneScheduler implements SchedulerLike { constructor(private zone: NgZone, private scheduler: SchedulerLike) { } schedule(...args: any[]): Subscription { return this.zone.runOutsideAngular(() => this.scheduler.schedule.apply(this.scheduler, args) ) } now (): number { return this.scheduler.now() } } class EnterZoneScheduler implements SchedulerLike { constructor(private zone: NgZone, private scheduler: SchedulerLike) { } schedule(...args: any[]): Subscription { return this.zone.run(() => this.scheduler.schedule.apply(this.scheduler, args) ) } now (): number { return this.scheduler.now() } } export function leaveZone(zone: NgZone, scheduler: SchedulerLike): SchedulerLike { return new LeaveZoneScheduler(zone, scheduler) } export function enterZone(zone: NgZone, scheduler: SchedulerLike): SchedulerLike { return new EnterZoneScheduler(zone, scheduler) }El efecto debería verse así:
import { asyncScheduler, queueScheduler } from 'rxjs' import { filter, observeOn, bufferTime } from 'rxjs/operators' import { enterZone, leaveZone } from './util'; actions$.ofType('[Light] Turn On') .pipe( bufferTime(300, leaveZone(this.ngZone, asyncScheduler)), filter(messages => messages.length > 0), observeOn(enterZone(this.ngZone, queueScheduler)), )