Files upload testing in Enzyme - reactjs

I have a FileInput in my render function
<FileInput
accept= "image/jpeg,image/png,audio/mp3"
onChange= {this.fileInputOnChange}
children= {<i className="fa fa-paperclip"/>}
className= 'fileInput'
/>
I need to write a test for file upload, when I simulate the change function it call the function fileInputOnChange
fileInputOnChange: function(evt){
var file = evt.target.files[0];
var fileReader = new FileReader();
fileReader.onload = function(readFile){
// Check the file type.
var fileType = file.type.toLowerCase();
if(fileType.lastIndexOf('image') === 0 && (fileType.lastIndexOf('png') >= 0 || fileType.lastIndexOf('jpeg'))){
var image = new Image();
image.onload = function(){
var attachedFile = {
attached: file,
mediaSource: readFile.target.result,
type: 'image'
}
this.props.onChange(attachedFile);
}.bind(this);
image.onerror = function(){
this.props.onError("INVALID_TYPE");
}.bind(this);
image.src = readFile.target.result;
}else if(fileType.lastIndexOf('audio') === 0 && fileType.lastIndexOf('mp3') >= 0){
//#todo: manage audio upload here
var audio = new Audio();
audio.oncanplaythrough = function(){
var attachedFile = {
attached: file,
mediaSource: readFile.target.result,
type: 'audio'
}
this.props.onChange(attachedFile);
}.bind(this);
audio.onerror = function(e){
this.props.onError("INVALID_TYPE");
}.bind(this)
audio.src = readFile.target.result;
}else if (fileType.lastIndexOf('video') === 0 && fileType.lastIndexOf('mp4') >= 0){
var video = document.createElement('video');
video.oncanplaythrough = function(){
var attachedFile = {
attached: file,
mediaSource: readFile.target.result,
type: 'video'
}
this.props.onChange(attachedFile);
}.bind(this);
video.onerror = function(){
this.props.onError("INVALID_TYPE");
}.bind(this)
video.src = readFile.target.result;
}
}.bind(this);
fileReader.onerror = function(){
this.props.onError("READING_ERROR");
}.bind(this)
fileReader.readAsDataURL(file); }
I could not add any files while simulating the upload button, I am confused of how to write the test for this scenario. Anyone ever came across this kinda scenarios? I would be greatful for all sorta helps.
it('testing attachfile change function...',()=>{
const wrapper=shallow(<AttachFile />);
wrapper.find('FileInput').simulate('change');
console.log(wrapper.debug());
});
When I tried the above test I got the following error
TypeError: Cannot read property 'target' of undefined
at [object Object].fileInputOnChange (js/components/home/chats/AttachFile.react.js:11:16)
at node_modules/enzyme/build/ShallowWrapper.js:768:23
at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:138:20)
at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:63:19)
at batchedUpdates (node_modules/react/lib/ReactUpdates.js:98:20)
at node_modules/enzyme/build/ShallowWrapper.js:767:45
at withSetStateAllowed (node_modules/enzyme/build/Utils.js:196:3)
at ShallowWrapper.simulate (node_modules/enzyme/build/ShallowWrapper.js:764:42)
at Context.<anonymous> (test/sample.js:40:27)

You'll need to provide a mocked event object, something like:
wrapper.find('FileInput').simulate('change', {
target: {
files: [
'dummyValue.something'
]
}
});
Your component its making a lot of work inside, its like a huge side effect (it defines two callbacks with logic nailed). It's going to be difficult, but I suppose you'll need to mock FileReader as well using two spies, one reacting to the readAsDataURL calling the onload and another one calling the onerror.
Then you can check if your callbacks are doing what is supposed to.
Hope it helps!

Related

Set component state, from unreachable variable

