web-dev-qa-db-fra.com

Pourquoi dois-je appeler deux fois detectChanges / whenStable?

Premier exemple

J'ai le test suivant:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { Component } from '@angular/core';

@Component({
    template: '<ul><li *ngFor="let state of values | async">{{state}}</li></ul>'
})
export class TestComponent {
    values: Promise<string[]>;
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;
    let element: HTMLElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
        element = (<HTMLElement>fixture.nativeElement);
    });

    it('this test fails', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });

    it('this test works', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });
});

Comme vous pouvez le voir, il existe un composant super simple, qui affiche simplement une liste d'éléments fournis par un Promise. Il y a deux tests, l'un qui échoue et l'autre qui réussit. La seule différence entre ces tests est que le test qui a réussi appelle deux fois fixture.detectChanges(); await fixture.whenStable();.

MISE À JOUR: Deuxième exemple (mis à jour à nouveau le 21/03/2019)

Cet exemple tente d'étudier les relations possibles avec ngZone:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { Component, NgZone } from '@angular/core';

@Component({
    template: '{{value}}'
})
export class TestComponent {
    valuePromise: Promise<ReadonlyArray<string>>;
    value: string = '-';

    set valueIndex(id: number) {
        this.valuePromise.then(x => x).then(x => x).then(states => {
            this.value = states[id];
            console.log(`value set ${this.value}. In angular zone? ${NgZone.isInAngularZone()}`);
        });
    }
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [FormsModule],
            declarations: [TestComponent],
            providers: [
            ]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    function diagnoseState(msg) {
        console.log(`Content: ${(fixture.nativeElement as HTMLElement).textContent}, value: ${component.value}, isStable: ${fixture.isStable()} # ${msg}`);
    }

    it('using ngZone', async() => {
        // setup
        diagnoseState('Before test');
        fixture.ngZone.run(() => {
            component.valuePromise = Promise.resolve(['a', 'b']);

            // execution
            component.valueIndex = 1;
        });
        diagnoseState('After ngZone.run()');
        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');
    });

    it('not using ngZone', async(async() => {
        // setup
        diagnoseState('Before setup');
        component.valuePromise = Promise.resolve(['a', 'b']);

        // execution
        component.valueIndex = 1;

        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');

        await fixture.whenStable();
        diagnoseState('After second whenStable()');
        fixture.detectChanges();
        diagnoseState('After second detectChanges()');

        await fixture.whenStable();
        diagnoseState('After third whenStable()');
        fixture.detectChanges();
        diagnoseState('After third detectChanges()');
    }));
});

Ce premier de ces tests (utilisant explicitement ngZone) se traduit par:

Content: -, value: -, isStable: true # Before test
Content: -, value: -, isStable: false # After ngZone.run()
value set b. In angular zone? true
Content: -, value: b, isStable: true # After first whenStable()
Content: b, value: b, isStable: true # After first detectChanges()

Les deuxièmes journaux de test:

Content: -, value: -, isStable: true # Before setup
Content: -, value: -, isStable: true # After first whenStable()
Content: -, value: -, isStable: true # After first detectChanges()
Content: -, value: -, isStable: true # After second whenStable()
Content: -, value: -, isStable: true # After second detectChanges()
value set b. In angular zone? false
Content: -, value: b, isStable: true # After third whenStable()
Content: b, value: b, isStable: true # After third detectChanges()

Je m'attendais à ce que le test s'exécute dans la zone angular, mais ce n'est pas le cas. Le problème semble provenir du fait que

Pour éviter les surprises, les fonctions passées à then () ne seront jamais appelées de manière synchrone, même avec une promesse déjà résolue. ( Source )

Dans ce deuxième exemple, j'ai provoqué le problème en appelant .then(x => x) plusieurs fois, ce qui ne fera rien de plus que de replacer la progression dans la boucle d'événements du navigateur et donc de retarder le résultat. D'après ce que je comprends jusqu'à présent, l'appel à await fixture.whenStable() devrait essentiellement dire "attendre que cette file d'attente soit vide". Comme nous pouvons le voir, cela fonctionne réellement si j'exécute le code dans ngZone explicitement. Cependant, ce n'est pas la valeur par défaut et je ne trouve nulle part dans le manuel que je souhaite écrire mes tests de cette façon, donc cela semble gênant.

Que fait réellement await fixture.whenStable() dans le deuxième test?. Le code source montre que dans ce cas fixture.whenStable() ne fera que return Promise.resolve(false);. J'ai donc essayé de remplacer await fixture.whenStable() par await Promise.resolve() et en fait, cela a le même effet: cela a pour effet de suspendre le test et de commencer par la file d'attente des événements et donc le rappel passé à valuePromise.then(...) est en fait exécutée, si j'appelle juste await sur n'importe quelle promesse du tout assez souvent.

