Protractor: Comment implémenter des intervalles/interrogations dans angular2 pour travailler avec un rapporteur

Créé le 11 juil. 2016  ·  20Commentaires  ·  Source: angular/protractor

Rapport d'erreur

Le message original est ici
http://stackoverflow.com/questions/36358405/how-to-implement-intervals-polling-in-angular2-to-work-with-protractor

J'ai une application angular2 que je veux tester avec un rapporteur.

Dans cette application, j'ai une page avec un graphique qui est mis à jour à intervalles réguliers avec des données générées automatiquement.

Apparemment, une fonctionnalité du rapporteur attend que les scripts et les appels http se terminent avant d'exécuter le code de test. Cependant, s'il y a un script d'interrogation constante qui ne se termine jamais, le rapporteur attendra indéfiniment et expirera après un certain temps.

Dans angular1, cela pourrait être résolu en implémentant l'interrogation avec $interval , que le rapporteur n'attend pas. Malheureusement, dans angular2, il n'y a pas de $interval et la bonne façon d'implémenter l'interrogation semble être Observable.interval , voici donc à quoi ressemble mon code :

Observable.interval(500)
          .map(x => this.getRandomData())
          .subscribe(data => this.updateGraph(data));

Lors du test d'une page où ce code est en cours d'exécution, le rapporteur expirera. Il attend la fin du chargement de la page et pense que ce script se terminera un jour (alors qu'en fait il s'exécute pour toujours).

  • Existe-t-il un mécanisme d'intervalle dans angular2 que le rapporteur reconnaît, afin qu'il n'attende pas la fin de l'interrogation avant d'exécuter les tests d'interface utilisateur ?
  • Sinon, comment puis-je dire au rapporteur de ne pas attendre la fin de cet intervalle avant d'exécuter plus de code de test ?

EDIT : pour clarifier, le problème de délai d'attente existait déjà dans le rapporteur avec angular1, mais pourrait être résolu en utilisant $interval , voir :

Cela ne fonctionne pas dans angular2 car il n'y a pas de $interval .

Demande de fonctionnalité

  • Raisons d'adopter une nouvelle fonctionnalité
  • Est-ce un changement radical ? (Comment cela affectera-t-il les fonctionnalités existantes)
docs

Commentaire le plus utile

