How to provide mock files to change event of <i

2019-02-08 02:13发布

问题:

I'm having difficulties with a unit test in which I want to verify the processing of a file, which would usually be selected in the view via <input type='file'>.

In the controller part of my AngularJS app the file is processed inside the input's change event like so:

//bind the change event of the file input and process the selected file
inputElement.on("change", function (evt) {
    var fileList = evt.target.files;
    var selectedFile = fileList[0];
    if (selectedFile.size > 500000) {
        alert('File too big!');
    // ...

I'd like evt.target.files to contain my mock data instead of the user's selected file in my unit test. I realized that I can't instantiate a FileList and File object by myself, which would be the according objects the browser is working with. So I went with assigning a mock FileList to the input's files property and triggering the change event manually:

describe('document upload:', function () {
    var input;

    beforeEach(function () {
        input = angular.element("<input type='file' id='file' accept='image/*'>");
        spyOn(document, 'getElementById').andReturn(input);
        createController();
    });

    it('should check file size of the selected file', function () {
        var file = {
            name: "test.png",
            size: 500001,
            type: "image/png"
        };

        var fileList = {
            0: file,
            length: 1,
            item: function (index) { return file; }
        };

        input.files = fileList; // assign the mock files to the input element 
        input.triggerHandler("change"); // trigger the change event

        expect(window.alert).toHaveBeenCalledWith('File too big!');
    });

Unfortunately, this causes the following error in the controller which shows that this attempt failed because the files were not assigned to the input element at all:

TypeError: 'undefined' is not an object (evaluating 'evt.target.files')

I already found out that the input.files property is read-only for security reasons. So I started another approach by dispatching a customized change which would provide the files property, but still without success.

So long story short: I'd be eager to learn a working solution or any best practices on how to approach this test case.

回答1:

Let's rethink AngularJS, DOM must be handled in a directive

We should not deal with DOM element in a controller, i.e. element.on('change', .., especially for testing purpose. In a controller, You talk to data, not to DOM.

Thus, those onchange should be a directive like the following

<input type="file" name='file' ng-change="fileChanged()" /> <br/>

However, unfortunately, ng-change does not work well with type="file". I am not sure that the future version works with this or not. We still can apply the same method though.

<input type="file" 
  onchange="angular.element(this).scope().fileChanged(this.files)" />

and in the controller, we just simply define a method

$scope.fileChanged = function(files) {
  return files.0.length < 500000;
};

Now, everything is just a normal controller test. No more dealing with angular.element, $compile, triggers, etc.! :)

describe(‘MyCtrl’, function() {
  it('does check files', inject(
    function($rootScope, $controller) {
      scope = $rootScope.new();
      ctrl = $controller(‘UploadCtrl’, {‘$scope’: scope});

      var files = { 0: {name:'foo', size: 500001} };
      expect(scope.fileChanged(files)).toBe(true);
    }
  ));
});

http://plnkr.co/edit/1J7ETus0etBLO18FQDhK?p=preview



回答2:

UPDATE: Thanks to @PeteBD,

Since angularjs version 1.2.22, the jqLite are now support passing a custom event object to triggerHandler(). See: d262378b


If you are using only jqLite,

the triggerHandler() will never work as it will pass a dummy event object to handlers.

The dummy event object look like this (copied from jqLite.js#L962)

{
  preventDefault: noop,
  stopPropagation: noop
}

As you can see, it doesn't even have a target property.

If you are using jQuery,

you could trigger an event with a custom event object like this:

input.triggerHandler({
  type: 'change',
  target: {
    files: fileList
  }
});

and the evt.target.files will be the fileList as you are expecting.

Hope this helps.



回答3:

Here is an example spec for input file/image using angular2+.

it('should call showError on toastService Api on call of onSaveOfImage() method', () => {

    spyOn(component.commonFacade.fileIOApi, 'uploadFile');
    let file = new File([new ArrayBuffer(2e+5)], 'test-file.jpg', { lastModified: null, type: 'image/jpeg' });
    let fileInput={ files: [file] };
    component['onSaveOfImage'](fileInput,"",null,"","");
    expect(component.commonFacade.fileIOApi.uploadFile).toHaveBeenCalledTimes(1);
    expect(component.uploadedFileData).toBeUndefined();
    expect(component.commonFacade.employeeApi.toastService.showError).toHaveBeenCalledTimes(1);
  })


回答4:

Your file change handler should probably be a function directly on your controller. You can bind that function to the change event either from the HTML or a directive. That way you can call your handler function directly without worrying about triggering an event. This egghead.io video covers a couple ways you can do that: https://egghead.io/lessons/angularjs-file-uploads

There are a lot of things you need to worry about when rolling your own file uploader with Angular so I would just use one of the existing libraries out there that takes care of it. e.g. angular-file-upload