Pourquoi dois-je appeler await fixture.whenStable(); plusieurs fois? Suis-je mal utilisé? Est-ce ce comportement voulu? Existe-t-il une documentation "officielle" sur la façon dont il est censé fonctionner/comment y faire face?

16
yankee

Je crois que vous rencontrez Delayed change detection.

La détection de changement retardé est intentionnelle et utile. Il donne au testeur l'occasion d'inspecter et de modifier l'état du composant avant Angular lance la liaison de données et appelle les hooks du cycle de vie.

detectChanges ()


L'implémentation de Automatic Change Detection Vous permet d'appeler une seule fois fixture.detectChanges() dans les deux tests.

 beforeEach(async(() => {
            TestBed.configureTestingModule({
                declarations: [TestComponent],
                providers:[{ provide: ComponentFixtureAutoDetect, useValue: true }] //<= SET AUTO HERE
            })
                .compileComponents();
        }));

Stackblitz

https://stackblitz.com/edit/directive-testing-fnjjqj?embed=1&file=app/app.component.spec.ts

Ce commentaire dans l'exemple Automatic Change Detection Est important, et pourquoi vos tests doivent encore appeler fixture.detectChanges(), même avec AutoDetect.

Les deuxième et troisième tests révèlent une limitation importante. L'environnement de test Angular ne sait pas que le test a changé le titre du composant. Le service ComponentFixtureAutoDetect répond aux activités asynchrones telles que la résolution de promesse, les temporisateurs et les événements DOM. Mais une mise à jour directe et synchrone du La propriété du composant est invisible. Le test doit appeler manuellement fixture.detectChanges () pour déclencher un autre cycle de détection des modifications.

En raison de la façon dont vous résolvez la Promesse telle que vous la définissez, je soupçonne qu'elle est traitée comme une mise à jour synchrone et le Auto Detection Service N'y répondra pas.

component.values = Promise.resolve(['A', 'B']);

Détection automatique des modifications


En examinant les divers exemples fournis, vous pouvez comprendre pourquoi vous devez appeler fixture.detectChanges() deux fois sans AutoDetect. La première fois déclenche ngOnInit dans le modèle Delayed change detection ... l'appelant la deuxième fois met à jour la vue.

Vous pouvez le voir sur la base des commentaires à droite de fixture.detectChanges() dans l'exemple de code ci-dessous

it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  tick(); // flush the observable to get the quote
  fixture.detectChanges(); // update view

  expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
  expect(errorMessage()).toBeNull('should not show error');
}));

Plus d'exemples de tests asynchrones


En résumé: Lorsque vous ne tirez pas parti de Automatic change detection, Appeler fixture.detectChanges() "passera" à travers le Delayed Change Detection model ... vous permettant d'inspecter et de modifier l'état du composant avant Angular lance la liaison de données et appelle les hooks du cycle de vie.

Veuillez également noter le commentaire suivant à partir des liens fournis:

Plutôt que de se demander quand le dispositif de test effectuera ou non la détection de changement, les exemples de ce guide appellent toujours detectChanges () explicitement. Il n'y a aucun mal à appeler detectChanges () plus souvent qu'il n'est strictement nécessaire.


Deuxième exemple Stackblitz

Deuxième exemple de stackblitz montrant que la mise en commentaire de la ligne 53 detectChanges() entraîne la même sortie console.log. Il n'est pas nécessaire d'appeler detectChanges() deux fois avant whenStable(). Vous appelez detectChanges() trois fois mais le deuxième appel avant whenStable() n'a aucun impact. Vous ne gagnez vraiment rien de deux des detectChanges() dans votre nouvel exemple.

Il n'y a aucun mal à appeler detectChanges () plus souvent qu'il n'est strictement nécessaire.

https://stackblitz.com/edit/directive-testing-cwyzrq?embed=1&file=app/app.component.spec.ts


MISE À JOUR: Deuxième exemple (mis à jour le 21/03/2019)

Fournir stackblitz pour démontrer les différentes sorties des variantes suivantes pour votre examen.

  • attendre fixture.whenStable ();
  • fixture.whenStable (). then (() => {})
  • attendre fixture.whenStable (). then (() => {})

Stackblitz

https://stackblitz.com/edit/directive-testing-b3p5kg?embed=1&file=app/app.component.spec.ts

11
Marshal

À mon avis, le deuxième test semble erroné, il devrait être écrit selon ce modèle:

component.values = Promise.resolve(['A', 'B']);
fixture.whenStable().then(() => {
  fixture.detectChanges();       
  expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
});

S'il vous plaît voir: Lorsque l'utilisation stable

Vous devez appeler detectChanges dans whenStable() comme

Le fixture.whenStable () renvoie une promesse qui se résout lorsque la file d'attente de tâches du moteur JavaScript devient vide.

0
Mac_W