How I can get media.duration to live in a state environment ?
Right now I don't have access to it, even after trying to change state
handleClick(e) {
e.preventDefault();
var reader = new FileReader();
reader.readAsDataURL(this.state.uploadedFile);
reader.onload = function() {
var media = new Audio(reader.result);
media.onloadedmetadata = function() {
console.log(media.duration); // <-- THIS WORKS!!!!!!!!!!!!!!
};
};
console.log(media.duration) //<------THIS DOES NOT WORK!!!!!!!!!!
console.log("The link was clicked.");
console.log(this.state.duration);
}
from: How to get duration of video when I am using filereader to read the video file?
_________________________________________
I tried this but it did not work
this.setState({duration:media.duration},()=>{
console.log(this.state.duration)
})
It's because you use media variable is out of the scope.
Please try this one.
handleClick(e) {
e.preventDefault();
var reader = new FileReader();
reader.readAsDataURL(this.state.uploadedFile);
reader.onload = () => {
var media = new Audio(reader.result);
media.onloadedmetadata = () => {
console.log(media.duration); // <-- THIS WORKS!!!!!!!!!!!!!!
this.setState({duration: media.duration});
};
};
}

this.state is not working on reader.onload funtion in react js

I want to set state, but this.state is not working, can anyone please check this code, and help me what's issue in that, here is my code
save_import_permit() {
//alert('sdsdsd');
var file_name = jQuery('input[type=file]').val().split('\\').pop(); //jQuery('#permit_csv_file').prop('files')[0];
let files = jQuery('input[type=file]')[0].files[0];
console.log(files);
let reader = new FileReader();
let data = [];
reader.readAsDataURL(files);
reader.onload = function(e) {
console.log(e.target.result);
data.result = e.target.result;
data.action = 'Permity::saveImportCsvFile';
const url = 'admin-ajax.php';
const formdata = "file="+data.result+"&action="+data.action;
this.state({loading:'loading_show'})
return post(url,formdata,function (r) {
this.state({loading:'loading_hide'});
alert(r.data.msg)
}.bind(this));
}.bind(this);
}
You shouldn't attribute a value to the state using this.state, you should always use this.setState(), wichs is a build-in function of the react framework. Check this link to know better.
So you need to change
this.state({loading:'loading_show'})
to
this.setState({loading:'loading_show'})

File reader Sync

I have the following javascript code which is trying to upload a file to a repository but I need to pass it as a buffer first. The problem is my "getBatchFileBuffer" function is somehow continuing without waiting for it to resolve which make the "UploadFiles" to only get a promise and not the real object. TIA
const getBatchFileBuffer = file => {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onloadend = function(e) {
file[0].fileBuffer = e.target.result;
resolve(file);
};
reader.readAsArrayBuffer(file[0].fileObject);
});
};
getBatchFileBuffer(self.state.tempAttachment).then(function(
tempAttachment
) {
self.setState({ tempAttachment });
UploadFiles(
self.state.AppMainObject.ID,
getBatchFileBuffer(self.state.tempAttachment)
).then(function(UploadFilesFileURL) {
console.log(UploadFilesFileURL);
});
});
I think I know the solution, I was calling the "getBatchFileBuffer" twice. That was my mistake

function never ends - fileReader xlsx.js angularJS

