Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue Acceptance Testing Uploads #123

Open
bryanaka opened this issue Aug 5, 2016 · 12 comments
Open

Issue Acceptance Testing Uploads #123

bryanaka opened this issue Aug 5, 2016 · 12 comments

Comments

@bryanaka
Copy link

bryanaka commented Aug 5, 2016

I am trying to write acceptance tests against uploading files using this lib. I've gotten to the point where I can correctly simulate all of the users' actions, including generating a mock file.

The problem is here:
https://github.com/benefitcloud/ember-uploader/blob/master/addon/components/file-field.js#L10

For security reasons, you are not allowed to change the files of an input type:
http://stackoverflow.com/questions/1017224/dynamically-set-value-of-a-file-input

So it makes it near impossible (unless I am missing something) to write an automated test simulating a user uploading a file.

I can understand this being a sensible route to go in production, but it would be nice to have a build in solution in test/dev that allows programmatic manipulation of the files.

right now in our app we are overwriting to do this

// components/file-field.js
// ...snip 
change(evt) {
    if( config.environment === 'production' || !Ember.isEmpty(input.files) ) {

      this._super(...arguments);
    } else {

      if (!Ember.isEmpty(evt.files)) {
        this.trigger('filesDidChange', evt.files);
      }
    }
},
// snip...

Any suggestions on how to do this better?

@digitaltoad
Copy link
Member

I guess I'm not totally sure what you are trying to do when you say that you cannot change the files of an input type. I understand that you cannot dynamically set the files of an input but you can simulate it by bypassing the input or messing with the change event which I think is the route you have taken. I have some tests around the uploader and input that use a Blob instead of an actual file.

@bryanaka
Copy link
Author

bryanaka commented Aug 7, 2016

Yep, this is fine in Unit tests like what you have.

But in Acceptance Tests, you need to fire the event on some kind of DOM element. Since the component relies on the change event on the input element, this is where I get stuck.

Once I wrap up the tests in our app, I'll create a gist or twiddle to demonstrate what I mean in more detail.

@marpo60
Copy link

marpo60 commented Sep 28, 2016

@bryanaka Faced a similar issue, how about doing the following:

// tests/helpers/start-app.js

export default function startApp(attrs) {
  ...
  let attributes = Ember.merge({
    customEvents: {
      filesDidChange: 'filesDidChange'
    }
  }, config.APP);
  attributes = Ember.merge(attributes, attrs); // use defaults, but you can override;
  ...
}

// tests/acceptance/home-test.js

test("something that needs to work", function(assert) {
  ...
  triggerEvent('.Home-files__file-input', 'filesDidChange');
  ...
});

@thedig
Copy link

thedig commented Oct 28, 2016

Also interested in a way to do acceptance test upload simulation. Have tried to use triggerHandler to trigger the change handler with a Blob, but the lib does not appear to respect this as I think Ember.Evented doesn't respond. So instead, tried directly triggering the event change instead but in this circumstance there's no way to append a mock file to the FileList object that ember-uploader checks in file-field.js. A means of simulating an upload for acceptance testing the UI response would be very helpful.

@digitaltoad
Copy link
Member

I'm working on a small example project to help with this.

@Duder-onomy
Copy link

I am also running into this issue right now. Would be helpful to get some guidance. Thanks for the cool library!

@jelhan
Copy link

jelhan commented Feb 2, 2017

ember-cli-file-picker provides test helpers. You could mock a change event. Files could be mocked by Blob object and used as event target. But a mocked event having a custom target does not fire embers change event. It's only working in ember-cli-file-picker cause it registers jQuery event handler in didInsertElement hook. I've asked at stackoverflow about the strange difference between jQuery event handler and ember event. Perhaps someone has an idea. Otherwise we could use same approach as ember-cli-file-picker.

@coreypmurphy
Copy link

coreypmurphy commented Nov 30, 2017

@marpo60 , your example helps in triggering the "filesDidChange" event and we get a little further along in testing but it doesn't solve the remaining hurdle for testing file upload which relates to the actual "files" object. Triggering that event does me no good if my "files" object is empty and I have no way to modify it with dummy file data.

