How to provide mock files to change event of <input type='file'> for unit testing
Solution 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
Solution 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.
Solution 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);
})
Mobiletainment
My name is David Pertiller. I'm a passionate software engineer
Updated on July 09, 2022Comments
-
Mobiletainment almost 2 years
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 aFileList
andFile
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'sfiles
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.