Angular 5 unit testing: How to make change detection work properly?

4.6k Views Asked by At

How to make change detection in unit tests work properly? From sources, changeDetection should be ran after microtasks are empty (including event tasks?).

this._onMicrotaskEmptySubscription = ngZone.onMicrotaskEmpty.subscribe({
    next: () => {
        if (this._autoDetect) {
            // Do a change detection run with checkNoChanges set to true to check
            // there are no changes on the second run.
            this.detectChanges(true);
        }
    }
});

In this short example, change detection ran after setTimeout, but not after manually clicking on the element. Is there a proper way to trigger change detection after event dispatching (inside fakeAsync zone, without fixture.detectChanges because in this case change detection will not be the same as in real life)?

import {
    fakeAsync, tick, ComponentFixture, TestBed, ComponentFixtureAutoDetect
} from '@angular/core/testing';
import {
    Component, QueryList, ViewChildren
} from '@angular/core';
import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
describe('bug', () => {
    let host: Host;
    let fixture: ComponentFixture<Host>;
    @Component({
        selector: 'child',
        template: ``,
    })
    class Child {}
    @Component({
        template: `
            <ng-container *ngFor="let show of shows">
                <child *ngIf="show"></child>
            </ng-container>
            <button (click)="shows[1] = true">show</button>`
    })
    class Host {
        shows = [false, false];
        @ViewChildren(Child) children: QueryList<Child>;
        constructor() {
            setTimeout(() => this.shows[0] = true, 50);
        }
    }
    fit('test', fakeAsync(() => {
        TestBed.configureTestingModule({
            imports: [
                CommonModule,
            ],
            declarations: [
                Host, Child,
            ],
            providers: [{
                provide: ComponentFixtureAutoDetect,
                useValue: true,
            }]
        });
        fixture = TestBed.createComponent(Host);
        host = fixture.componentInstance;
        tick(10);
        expect(host.children.length).toEqual(0);
        tick(50);
        expect(host.children.length).toEqual(1);
        const button = fixture.debugElement.query(By.css('button'));
        button.triggerEventHandler('click', new Event('click'));
        tick(50);
        // fixture.detectChanges();
        expect(host.children.length).toEqual(2); // fails here
    }));
});
1

There are 1 best solutions below

0
Ilia Volk On

I found the solution to my question. HTMLElement.dispatchEvent should be used. How to trigger input, keyup events:

const inputDe = this.de.query(By.css('input'));
const inputEl = inputDe.nativeElement;
inputEl.value = text;
inputEl.focus(); // if it has matAutocompleteTrigger value accessor
inputEl.dispatchEvent(new Event('input'));
inputEl.dispatchEvent(new KeyboardEvent('keyup', {
      key: 'Enter',
}));

Full example

import {
    fakeAsync, tick, ComponentFixture, TestBed, ComponentFixtureAutoDetect
} from '@angular/core/testing';
import {
    Component, QueryList, ViewChildren
} from '@angular/core';
import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
fdescribe('bug', () => {
    let host: Host;
    let fixture: ComponentFixture<Host>;
    @Component({
        selector: 'child',
        template: ``,
    })
    class Child {}
    @Component({
        template: `
            <ng-container *ngFor="let show of shows">
                <child *ngIf="show"></child>
            </ng-container>
            <button (click)="shows[1] = true">show</button>`
    })
    class Host {
        shows = [false, false];
        @ViewChildren(Child) children: QueryList<Child>;
        constructor() {
            setTimeout(() => this.shows[0] = true, 50);
        }
    }
    it('test', fakeAsync(() => {
        TestBed.configureTestingModule({
            imports: [
                CommonModule,
            ],
            declarations: [
                Host, Child,
            ],
            providers: [{
                provide: ComponentFixtureAutoDetect,
                useValue: true,
            }]
        });
        fixture = TestBed.createComponent(Host);
        host = fixture.componentInstance;
        tick(10);
        expect(host.children.length).toEqual(0);
        tick(50);
        expect(host.children.length).toEqual(1);
        const button = fixture.debugElement.query(By.css('button'));
        button.nativeElement.dispatchEvent(new Event('click')); // proper way
        tick(50);
        expect(host.children.length).toEqual(2); // no fail now
    }));
});