I'm trying to test that filenames are valid (e.g. don't contain special characters, whitespace, etc.) My "filesDidChange" method handles it but since I can't modify the files object from my test I can't test that the UI reflects the violation to the user.

@coreypmurphy
Copy link

coreypmurphy commented Nov 30, 2017

Best solution to date given that as of Ember 2.5, "event.target" is now read only (emberjs/ember.js#13540) appears to be the following post from @chrism: https://medium.com/@chrisdmasters/acceptance-testing-file-uploads-in-ember-2-5-1c9c8dbe5368

Chris's approach with a few modifications to the component have met my need. My approach is as follows for those who are still looking for a solution:

file-upload.js component

  1. rename "filesDidChange" method to something else so you can manage when it's called
  2. override the change event method so you can sniff out the original event and either pass along the actual files object or the files object created by your test
  3. use file upload test helper from the above mentioned post to facilitate creating a mock files object and call it from your test
export default EmberUploader.FileField.extend({
  uploadFiles: function(files) {
    const uploader = EmberUploader.Uploader.create({
      url: this.get('url')
    });
    uploader.upload(files[0], { whateverObject });
  },

  change(event){
    const files = Ember.testing ? event.originalEvent.testingFiles : event.originalEvent.target.files;
    if (files.length === 0) {
      Ember.Logger.log('You must select one or more files');
    }else if (files.length > 0) {
      this.uploadFiles(files);
    }
  }
});

@chvonrohr
Copy link

chvonrohr commented Apr 25, 2018

after some debugging, i found a solution where no adjustments on the original code have to be made.
I'm quite not sure, which ember versions this support, as I'm working with Ember 3.1.

The triggerEvent function used in ember acceptance tests checks for the change-event and creates a special FileEvent:
https://github.com/emberjs/ember-test-helpers/blob/master/addon-test-support/%40ember/test-helpers/dom/fire-event.js#L183

Only thing to do is, pass created Blob-Files in an array as options:

/* global Blob */
import { module, test } from 'qunit';
import { triggerEvent } from '@ember/test-helpers';

module('Acceptance | my tests', function(hooks) {
  setupApplicationTest(hooks);

  test('my file upload works', async function(assert) {
     
    // ...
    
    const imageFile = new Blob(['test'], {type : 'image/png'});
    imageFile.name = 'my-image.png';
    await triggerEvent('.my-wrapper input[type=file]', 'change', [imageFile] );
   
    // ... assert something

  }
}

@artemgurzhii
Copy link

Here, is my solution for this, which doesn't require any changes to the source code. All of the modifications will be performed only in the /tests/* folder.

Also, all comments and description of what/how/why are in the code.

tests/helpers/start-app.js

export default function startApp(attrs) {
  let attributes = merge({
    // overriding default file upload event name to make `ember-uploader` work in test env
    customEvents: {
      filesDidChange: 'filesDidChange'
    }
  }, config.APP);
  attributes = merge(attributes, attrs); // use defaults, but you can override;

   // NOTE: Here goes rest of the code
}

tests/helpers/utils/upload-file.js

import Ember from 'ember';
// In our app we also have limits on the size of uploaded files
// If yours doesn't - than you should get rid of `fileContentTypesBySize`, `fileContentProxy` and other
import {
  FILE_SIZE_MIN_LIMIT as fileMinLimit,
  FILE_SIZE_MAX_LIMIT as fileMaxLimit,
} from 'voicereel/utils/constants';

const {
  Test: { registerAsyncHelper }
} = Ember;

const { floor, ceil } = Math;

const fileContentTypesBySize = {
  min: floor(fileMinLimit),
  normal: ceil((fileMinLimit + fileMaxLimit) / 2),
  max: ceil(fileMaxLimit),
};

// Generates fake body for the file
const fileContentProxy = new Proxy(fileContentTypesBySize, {
  get(target, property) {
    const content = Array(target[property] || target['normal']).fill(' ').join('');

    return [content];
  }
});

// Default values for the `uploadFile` options
const defaultOptions = {
  contentType: 'normal',
  type: 'text/html',
  name: 'index.html',
  amount: 1,
};

/**
 * @description Creating fake file which we will 'upload'
 *
 * @param {Object} options - Options passed to configure generated file
 * @param {String} [options.contentType='normal'] - Size of the file to be uploaded
 * @param {String} [options.name='index.html'] - Name of the file to be uploaded
 * @param {String} [options.type='text/html'] - Type of the file to be uploaded
 *
 * @return {Object} Created file
 */
function createFile({ contentType, name, type }) {
  const content = fileContentProxy[contentType];
  const file = new Blob(content, { type }); // NOTE: `Blob` constructor takes array of content as first argument

  file.name = name;

  return file;
}

/**
 * @description Upload file testing helper
 *
 * @param {Object} app - Application instance passed by ember directly
 * @param {String} selector - CSS selector where file should be uploaded
 * @param {Object} options - File configuration and uploading options
 * @param {String} [options.contentType='normal'] - Size of the file to be uploaded(gives ability to test uploaded file size validations)
 * @param {Number} [options.amount=1] - Amount of the files to be uploaded(gives ability to test uploaded files amount validations)
 * @param {String} [options.name='index.html'] - Name of the file to be uploaded
 * @param {String} [options.type='text/html'] - Type of the file to be uploaded
 *
 * @return {Promise} Triggered event promise
 */
export default registerAsyncHelper('uploadFile', function (app, selector, {
  contentType,
  amount,
  name,
  type,
} = defaultOptions) {
  const file = createFile({ contentType, name, type }); // Generating file

  // As in our app we have a limit on the files, that can be uploaded at once
  // Here we are configuring the amount of the files to be uploaded
  const files = Array(amount).fill(file);

  return triggerEvent(
    selector,
    'filesDidChange',
    { files }
  );
});

and finally our test file will look like

tests/acceptance/upload-html-file-test.js

import { test } from 'qunit';
import page from 'APP_NAME/tests/pages/upload-demos';
import HTMLFileUpload from 'APP_NAME/components/html-file-upload';
import moduleForAcceptance from 'voicereel/tests/helpers/module-for-acceptance';

moduleForAcceptance('Acceptance | upload html file', {
  beforeEach() {
    // Here we are overriding our custom upload component. 
    // To make sure that when `filesDidChange` is called
    // An array of files is being passed as an argument, but not the event, that was fired
    this.application.__container__.registry.register('component:html-file-upload', HTMLFileUpload.extend({
      filesDidChange(uploadedFiles) {
        // In test environment, `uploadedFiles` argument is an event that was fired
        // Here we are trying to get the files, that were uploaded
        const { files } = uploadedFiles.originalEvent;

        return this._super(files);
      }
    }));
  }
});

test('Upload single .html file', function(assert) {
  assert.expect(1);

  page
    .visit()
    .then(() => uploadFile('.html-uploader'))
    .then(() => {
      assert.equal(page.uploadedFiles.length, 1, 'One file was uploaded');
    });
});

test('Upload multiple .html files', function(assert) {
  assert.expect(1);

  page
    .visit()
    .then(() => uploadFile('.html-uploader', {
      contentType: 'normal',
      type: 'text/html',
      name: 'index.html',
      amount: 5,
    }))
    .then(() => {
      assert.equal(page.uploadedFiles.length, 5, 'All 5 files were succecfully uploaded');
    });
});

This is not a final solution, in some places it can be improved(for example when you want to configure any option of the uploadFile, you have to pass them all), but this is a first one that worked for me, so if anyone has any ideas how to improve it - please create gist and share it here.

P.S. @bryanaka @digitaltoad What do you think about this solution?
P.P.S. Inspired by acceptance testing file uploads in ember, thank you.

@Duder-onomy
Copy link

Just got here again.. You all rock!
@chvonrohr's solution worked for me (Ember 3.2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants