Как предоставить mock файлы для изменения события <input type='file'> для модульного тестирования

У меня возникают трудности с unit test, в котором я хочу проверить обработку файла, который обычно выбирается в представлении через <input type='file'>.

В части контроллера моего приложения AngularJS файл обрабатывается внутри события изменения ввода следующим образом:

//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!');
    // ...

Я хотел бы, чтобы evt.target.files содержал мои данные mock вместо выбранного пользователем файла в моем unit test. Я понял, что я не могу создать экземпляр объекта FileList и File самостоятельно, что будет соответствовать объектам, с которыми работает браузер. Поэтому я пошел с назначением mock FileList для свойства ввода files и запускал событие изменения вручную:

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!');
    });

К сожалению, это вызывает следующую ошибку в контроллере, которая показывает, что эта попытка потерпела неудачу, поскольку файлы вообще не были привязаны к входному элементу:

TypeError: 'undefined' не является объектом (оценка "evt.target.files" )

Я уже выяснил, что свойство input.files только для чтения по соображениям безопасности. Поэтому я начал новый подход, отправив настраиваемое изменение, которое обеспечило бы свойство файлов, но все же безуспешно.

Короче говоря: я бы хотел узнать о рабочем решении или о лучших методах подхода к этому тестовому сценарию.

Ответы

Ответ 1

Позвольте переосмыслить AngularJS, DOM must be handled in a directive

Мы не должны иметь дело с элементом DOM в контроллере, т.е. element.on('change', .., особенно для целей тестирования. В контроллере вы разговариваете с данными, а не с DOM.

Таким образом, те onchange должны быть директивой, подобной следующей

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

Однако, к сожалению, ng-change не работает с type="file". Я не уверен, что будущая версия работает с этим или нет. Однако мы все равно можем применить тот же метод.

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

и в контроллере мы просто просто определяем метод

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

Теперь все это обычный тест контроллера. Больше не имеет дело с angular.element, $compile, triggers и т.д.!:)

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

ОБНОВЛЕНИЕ: Благодаря @PeteBD,

Так как версия angularjs 1.2.22, jqLite теперь поддерживают передачу настраиваемого объекта события в triggerHandler(). См.: d262378b


Если вы используете только jqLite,

triggerHandler() никогда не будет работать, поскольку он передаст фиктивный объект события обработчикам.

Объект фиктивного события выглядит следующим образом (скопировано из jqLite.js#L962)

{
  preventDefault: noop,
  stopPropagation: noop
}

Как вы можете видеть, он даже не имеет свойства target.

Если вы используете jQuery,

вы можете вызвать событие с настраиваемым объектом события следующим образом:

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

а evt.target.files будет fileList, как вы ожидаете.

Надеюсь, что это поможет.

Ответ 3

Ваш обработчик изменения файла должен, вероятно, быть функцией непосредственно на вашем контроллере. Вы можете связать эту функцию с событием изменения либо из HTML, либо из директивы. Таким образом, вы можете напрямую вызвать функцию вашего обработчика, не беспокоясь о запуске события. Это видео egghead.io охватывает пару способов, которыми вы можете это сделать: https://egghead.io/lessons/angularjs-file-uploads

Есть много вещей, о которых вам нужно беспокоиться, когда вы загружаете свой собственный загрузчик файлов с помощью Angular, поэтому я бы просто использовал одну из существующих там библиотек, которая позаботится об этом. например angular-file-upload