I have an angular component which contains the following parts:
my.component.html (excerpt)
<button pButton
class="download ui-button ui-button-secondary"
(click)="exportFile(logEvent)"
icon="fas fa-file-download">
</button>
my.component.ts (excerpt)
import {saveAs} from 'file-saver';
exportFile(logEvent: LogEvent) {
saveAs(new Blob([logEvent.details]), 'log-details.txt');
}
This works perfectly in my application. I now wanted to test this in my unit tests. Looking for a way to make sure saveAs() has been called, I stumbled across two stackoverflow articles: mocking - Testing FileSaver in Angular 5 and Do you need spies to test if a function has been called in Jasmine?. Based on that I wrote the following test:
my.component.spec.ts (excerpt)
import * as FileSaver from 'file-saver';
beforeEach(() => {
spyOn(FileSaver, 'saveAs').and.stub();
});
it('should download a file if the download button is clicked', (fakeAsync() => {
// fakeAsync because in my real test, there are httpClient test aspects as well
advance(fixture);
expect(page.downloadButton).toBeDefined();
click(page.downloadButton);
advance(fixture);
expect(FileSaver.saveAs).toHaveBeenCalled();
}));
The two helper methods come from the Angular Testing Example:
export function advance(f: ComponentFixture<any>): void {
tick();
f.detectChanges();
}
export const ButtonClickEvents = {
left: {button: 0},
right: {button: 2}
};
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
}
}
My problem is, that test fails with the following output:
Error: Expected spy saveAs to have been called.
Error: 1 timer(s) still in the queue.
So it seems that neither the stubbing nor the assertion seem to work.
If I remove the click() call from my testm the 1 timer(s) still in the queue error no longer shows, so I gather that the click() method works and triggers the real saveAs() call - which I would have like to replace with the spy/mock.
How can I fix this?
Update, taking into account the proposed solutions:
I. Changing the import in my.component.spec.ts as suggested by SiddarthPal to:
import * as FileSaver from 'file-saver';
does not make any difference. The test still results in the two assertion errors.
II. Changing the spy setup as suggested by uminder to:
spyOn(FileSaver, 'saveAs').and.callFake(() => null);
does not make a difference either. The test still results in the two failed assertions.
I have tried writing the spy mock as follows:
spyOn(FileSaver, 'saveAs').and.callFake(() => {
console.log('--- faking saveAs ---');
return null;
});
Checking the output of the Karma Server output, I don't see this anywhere, so it looks like the spy does not catch my component's call to saveAs() at all.
III. The suggestion by uminder to replace:
click(page.downloadButton);
advance(fixture);
with
click(page.downloadButton);
flush();
does consume the pending timer error. Howver, that onyl hides the fact that the real saveAs() call is used, not the spy/mock. So I am still looking for ways to get that working.
Alright so I'm struggling with the exact same problem right now. My spy is also not working and it calls the real FileSaver methods. The suggestions here did also not solve the problem for me.
I believe the problem is, that the FileSaver is not part of a proper Angular Module, its just imported from FileSaver.js.
I had a very similar problem with mocking, described in this thread.
Tl;dr: I solved that problem by switching from a import usage of my Funnel to declaring a FunnelProvider and injecting that into the constructor of my real method like:
constructor(private funnelProvider: FunnelProvider) {}
In the spec file I was then able to mock that provider.
I thought, ok maybe there is a FileSaver.js wrapper library that provides an Angular Service for me, and there is. However the dependencies are outdated...that could be a problem. But switching from FileSaver.js to angular-file-saver should be a simple change.
The only other solution I can think of now, is to wrap the FileSaver usage into its own Angular Module yourself. Then import that Module and inject your newly built FileSaverService into your real class. In the spec file you could then do something like:
describe("MyComponent", () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let fileSaverServiceSpy: jasmine.SpyObj<FileSaverService>;
beforeEach(() => {
fileSaverServiceSpy= jasmine.createSpyObj('FileSaverService', ['saveAs']);
TestBed
.configureTestingModule({
declarations: [
MyComponent
],
providers: [
{ provide: FileSaverService, useValue: fileSaverServiceSpy }
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
});
it('saveMyFile() should call FileSaver', () => {
fileSaverServiceSpy.saveAs.and.stub();
MyComponent.saveMyFile("testFileName");
expect(fileSaverServiceSpy.saveAs).toHaveBeenCalled();
});
});
However, all that seems like a lot of work just to get the spy working.
Edit: I am testing this on an angular 9 project, these are my relevant dependencies:
"@angular/core": "9.1.12",
"jasmine-core": "3.6.0",
"jasmine-spec-reporter": "6.0.0",
"karma": "5.2.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "^2.0.0",
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.5.4",
"karma-typescript": "5.2.0",
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With