20

First example

I have got the following test:

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']);
    });
});

As you can see there is a super simple component, which just displays a list of items that are provided by a Promise. There are two tests, one which fails and one which passes. The only difference between those tests is that the test that passed calls fixture.detectChanges(); await fixture.whenStable(); twice.

UPDATE: Second example (updated again on 2019/03/21)

This example attempts to investigate into possible relations with 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()');
    }));
});

This first of these tests (explicitly using ngZone) results in:

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()

The second test logs:

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()

I kind of expected that the test runs in the angular zone, but it does not. The problem seems to come from the fact that

To avoid surprises, functions passed to then() will never be called synchronously, even with an already-resolved promise. (Source)

In this second example I provoked the problem by calling .then(x => x) multiple times, which will do no more than putting the progress again into the browser's event loop and thus delaying the result. In my understanding so far the call to await fixture.whenStable() should basically say "wait until that queue is empty". As we can see this actually works if I execute the code in ngZone explicitly. However this is not the default and I cannot find anywhere in the manual that it is intended that I write my tests that way, so this feels awkward.

What does await fixture.whenStable() actually do in the second test?. The source code shows that in this case fixture.whenStable() will just return Promise.resolve(false);. So I actually tried to replace await fixture.whenStable() with await Promise.resolve() and indeed it has the same effect: This does have an effect of suspending the test and commence with the event queue and thus the callback passed to valuePromise.then(...) is actually executed, if I just call await on any promise at all often enough.

Why do I need to call await fixture.whenStable(); multiple times? Am I using it wrong? Is it this intended behaviour? Is there any "official" documentation about how it is intended to work/how to deal with this?

HDJEMAI
  • 7,766
  • 41
  • 60
  • 81
yankee
  • 32,900
  • 12
  • 87
  • 147
  • I had the same thing in many cases in my app and gave up, just added it twice :) Would be interesting to see if anybody solves it here! – waterplea Mar 19 '19 at 10:06
  • It seems that it's got something to do with the promise and how the resolve works. Interestingly using a observable rather than the promise you wouldn't need to trigger the detectChanges twice. It would be interesting to know why though. https://stackblitz.com/edit/directive-testing-1bdxlz – Erbsenkoenig Mar 19 '19 at 14:45

2 Answers2

18

I believe you are experiencing Delayed change detection.

Delayed change detection is intentional and useful. It gives the tester an opportunity to inspect and change the state of the component before Angular initiates data binding and calls lifecycle hooks.

detectChanges()


Implementing Automatic Change Detection allows you to only call fixture.detectChanges() once in both test.

 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

This comment in Automatic Change Detection example is important, and why your tests still need to call fixture.detectChanges(), even with AutoDetect.

The second and third test reveal an important limitation. The Angular testing environment does not know that the test changed the component's title. The ComponentFixtureAutoDetect service responds to asynchronous activities such as promise resolution, timers, and DOM events. But a direct, synchronous update of the component property is invisible. The test must call fixture.detectChanges() manually to trigger another cycle of change detection.

Because of the way you are resolving the Promise as you are setting it, I suspect it is being treated as a synchronous update and the Auto Detection Service will not respond to it.

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

Automatic Change Detection


Inspecting the various examples given provides a clue as to why you need to call fixture.detectChanges() twice without AutoDetect. The first time triggers ngOnInit in the Delayed change detection model... calling it the second time updates the view.

You can see this based on the comments to the right of fixture.detectChanges() in the code example below

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');
}));

More async tests Example


In Summary: When not leveraging Automatic change detection, calling fixture.detectChanges() will "step" through the Delayed Change Detection model... allowing you the opportunity to inspect and change the state of the component before Angular initiates data binding and calls lifecycle hooks.

Also please note the following comment from the provided links:

Rather than wonder when the test fixture will or won't perform change detection, the samples in this guide always call detectChanges() explicitly. There is no harm in calling detectChanges() more often than is strictly necessary.


Second Example Stackblitz

Second example stackblitz showing that commenting out line 53 detectChanges() results in the same console.log output. Calling detectChanges() twice before whenStable() is not necessary. You are calling detectChanges() three times but the second call before whenStable() is not having any impact. You are only truly gaining anything from two of the detectChanges() in your new example.

There is no harm in calling detectChanges() more often than is strictly necessary.

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


UPDATE: Second example (updated again on 2019/03/21)

Providing stackblitz to demonstrate the different output from the following variants for your review.

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

Stackblitz

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

deamon
  • 78,414
  • 98
  • 279
  • 415