I'm using the FileReader Object in combination with xlsx.js to import data from an Excel-Sheet into my AngularJS Web-App.
The function never gets ended, so I need to continue the script manually by invoking an empty dummy function check(){} manually by clicking on a button to continue my script. I don't know, why the script behaves like that.
Code of my controller:
$scope.fileChanged = function(files) {
var i,f;
f = files[0];
reader = new FileReader();
reader.onload = function(e) {
var data = e.target.result;
var workbook = XLSX.read(data, {type: 'binary'});
formData.data.teams = XLSX.utils.sheet_to_json(workbook.Sheets['latusch']);
var sheet_name_list = workbook.SheetNames;
var worksheet = workbook.Sheets['latusch'];
var result = {};
workbook.SheetNames.forEach(function(sheetName) {
var roa = XLSX.utils.sheet_to_row_object_array(workbook.Sheets[sheetName]);
if(roa.length > 0){
result[sheetName] = roa;
}
});
reader.readAsArrayBuffer(f);
}
readAsArrayBuffer is inside the onload callback, so it will not be called.
Remove it from the callback.

Backbone. Form with file upload, how to handle?

I want to organize the workflow only through the REST API. I have a form that allows to upload image (enctype="multipart/form-data"). How do I handle this form via backbone? Help me please, how I can to serialize it into JSON with a file field.
Thanks.
Vitaliy
If you are using HTML5, you can use the readAsDataURL method from the file api to read and store it on your models.
Here's the code i use to read and store.
var Image = Backbone.Model.extend({
readFile: function(file) {
var reader = new FileReader();
// closure to capture the file information.
reader.onload = (function(theFile,that) {
return function(e) {
//set model
that.set({filename: theFile.name, data: e.target.result});
};
})(file,this);
// Read in the image file as a data URL.
reader.readAsDataURL(file);
}
});
You could try the jquery.iframe.transport plugin.
IMHO, you cannot serialize a file into JSON.
If you need to send some data along with the file you can send them as query params with POST method.
www.example.com/upload?param1=value1&param2=value2
There's no good way to submit a file via AJAX. So I wrote a function to fake it--it inserts a secret iframe into your DOM that is never visible but still works as a target to submit your form on, and it installs a function for your response to call that cleans house when the file is uploaded.
Have your upload form's submit button fire this function I wrote. It uses jQuery because it's easy and nice, but in principle that shouldn't be strictly necessary:
function backgroundUpload(form, container) {
$(container).append('<iframe name="targetFrame" id="targetFrame" style="display: none; height: 0px; width:0px;" ></iframe>');
$(form).attr('target', 'targetFrame');
window.backgroundUploadComplete = function() {
//clear your form:
$(form).find(':file').val('');
$(form).find(':text').val('');
//do whatever you do to reload your screenful of data (I'm in Backbone.js, so:)
window.Docs.fetch().complete( function() { populateDocs(); });
//get rid of the target iframe
$('#targetFrame').remove();
};
$(form).submit();
}
Then have your form handler that does your file parsing and saving return the string:
<script>window.parent.backgroundUploadComplete();</script>
Your form can look like:
<form id="uploadForm" method="POST" action="/your/form/processor/url" enctype="multipart/form-data">
<input type="file" name="file"/>
<!-- and other fields as needed -->
<input type="button" onClick="backgroundUpload(this.form, $('#documents'));" value="Upload" />
</form>
(#documents is the div that this form lives in. Could probably be any DOM element, it just needs a home.)
events : {
"click #uploadDocument" : "showUploadDocumentDetails",
"change #documents" : "documentsSelected",
"click .cancel-document" : "cancelDocument"
},
showUploadDocumentDetails : function(event) {
$('#id-gen-form').attr("enctype","multipart/form-data");
$('#id-gen-form').attr("action",this.model.url);
var config = {
support : "image/jpg,image/png,image/bmp,image/jpeg,image/gif", // Valid file formats
form: "id-gen-form", // Form ID
dragArea: "dragAndDropFiles", // Upload Area ID
uploadUrl: this.model.url // Server side upload url
};
initMultiUploader(config);
if($('#uploadDocument').attr("checked")){
$('#id-documentCategory-div').show();
$('#id-documentName-div').show();
this.model.set({"uploadDocument": "YES"},{silent: true});
}
else{
$('#id-documentCategory-div').hide();
$('#id-documentName-div').hide();
this.model.set({"uploadDocument": "NO"},{silent: true});
}
},
cancelDocument : function(event) {
var targ;
if (!event) event = window.event;
if (event.target) targ = event.target;
else if (event.srcElement) targ = event.srcElement;
$('#' + event.target.id).parent().parent().remove();
var documentDetails = this.model.get("documentDetails");
documentDetails = _.without(documentDetails, _(documentDetails).find(function(x) {return x.seqNum == event.target.id;}));
this.model.set({
"documentDetails" : documentDetails
}, {
silent : true
});
},
documentsSelected : function(event) {
/*var targ;
if (!event) event = window.event;
if (event.target) targ = event.target;
else if (event.srcElement) targ = event.srcElement;
if (targ.nodeType == 3) // defeat Safari bug
targ = targ.parentNode;
var files = event.target.files; // FileList object
var html = [];
var documentDetails = [];
$(".files").html(html.join(''));
var _this = this;
_this.model.set({
"documentDetails" : documentDetails
}, {
silent : true
});
var seqNum = 0;
for(var i=0; i< files.length; i++){
(function(file) {
html.push("<tr class='template-upload' style='font-size: 10px;'>");
html.push("<td class='name'><span>"+file.name+"</span></td>");
html.push("<td class='size'><span>"+file.size+" KB <br/>"+file.type+"</span></td>");
//html.push("<td><div class='progress progress-success progress-striped active'style='width: 100px;' role='progressbar' aria-valuemin='0' aria-valuemax='100' aria-valuenow='0'><div class='bar' style='width:0%;'></div></div></td>");
if(LNS.MyesqNG.isMimeTypeSupported(file.type)){
if(!LNS.MyesqNG.isFileSizeExceeded(file.size)){
html.push("<td class='error' colspan='2'></td>");
var reader = new FileReader();
console.log(reader);
reader.onload = function(e) {
var targ;
if (!e) e = window.event;
if (e.target) targ = e.target;
else if (e.srcElement) targ = e.srcElement;
if (targ.nodeType == 3) // defeat Safari bug
targ = targ.parentNode;
console.log(e.target.result);
var content = e.target.result;
var document = new Object();
document.name = file.name;
document.type = file.type;
document.content = content;
document.seqNum = "document"+seqNum;
seqNum++;
documentDetails.push(document);
// _this.model.set({"documentDetails" : documentDetails},{silent:true});
};
reader.readAsDataURL(file, "UTF-8");
}else{
seqNum++;
html.push("<td class='error' colspan='2'><span class='label label-important'>Error</span> Too long</td>");
}
}else{
seqNum++;
html.push("<td class='error' colspan='2'><span class='label label-important'>Error</span> Not suported</td>");
}
html.push("<td><a id='document"+i+"' class='btn btn-warning btn-mini cancel-document'>Cancel</a></td>");
html.push("</tr>");
})(files[i]);
}
$(".files").html(html.join(''));*/
}
LNS.MyesqNG.isMimeTypeSupported = function(mimeType){
var mimeTypes = ['text/plain','application/zip','application/x-rar-compressed','application/pdf'];
if($.inArray(mimeType.toLowerCase(), mimeTypes) == -1) {
return false;
}else{
return true;
}
};
LNS.MyesqNG.isFileSizeExceeded = function(fileSize) {
var size = 2000000000000000000000000000;
if(Number(fileSize) > Number(size)){
return true;
}else{
return false;
}
};
Use this, it can work but not more than 5 MB file
Based on Anthony answer (https://stackoverflow.com/a/10916733/2750451), I've written a solution in coffeescript based on a defer object.
readFile: (file) =>
def = $.Deferred()
reader = new FileReader()
reader.onload = (ev) =>
def.resolve
name: file.name
binary: ev.target.result
reader.onerror = ->
def.reject()
reader.readAsDataURL(file)
def.promise()
Then, you could use it this way
readFile(file)
.done (parsedFile) =>
# do whatever you want with parsedFile
#model.set
image_name: parsedFile.name
image: parsedFile.binary
#model.save
.fail ->
console.log "readFile has failed"
To handle it on the server side (because it's Base64 encoded), here the solution in RoR (based on https://stackoverflow.com/a/16310953/2750451)
my_object.image = decode_image(params[:image])
my_object.image.name = params[:image_name]
def decode_image(encoded_file)
require 'base64'
image_data_string = split_base64(encoded_file)[:data]
Base64.decode64(image_data_string)
end
def split_base64(uri)
if uri.match(%r{^data:(.*?);(.*?),(.*)$})
return {
type: $1, # "image/png"
encoder: $2, # "base64"
data: $3, # data string
extension: $1.split('/')[1] # "png"
}
end
end
To ellaborate on Anthony Chua's answer. You need to add Image handling to Backbone.Form.editors like
Backbone.Form.editors.Image = Backbone.Form.editors.Text.extend({
tagName: 'div',
events: {
'change input[type=file]': 'uploadFile',
'click .remove': 'removeFile'
},
initialize: function(options) {
_.bindAll(this, 'filepickerSuccess', 'filepickerError', 'filepickerProgress');
Backbone.Form.editors.Text.prototype.initialize.call(this, options);
this.$input = $('<input type="hidden" name="'+this.key+'" />');
this.$uploadInput = $('<input type="file" name="'+this.key+'" accept="image/*" />');
this.$loader = $('<p class="upload-status"><span class="loader"></span> please wait..</p>');
this.$error = $('<p class="upload-error error">Error</p>');
this.$list = $('<ul class="file-list">');
},
// return an array of file dicts
getValue: function() {
var val = this.$input.val();
return (val ? JSON.parse(val) : [])[0].value;
},
setValue: function(value) {
var str, files = value;
if (_(value).isObject()) {
str = JSON.stringify(value);
} else {
files = value ? JSON.parse(value) : [];
}
this.$input.val(str);
this.updateList(files);
},
render: function(options) {
Backbone.Form.editors.Text.prototype.render.apply(this, arguments);
this.$el.append(this.$input);
this.$el.append(this.$uploadInput);
this.$el.append(this.$loader.hide());
this.$el.append(this.$error.hide());
this.$el.append(this.$list);
return this;
},
uploadFile: function() {
var fileInput = this.$uploadInput.get(0);
var fileObj = fileInput.files[0]
var reader = new FileReader();
var that = this;
// closure to capture the file information.
reader.onload = function(file){
var dataURL = reader.result;
var fileValue = {
value: dataURL,
name: fileObj.name,
content_type: fileObj.type
}
that.filepickerSuccess(fileValue);
};
// Read in the image file as a data URL.
reader.readAsDataURL(fileObj);
},
filepickerSuccess: function(files) {
console.log('File (raw)', files);
this.$loader.hide();
this.$error.hide();
this.$uploadInput.val('');
// when uploading one file, it returns just an object
if (!_(files).isArray()) { files = [files]; }
// turn response array into a flatter array of objects
var newFiles = _(files).map(function(file, index) {
return {
url: "#",
value: file.value,
filename: file.name,
key: index,
content_type: file.type
};
});
console.log('File (processed)', newFiles);
this.setValue(newFiles);
},
filepickerError: function(msg) {
console.debug('File error', msg);
this.$loader.hide();
this.$error.show();
},
filepickerProgress: function(percent) {
this.$loader.show();
this.$error.hide();
},
updateList: function(files) {
// this code is currently duplicated as a handlebar helper (I wanted to let this
// backbone-forms field stand on its own)
this.$list.empty();
_(files).each(function(file) {
var a = $('<a>', {
target: '_blank',
href: file.url,
text: file.filename + ' (' + file.content_type + ') '
});
var li = $('<li>').append(a);
li.append(a, ' ', $('<i class="icon-remove"></i>').data('key', file.key));
this.$list.append(li);
}, this);
this.$list[files.length ? 'show' : 'hide']();
},
removeFile: function(ev) {
if (ev) ev.preventDefault();
var files = this.getValue();
this.setValue([]);
}
});
You can use above code as follows
var ImgSlot = Backbone.Model.extend({
defaults: {
},
schema: {
imageField: {
type: "Image"
}
}
})
Render form using:
this.form = new Backbone.Form({
model: new ImgSlot(),
submitButton: "Example Image file input handling"
}).render();
var errors = that.form.commit({validate: true})
if(errors != null)
{
return false;
}
var data = that.form.model.attributes;
console.debug(data.imageField); // Will return base64 of image selected.
It is not possible to submit a file over AJAX before HTML5 (including IE9).
You need to sync the model attributes over ajax, and then send another html form post with the file, and then sync them up somehow. Generally, save the model over ajax, get an id back, add the id to the other form, and then post the file.
The jQuery plug in "jquery.form" may help you to construct a form to post the file. It manages the "hidden iframe trick" so that it looks like AJAX to the end user.
You might just need to spend some time googling "hidden iframe trick" ...

Resources