Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

fakeAsync not waiting for async operation to complete

Context

I'm testing a component which is using an observable-based service to get some data and show it for i18n purpose.

The i18n service is custom because of a specific need.

The component works in dev mode (used in some templates, works fine) but the test fails.

Source

Component

@Component({
    selector     : "i18n",
    template     : '<span [innerHTML]="text"></span><span #wrapper hidden="true"><ng-content></ng-content><span>',
    encapsulation: ViewEncapsulation.None
})
export class I18nComponent implements OnChanges {

    constructor(private i18n:I18n) {
    }

    @ViewChild('wrapper')
    content:ElementRef;

    @Input('key')
    key:string;

    @Input('domain')
    domain:string;

    @Input('variables')
    variables:Variables = [];

    @Input("plural")
    plural:number;

    text:string;

    ngOnChanges():any {
        this.i18n.get(this.key, this.content.nativeElement.innerHTML, this.variables, this.domain).subscribe((res) => {
            this.text = res;
        });
    }
}

I18n.get

public get(key:string,
               defaultValue?:string,
               variables:Variables = {},
               domain?:string):Observable<string>{
    const catalog = {
                         "StackOverflowDomain":
                         {
                             "my-key":"my-value"
                         }
                    };

    return Observable.of(catalog[domain][key]).delay(300);
}

with Variables:

export interface Variables {
    [key:string]:any;
}

The test

describe("I18n component", () => {

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers   : [
                I18n,
                {
                    provide : I18N_CONFIG,
                    useValue: {
                        defaultLocale : "fr_FR",
                        variable_start: '~',
                        variable_end  : '~'
                    }
                },
                {
                    provide : I18N_LOADERS,
                    useClass: MockLocaleLoader,
                    multi   : true
                }
            ],
            declarations: [
                I18nComponent
            ]
        });
        fixture = TestBed.createComponent<I18nComponent>(I18nComponent);
        comp = fixture.componentInstance;
    });

    fit("can call I18n.get.", fakeAsync(() => {
        comp.content.nativeElement.innerHTML = "nope";
        comp.key = "test";
        comp.domain = "test domain";
        comp.ngOnChanges();
        tick();
        fixture.detectChanges();
        expect(comp.text).toBe("test value");
    }));

});

Problem

The test fails with message :

Expected undefined to be 'test value'.

Error: 1 periodic timer(s) still in the queue.

Because i18n.get did not finish its work before assertion has been checked, so comp.text is still undefined.

Already tried

  • Adding an very high value in tick method call, changed nothing (tried with 5000).
  • Make ngOnChanges return a Promise<void> that resolves right after this.text = res; and change fakeAsync zone for a simple test using a done method caled in then of comp.ngOnChanges. It works, but ngOnChanges should not return a Promise and I want a clean solution.
like image 967
Supamiu Avatar asked Oct 06 '16 12:10

Supamiu


1 Answers

Note that async and fakeasync are not as powerful and inclusive as jasmine.done

Taking the exact note from (at the time of this writing): https://angular.io/guide/testing#component-fixture

It says:

Writing test functions with done, while more cumbersome than async and fakeAsync, is a viable and occasionally necessary technique. For example, you can't call async or fakeAsync when testing code that involves the intervalTimer, as is common when testing async Observable

like image 197
Nylon Smile Avatar answered Nov 03 '22 12:11

Nylon Smile