I have an input
HTML
File
filed
<input type="file" class="custom-file-input" id="question-file-upload" formControlName="image" (change)="handleFileSelect($event)">
I want to unit test handleFileSelect
function. But I don't know how to trigger the onChange
method of the input. Following is the spec
I have written but I get error imageInputNE.onchange is not a function
fit('should keep track of image counter when an image is loaded', () => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
let imageInputDE = fixture.debugElement.query(By.css("#question-file-upload"));
expect(imageInputDE).toBeTruthy();
spyOn(newPracticeQuestionComponent,'handleFileSelect');
let imageInputNE:HTMLElement = imageInputDE.nativeElement as HTMLElement;
imageInputNE.onchange(new Event("some event"));
expect(newPracticeQuestionComponent.handleFileSelect).toHaveBeenCalled();
});
Referring to documentation at Mozilla's website, Events are usually triggered by "external" sources like Buttons, they can also be triggered programmatically, such as by calling the HTMLElement.click() method of an element. or by defining the event, then sending it to a specified target using EventTarget.dispatchEvent().
I suppose that click
is a simple Event and can be triggered programatically but change
Event for <input type=file..>
is more complicated as it requires selecting a file, converting into a blog, updating files
property etc. So probably for this reason, I cannot just simply call imageElementNE.change()
.
So I thought of using dispatchEvent
and it seem to work
1) I created an Event explicitly. I suppose Event
's constructor takes 2 arguments, 2nd is optional.
From https://developer.mozilla.org/en-US/docs/Web/API/Event/Event
event = new Event(typeArg, eventInit);
From documentation, typeArg
is a DOMString representing the name of the event. In other words, the 1st argument seem to be the type
of the event. As I want to send change
event, I need to call it change
.
For the eventInit
part, I looked at the definition of change
event at https://developer.mozilla.org/en-US/docs/Web/Events/change and picked the values of bubbles
and cancelable
from there
let fileSelectEvent = new Event("change",{bubbles:true,
cancelable: false});
This creates an Event
which looks as follows (I must confess I don't understand much of it)
Event {isTrusted: false, type: "change", target: input#question-file-upload.custom-file-input.ng-untouched.ng-pristine.ng-valid, currentTarget: input#question-file-upload.custom-file-input.ng-untouched.ng-pristine.ng-valid, eventPhase: 2, …}bubbles: truecancelBubble: falsecancelable: falsecomposed: falsecurrentTarget: nulldefaultPrevented: falseeventPhase: 0isTrusted: falsepath: (10) [input#question-file-upload.custom-file-input.ng-untouched.ng-pristine.ng-valid, div#file-upload.custom-file, div.form-group, form#new-question-form.practice-question-form.ng-untouched.ng-pristine.ng-invalid, div#form-div.body__div--background, div#root0, body, html, document, Window]returnValue: truesrcElement: input#question-file-upload.custom-file-input.ng-untouched.ng-pristine.ng-validtarget: input#question-file-upload.custom-file-input.ng-untouched.ng-pristine.ng-validtimeStamp: 3759.4000000026426type: "change"__proto__: Event
2) Once the Event
was created, I called the dispatchEvent
method on imageInputNE
. Comparing with EventTarget.dispatchEvent()
, I think it means that the EventTarget
is triggering the event (in my case imageInputNE
is triggering fileSelectEvent
.
imageInputNE.dispatchEvent(fileSelectEvent);
The entire spec which passed is
fit('should keep track of image counter when an image is loaded', () => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
let imageInputDE = fixture.debugElement.query(By.css("#question-file-upload"));
expect(imageInputDE).toBeTruthy();
spyOn(newPracticeQuestionComponent,'handleFileSelect');/*.and.callThrough();/*.and.callFake((event)=>{
console.log("fake handleFileSelect called with event",event);
});*/
/*
nativeElemenet hasn'nt got any type. As this program will run in a browser, we can
typecast it to HTMLElement so that we can access prperties and methods of the
corresponding nativeElement
The HTMLElement interface represents any HTML element. Some elements directly
implement this interface, while others implement it via an interface that inherits it.
*/
let imageInputNE = imageInputDE.nativeElement ;
console.log("input element is ",imageInputNE);
console.log("debug element is ",imageInputDE);
//imageInputNE.click();
let fileSelectEvent = new Event("change",{bubbles:true,
cancelable: false});
console.log("created event ",fileSelectEvent);
imageInputNE.dispatchEvent(fileSelectEvent);
expect(newPracticeQuestionComponent.handleFileSelect).toHaveBeenCalled();
});
The above might not be the best solution as I can't manipuate files
in the input
field but this is the best I could come up with!
Coming from the other answer, I'd suggest you change the handleFileSelect
to just take files
.
interface File {
// the properties you need, for example:
name: string;
}
// You don't need all this unless you must use `item()`
// interface FileList {
// readonly length: number;
// item(index: number): File | null;
// [index: number]: File;
// }
// instead, just use ArrayLike<File>
export function handleFileSelect(files: ArrayLike<File>) {
// Write code here, for example:
if (files.length > 0) {
console.log(files[0].name);
}
}
Then your test should be just:
const files = [{
name: 'File 1'
}];
handleFileSelect(files);
And your component will just:
handleFileSelect(event.target.files);
Here's an example that shows how both usages do not trigger an error in the compiler.