bonne discussion, cela a résolu le problème. merci juliemr ! :+1:

    this.ngZone.runOutsideAngular(() => {
          this.timer = Observable.interval(1000)
    }

Tous les 20 commentaires

Je publierai un formulaire plus long plus tard, mais la réponse courte est que vous pouvez dire à Protractor de ne pas attendre quelque chose en l'exécutant en dehors de la zone angulaire. Voir mon discours sur https://www.youtube.com/watch?v=DltUEDy7ItY et un exemple sur https://github.com/juliemr/ngconf-2016-zones/blob/master/src/app/main.ts# L38

bonne discussion, cela a résolu le problème. merci juliemr ! :+1:

    this.ngZone.runOutsideAngular(() => {
          this.timer = Observable.interval(1000)
    }

Salut, @juliemr. J'essaie d'exécuter un service d'interrogation en dehors de ngZone, mais je n'arrive pas à le faire fonctionner et mes tests E2E expirent. L'idée est que je dois mettre à jour certaines données utilisateur toutes les X secondes en les récupérant à partir du back-end et je peux avoir plusieurs consommateurs de ces données qui peuvent s'abonner ou se désabonner à tout moment. Voici le code :

import { Injectable, NgZone } from "@angular/core";
import { Http } from "@angular/http";
import { Observable } from "rxjs";

@Injectable()
export class SomeService {
    private someEmitter: Observable<SomeData>;

    constructor(private http: Http, private ngZone: NgZone) {
        this.ngZone.runOutsideAngular(() => {
            this.someEmitter = Observable
                .timer(0, 10000) // Run every 10 seconds
                .flatMap(() => http.get(`http://localhost/userData`))
                .map(response => {
                    this.ngZone.run(() => {
                        console.log('Done!');
                    });
                    return response.json().result;
                })
                .share();
        });
    }

    get data(): Observable<SomeData> {
        return this.someEmitter;
    }
}

Des suggestions sur la façon de résoudre ce problème ?

@Auxx J'ai eu un problème similaire et je l'ai résolu en écrivant mon Observable personnalisé qui imite Observable.delay(1000):

Observable.create((observer) => {
          let timeout = null;
          this._zone.runOutsideAngular(() => {
            timeout = setTimeout(() => {
              observer.next(null);
              observer.complete();
              return () => {
                if (timeout) {
                  clearTimeout(timeout);
                }
              };
            },1000);
          });
        });

Dans votre cas, je pense que le problème est que la minuterie sera créée lorsque vous appelez pour vous abonner pour la première fois et que vous le faites probablement à l'intérieur de la zone angulaire.

A FAIRE (moi) : en général, nous avons besoin de meilleures docs sur les tests Angular2. Trouvez une section où cela devrait aller.

Voici quelques retours pour l'équipe de rapporteurs...

Nous étions confrontés au même problème. Nous avons un magasin de commerce électronique basé sur Angular 2 avec une petite notification qui s'affiche dans le coin de l'écran lorsqu'un article est ajouté au panier, puis disparaît après 3 secondes (via setTimeout). Cela ne doit pas empêcher l'utilisateur de pouvoir ajouter des articles au panier.

L'un de nos tests E2E ajoute plusieurs articles au panier puis vérifie. Lorsque nous avons ajouté le code de notification, nous avons remarqué que le test E2E prenait beaucoup plus de temps car il attendait que la notification disparaisse entre l'ajout de chaque élément.

Je suis venu ici et j'ai vu ce problème et je me suis dit ce qui suit :
"c'est terrible que je doive écrire zone.runOutsideAngular dans le code de production juste pour que mon test E2E reflète le comportement réel de l'utilisateur et continue d'ajouter des éléments avec la notification toujours affichée. Si c'était Angular 1, j'utiliserais setTimeout ou $timeout comme voulu"

J'ai vite réalisé que cette idée était fausse, car faire la différence entre setTimeout et $timeout revient à faire la différence entre un setTimeout nu et un setTimeout dans zone.runOutsideAngular. Dans les deux cas, mon code de production est différent à cause de mes tests.

Ensuite, il m'a semblé que Protractor avait toutes ces manigances d'intégration waitForAngular et Angular pour nous aider en obéissant aux délais d'attente, aux requêtes HTTP, etc. Je suppose donc que cela fonctionne exactement comme prévu. C'est juste quelque chose qui, pour notre cas d'utilisation, est plus un obstacle.

Donc pour nous, la solution était que dans notre beforeEach nous faisons ceci :
browser.waitForAngularEnabled(false);

La raison en est que nous sommes très heureux que Protractor recule et que nous pouvons gérer l'attente nous-mêmes. Nous avons plusieurs assistants définis ici qui font diverses choses intéressantes en attente pour nous : https://github.com/SMH110/Pizza-website/blob/d342755a728e5afe7632a2c0e9b48f3253882a62/specs/e2e/utils.ts. Partout où nous voulons que Protractor attende Angular, nous pouvons l'activer explicitement puis le désactiver à nouveau après.

@massimocode qui ne fonctionne que pour les choses qui se produisent une fois, pas pour les minuteries qui fonctionnent tout le temps.

En général, j'ai trouvé qu'il vaut mieux que Protractor attende moins. Les utilisateurs finaux peuvent toujours ajouter plus d'attente plus tard, mais il est difficile de gérer les fonctions waitForAngular trop zélées. La seule option pour les utilisateurs finaux est de désactiver complètement l'attente et de mettre en œuvre leur propre truc. L'un de mes espoirs à l'avenir est de rendre les "manigances" waitForAngular plus flexibles, afin que les utilisateurs puissent ajouter de l'attente selon leurs besoins, tout en utilisant une API raisonnable qui leur permet de contrôler les types d'événements qu'ils attendent.

Je interroge un service http pour les mises à jour de statut dans mon application. Au départ, lorsque j'ai obtenu les délais d'attente du rapporteur, j'ai essayé l'option runOutsideAngular() . Cela permet au test de s'exécuter, mais j'ai besoin que la détection de changement fonctionne.

J'ai eu recours au réglage browser.ignoreSynchronization = true combiné avec browser.driver.sleep(1000) après tout changement de page, mais c'est maladroit et peu fiable.
Toute solution du côté du rapporteur serait la bienvenue.

Le service suivant fonctionne pour moi :

import {Injectable, NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {Observer} from 'rxjs/Observer';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/multicast';


@Injectable()
export class PoolingService {
    constructor(private zone: NgZone) {}

    // NOTE: Running the interval outside Angular ensures that e2e tests will not hang.
    execute<T>(operation: () => Observable<T>, frequency: number = 1000): Observable<T> {
        const subject = new Subject();
        const source = Observable.create((observer: Observer<T>) => {
            let sub: Subscription;
            this.zone.runOutsideAngular(() => {
                const zone = this.zone;
                sub = Observable.interval(frequency)
                    .mergeMap(operation)
                    .subscribe({
                        next(result) {
                            zone.run(() => {
                                observer.next(result);
                            });
                        },
                        error(err) {
                            zone.run(() => {
                                observer.error(err);
                            });
                        }
                    });
            });

            return () => {
                if (sub) {
                    sub.unsubscribe();
                }
            };
        });


        return source.multicast(subject)
            .refCount();
    }
}

Et vous l'utilisez comme tel :

const obs = this.pooling.execute(() => ... return an observable here, 1500);

obs.subscribe(() => {
    ... do something here
});

Cependant, si vous devez réellement attendre que les choses soient mises à jour, je ne sais pas quelle serait la solution.

@mgiambalvo - Je suis d'

@juliemr Existe-t-il un moyen de déterminer quelles tâches asynchrones sont en cours d'exécution lors de l'obtention d'un délai d'attente ? Nous avons parfois du mal à trouver la raison pour laquelle nous recevons des messages ScriptTimeoutError: asynchronous script timeout: result was not received in 11 seconds

Nous avons mis à jour la documentation à ce sujet et travaillons à améliorer les erreurs de délai d'attente dans les applications Angular modernes (c'est-à-dire pas AngularJS).

Cela fonctionne très bien si vous avez juste besoin de retarder un appel de fonction.

@Injectable()
export class DelayService {
    constructor (
        private _zone: NgZone
    ) {}

    public delay (fn: Function, delay: number): void {
        if (!fn || delay < 0) {
            throw new FrendError(`No function or invalid delay provided.`);
        }

        this._zone.runOutsideAngular(() => {
            Observable
                .of(null)
                .delay(delay)
                .subscribe(() => this._zone.run(() => fn()));
        });
    }
}

Devoir introduire de la complexité dans l'implémentation, afin de satisfaire certains tests de bout en bout est loin d'être idéal, mais il semble recommandé de s'amuser avec les zones et de contrôler manuellement ce qui s'exécute à l'intérieur ou à l'extérieur de la zone.

Je me demande s'il y a une sorte de patch de singe que nous pouvons faire pendant la phase de configuration de nos tests de bout en bout, afin que nous n'ayons pas à modifier le code de l'application. Toujours à la recherche...

@Merott tl;dr; Non. Vous ne pouvez rien faire pendant la phase de configuration.

La complexité n'est pas seulement "pour satisfaire certains tests de bout en bout" (en fait, il semble que vous sous-estimez la valeur des tests e2e) mais plutôt une question de structure d'application appropriée.

C'est ainsi que fonctionne le rapporteur - il doit savoir quand les tâches initiales de chargement de la page sont terminées. Si vous avez des tâches en arrière-plan, vous devez les exécuter dans leurs propres zones, non seulement pour faire fonctionner le rapporteur, mais pour de nombreuses autres raisons (comme la gestion des erreurs par zone, par exemple).

Juste mon 2c.

@AlexandrosD

Je ne pense pas sous-estimer la valeur des tests e2e. Cependant, je ne comprends peut-être pas très bien pourquoi la manière appropriée ou meilleure d'écrire la méthode suivante serait de la faire fonctionner en dehors de la zone d'Angular :

public poll(endpoint: string, pollingInterval: number) {
  return this.http
    .get(endpoint)
    .repeatWhen(() => Observable.interval(pollingInterval))
    .map(response => response.json());
}

Je me rends compte que cela peut légèrement dérouter le sujet de ce fil, mais j'essaie simplement de comprendre si ce problème Github concerne quelque chose à _réparer_, ou quelque chose à mieux _expliqué_.

Je suis tombé sur une solution plus élégante, utiliser un ordonnanceur pour exécuter certaines opérations en dehors de la zone angulaire. Cela permet de ne pas bidouiller run et runOutsideAngular au moment de souscrire à un observable mais plutôt de définir les choses à exécuter en dehors de la zone angulaire à la position de la séquence Observable .

https://stackoverflow.com/questions/43121400/run-ngrx-effect-outside-of-angulars-zone-to-prevent-timeout-in-protractor

Je viens d'écrire un article sur la façon dont cela pourrait également être résolu via le tuyau asynchrone (ou une fourchette de celui-ci): https://medium.com/@magnattic/preventing -the-infamous-protractor-timeouts-the-smart-way -66b8cc517b04

Ce problème bloque efficacement l'émission du tuyau async Angular avec timer observable basé sur runOutsideAngular , car l'abonnement se produit en interne, dans le tuyau async code.

La solution de contournement consiste à ne pas utiliser le tube async en effectuant manuellement toutes les modifications d'abonnement/désabonnement/déclenchement

En termes de montant de code :

template: '<span *ngIf="time" [innerHTML]="label$ | async"></span>',

ngOnInit() {
  this.label$ = timer(0, UPDATE_INTERVAL).pipe(map(() => this.formatLabel()));
}

VS:

  template: '<span *ngIf="time" [innerHTML]="label"></span>',

  ngOnInit() {
    this.setupLabelUpdate();
  }

  ngOnDestroy() {
    // eslint-disable-next-line chai-friendly/no-unused-expressions
    this.labelSubscription?.unsubscribe();
  }

  private setupLabelUpdate() {
    if (!this.time) {
      return;
    }

    this.ngZone.runOutsideAngular(() => {
      this.labelSubscription = timer(0, UPDATE_INTERVAL)
        .pipe(map(() => this.formatLabel()))
        .subscribe((label) => {
          this.label = label;
          this.changeDetector.detectChanges();
        });
    });
  }

Ce serait bien d'avoir une solution plus robuste pour des cas comme celui-ci.

Cette page vous a été utile?
0 / 5 - 0 notes