how to trigger input onchange from Angular spec

2019-08-01 16:32发布

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();


  });

2条回答
2楼-- · 2019-08-01 16:55

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!

查看更多
姐就是有狂的资本
3楼-- · 2019-08-01 17:12

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.

查看更多
登录 后发表回答