Marshal
  • 8,208
  • 2
  • 21
  • 38
  • While these information are very good, they don't really answer the question why I need to call these functions TWICE. Of course automatic change detection cannot detect things like property changes, so I need to call `detectChanges()`, but why twice? In the meantime I did some more research and added a second example to my question. – yankee Mar 20 '19 at 14:34
  • Your first call of `detectChanges()` is `NgOninit`, your second call of `detectChanges()` updates the view and initiates `data binding`. Your additional example illustrates this fact. – Marshal Mar 20 '19 at 14:45
  • The first call to `detectChanges()` which does call `NgOnInit` is in line 27 (first example) and line 37 (second example) in the `beforeEach` block. If you are counting that one in, then the question is why I need to call that function three times. – yankee Mar 20 '19 at 15:18
  • Comment out `detectChanges()` in line 53 after "After setting index" line, you will notice your output is the same with or without this `detectChanges()` line. Calling `detectChanges()` twice before `whenStable()` is not doing anything additional, and is unnecessary. You are only truly using `detectChanges()` twice in your example even though you are calling it three times. – Marshal Mar 20 '19 at 15:35
  • Provided second stackblitz example to illustrate commenting out line 53 `detectChanges()` results in the same `console.log` output. – Marshal Mar 20 '19 at 16:01
  • Thanks for the example. That is correct and actually I can also remove line 61, because it is also not doing anything (I already wrote this in the question). I thought that the second examples helps to figure out what is wrong with the first example. I should clarify that. – yankee Mar 20 '19 at 16:20
  • No worries. I definitely agree this is not very intuitive, simply stating in the official docs `there is no harm calling detectChanges() as much as you like` leaves a lot to be desired. This was my best attempt at providing clarity to your question. I hope it was somewhat helpful, or at a minimum helpful to others in the future. I think the key here is when not using `Auto detect`, call `detectChanges()` once for `NgOninit`, once to `update the view and initiate data binding`, and anytime after that when you `make an explicit synchronous change to a component property` required for the view. – Marshal Mar 20 '19 at 16:45
  • I got a little further with my own research and revised my second example again. In case that gives you any new ideas... – yankee Mar 21 '19 at 10:28
  • Still no change. All this does is change order of execution in a confusion fashion. After all `await somePromise; x();` is the same as `somePromise.then(() => x());`. The only reason why this stackblitz works is because you removed the `then(x => x).then(x => x)` from the component. – yankee Mar 21 '19 at 13:05
  • Line 13 in the function `set valueIndex` – yankee Mar 21 '19 at 13:08
  • I see what you mean, I did in fact miss this piece and only copied the it. I have removed my revision and additional comments. – Marshal Mar 21 '19 at 13:18
  • Please note: with your complete example in a stackblitz, `await somePromise; x();` does not behave the same as `somePromise.then(() => x());` – Marshal Mar 21 '19 at 13:22
  • `await fixture.whenStable();`, `fixture.whenStable().then(()=>{})` and `await fixture.whenStable().then(()=>{})` all have a different outcome in your console.log when applied to the `diagnoseState('After first whenStable()'); fixture.detectChanges();` does the third variant look correct in the output? – Marshal Mar 21 '19 at 13:28
  • provided revised stackblitz for your review of the three variants. – Marshal Mar 21 '19 at 13:38
  • `await fixture.whenStable().then(() => fixture.detectChanges()});` is in this case equal to `await Promise.resolve().then(() => fixture.detectChanges());` which again is equal to `await Promise.resolve().then(() => null); fixture.detectChanges()`, but is different to `await Promise.resolve(); fixture.detectChanges()` even if it looks the same. You can use `await` or you can use `then` to do something on Promise resolution, but mixing both together will end up in a headache. – yankee Mar 21 '19 at 15:46
  • So in reply to "Please note: with your complete example in a stackblitz, `await somePromise; x();` does not behave the same as `somePromise.then(() => x());`", that is correct, because it is not the same. However `await somePromise.then(x => null); x();` and `somePromise.then(() => x());` are equal. – yankee Mar 21 '19 at 15:48
0

In my opinion the second test seems wrong, it should be written following this pattern:

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

Please see: When Stable Usage

You should call detectChanges within whenStable() as

The fixture.whenStable() returns a promise that resolves when the JavaScript engine's task queue becomes empty.

Mac_W
  • 2,251
  • 1
  • 14
  • 28
  • This basically is the same as writing `await fixture.whenStable(); fixture.detectChanges();` rather than `fixture.detectChanges(); await fixture.whenStable();` and this does not change the outcome of the test. – yankee Mar 18 '19 at 15:29
  • So the above example still fails? – Mac_W Mar 18 '19 at 16:16
  • Yes, it does still fail – yankee Mar 18 '19 at 18:31
  • I think your "When Stable Usage" link might have been moved or changed. I can't find anything about whenStable() on that page any longer. – jmrah Nov 30 '20 at 01:41