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.
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 followingHowever, unfortunately,
ng-change
does not work well withtype="file"
. I am not sure that the future version works with this or not. We still can apply the same method though.and in the controller, we just simply define a method
Now, everything is just a normal controller test. No more dealing with
angular.element
,$compile
,triggers
, etc.! :)http://plnkr.co/edit/1J7ETus0etBLO18FQDhK?p=preview
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
Here is an example spec for input file/image using angular2+.
UPDATE: Thanks to @PeteBD,
Since angularjs version 1.2.22, the jqLite are now support passing a custom event object to
triggerHandler()
. See: d262378bIf 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)
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:
and the
evt.target.files
will be thefileList
as you are expecting.Hope this helps.