I am having the following error while writing a test: "Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout."
This is the test:
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DashboardTimerComponent, FormatTimePipe],
imports: [BrowserAnimationsModule, ReactiveFormsModule],
providers: [FormBuilder],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(DashboardTimerComponent);
component = fixture.componentInstance;
});
it('should stop counter and emit event', fakeAsync(() => {
spyOn(component.stopped, 'emit');
component.stopRequested = true;
component.runningTimer = { timer: 19 };
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector('#stop');
button.click();
expect(component.timer).toBeNull();
expect(component.stopped.emit).toHaveBeenCalled();
}));
This is the component:
@Component({
selector: 'dashboard-timer',
templateUrl: './dashboard-timer.component.html',
providers: [DashboardTimerService],
animations: [fadeInAnimation],
})
export class DashboardTimerComponent {
@Input() projects: any;
@Input() runningTimer: any = null;
@Output() started = new EventEmitter();
@Output() stopped = new EventEmitter();
public form: FormGroup;
public timer: number = null;
public stopRequested: boolean = false;
public counter: Subscription;
private project: FormControl = new FormControl('');
private note: FormControl = new FormControl('');
constructor(
private dashboardTimerService: DashboardTimerService,
private fb: FormBuilder,
) {}
ngOnInit() {
// initialize form
this.form = this.fb.group({
project: this.project,
note: this.note,
});
if (this.runningTimer) {
this.timer = this.runningTimer.timer;
this.form.controls['project'].setValue(this.runningTimer.project || '');
this.form.controls['note'].setValue(this.runningTimer.note || '');
this.counter = this.dashboardTimerService
.getCounter()
.subscribe(() => this.timer++);
}
}
/**
* check if stop requested, stop counter, emit stop to parent component
*/
stop(): void {
if (this.stopRequested === false) {
this.stopRequested = true;
setTimeout(() => {
this.stopRequested = false;
}, 5000);
return;
}
this.stopRequested = false;
this.counter.unsubscribe();
this.stopped.emit();
this.timer = null;
}
}
The error seems to be resulting from this service:
import { Injectable } from '@angular/core';
import { timer } from 'rxjs';
@Injectable()
export class DashboardTimerService {
getCounter() {
return timer(0, 1000);
}
}
I suppose the timer is still running, even though I unsubscribe from it in the component.
Any ideas how to solve this are very much appreciated!
Thank you!
Looking at your stackblitz, the component actually provides the service, which means, that the component creates its own instance of the service and does not use the mock value you provided inside the TestBed
providers.
So my first question would be, does this service really needs to be provided on the component itself?
If so I see two options:
Since your dataservice uses a timer with 1000 you would actually need to wait exactly that amount of time. I would use a fakeAsync
in combination with tick
for that.
it("should stop counter and emit event", fakeAsync(() => {
spyOn(component.stopped, "emit");
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector("#stop");
button.click();
tick(1000); // -> wait till you know that the promise will be resolved
fixture.detectChanges();
expect(component.timer).toBeNull();
expect(component.stopped.emit).toHaveBeenCalled();
}));
The other option would be to actually override the injected service after the component was created and with it the instance of the service.
it("should stop counter and emit event", fakeAsync(() => {
spyOn(component.stopped, "emit");
// override the actual injected service with your mock values here
(<any>TestBed.inject(DashboardTimerService)).getCounter = jest.fn().mockReturnValue(of(0))
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector("#stop");
button.click();
tick(); // i would still use fakeAsync and tick, since you are handling
// observables, which are still async even if they emit directly
fixture.detectChanges();
expect(component.timer).toBeNull();
expect(component.stopped.emit).toHaveBeenCalled();
}));