How do I write FileReader test in Jasmine? - javascript

I'm trying to make this test work, but I couldn't get my head around how to write a test with FileReader. This is my code
function Uploader(file) {
this.file = file;
}
Uploader.prototype = (function() {
function upload_file(file, file_contents) {
var file_data = new FormData()
file_data.append('filename', file.name)
file_data.append('mimetype', file.type)
file_data.append('data', file_contents)
file_data.append('size', file.size)
$.ajax({
url: "/upload/file",
type: "POST",
data: file_contents,
contentType: file.type,
success: function(){
// $("#thumbnail").attr("src", "/upload/thumbnail");
},
error: function(){
alert("Failed");
},
xhr: function() {
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){
myXhr.upload.addEventListener('progress',showProgress, false);
} else {
console.log("Upload progress is not supported.");
}
return myXhr;
}
});
}
return {
upload : function() {
var self = this,
reader = new FileReader(),
file_content = {};
reader.onload = function(e) {
file_content = e.target.result.split(',')[1];
upload_file(self.file, file_content);
}
}
};
})();
And this is my test
describe("Uploader", function() {
it("should upload a file successfully", function() {
spyOn($, "ajax");
var fakeFile = {};
var uploader = new Uploader(fakeFile);
uploader.upload();
expect($.ajax.mostRecentCall.args[0]["url"]).toEqual("/upload/file");
})
});
But it never gets to reader.onload.

The problem here is the use of reader.onload which is hard to test. You could use reader.addEventListener instead so you can spy on the global FileReader object and return a mock:
eventListener = jasmine.createSpy();
spyOn(window, "FileReader").andReturn({
addEventListener: eventListener
})
then you can fire the onload callback by yourself:
expect(eventListener.mostRecentCall.args[0]).toEqual('load');
eventListener.mostRecentCall.args[1]({
target:{
result:'the result you wanna test'
}
})

This syntax changed in 2.0. Code below gives an example based on Andreas Köberle's answer but using the new syntax
// create a mock object, its a function with some inspection methods attached
var eventListener = jasmine.createSpy();
// this is going to be returned when FileReader is instantiated
var dummyFileReader = { addEventListener: eventListener };
// pipe the dummy FileReader to the application when FileReader is called on window
// this works because window.FileReader() is equivalent to new FileReader()
spyOn(window, "FileReader").and.returnValue(dummyFileReader)
// your application will do something like this ..
var reader = new FileReader();
// .. and attach the onload event handler
reader.addEventListener('load', function(e) {
// obviously this wouldnt be in your app - but it demonstrates that this is the
// function called by the last line - onloadHandler(event);
expect(e.target.result).toEqual('url');
// jasmine async callback
done();
});
// if addEventListener was called on the spy then mostRecent() will be an object.
// if not it will be null so careful with that. the args array contains the
// arguments that addEventListener was called with. in our case arg[0] is the event name ..
expect(eventListener.calls.mostRecent().args[0]).toEqual('load');
// .. and arg[1] is the event handler function
var onloadHandler = eventListener.calls.mostRecent().args[1];
// which means we can make a dummy event object ..
var event = { target : { result : 'url' } };
// .. and call the applications event handler with our test data as if the user had
// chosen a file via the picker
onloadHandler(event);

I also faced similar problem and was able to achieve it without use of addeventlistener. I had used onloadend, so below is what I did.
My ts file had below code:-
let reader = new FileReader();
reader.onloadend = function() {
let dataUrl = reader.result;
// Some working here
};
reader.readAsDataURL(blob);
My spec file (test) case code :-
let mockFileReader = {
result:'',
readAsDataURL:(blobInput)=> {
console.log('readAsDataURL');
},
onloadend:()=> {
console.log('onloadend');
}
};
spyOn<any>(window, 'FileReader').and.returnValue(mockFileReader);
spyOn<any>(mockFileReader, 'readAsDataURL').and.callFake((blobInput)=> {
// debug your running application and assign to "encodedString" whatever
//value comes actually after using readAsDataURL for e.g.
//"data:*/*;base64,XoteIKsldk......"
mockFileReader.result = encodedString;
mockFileReader.onloadend();
});
This way you have mocked the FileReader object and returned a fake call to your own "readAsDataURL". And thus now when your actual code calls "reasAsDataURL" your fake function is called in which you are assigning an encoded string in "result" and calling "onloadend" function which you had already assigned a functionality in your code (.ts) file. And hence it gets called with expected result.
Hope it helps.

I think the best way is to use the real FileReader (don't mock it), and pass in a real File or Blob. This improves your test coverage and makes your tests less brittle.
If your tests don't run in IE, you can use the File constructor, e.g.
const fakeFile = new File(["some contents"], "file.txt", {type: "text/plain"});
If you need to be compatible with IE, you can construct a Blob and make it look like a file:
const fakeFile = new Blob(["some contents"]);
fakeFile.name = "file.txt";
fakeFile.type = "text/plain";
The FileReader can read either of these objects so there is no need to mock it.

i found easiest for myself to do next.
mock blob file
run reader.onload while in test environment.
as result - i do not mock Filereader
// CONTROLLER
$scope.handleFile = function (e) {
var f = e[0];
$scope.myFile = {
name: "",
size: "",
base64: ""
};
var reader = new FileReader();
reader.onload = function (e) {
try {
var buffer = e.target.result;
$scope.myFile = {
name: f.name,
size: f.size,
base64: XLSX.arrayBufferToBase64(buffer)
};
$scope.$apply();
} catch (error) {
$scope.error = "ERROR!";
$scope.$apply();
}
};
reader.readAsArrayBuffer(f);
//run in test env
if ( typeof jasmine == 'object') {reader.onload(e)}
}
//JASMINE TEST
it('handleFile 0', function () {
var fileContentsEncodedInHex = ["\x45\x6e\x63\x6f\x64\x65\x49\x6e\x48\x65\x78\x42\x65\x63\x61\x75\x73\x65\x42\x69\x6e\x61\x72\x79\x46\x69\x6c\x65\x73\x43\x6f\x6e\x74\x61\x69\x6e\x55\x6e\x70\x72\x69\x6e\x74\x61\x62\x6c\x65\x43\x68\x61\x72\x61\x63\x74\x65\x72\x73"];
var blob = new Blob(fileContentsEncodedInHex);
blob.type = 'application/zip';
blob.name = 'name';
blob.size = 11111;
var e = {0: blob, target: {result: {}}};
$scope.handleFile(e);
expect($scope.error ).toEqual("");
});

I struggled to figure out how to test onloadend when it gets called from readAsDataURL.
Here is a dump of what I ended up with.
Production code:
loadFileDataIntoChargeback(tempFileList) {
var fileNamesAndData = [];
for (var i = 0, f; f = tempFileList[i]; i++) {
let theFile = tempFileList[i];
var reader = new FileReader();
reader.onloadend = ((theFile) => {
return (fileData) => {
var insertionIndex = this.chargeback.fileList.length;
this.chargeback.fileList.push({ FileName: theFile.name, Data: fileData.target.result, FileType: theFile.type });
this.loadFilePreviews(theFile, insertionIndex);
}
})(f);
reader.readAsDataURL(f);
}
this.fileInputPath = "";
}
Test code:
describe('when the files are loaded into the chargeback', () => {
it('loads file previews', () => {
let mockFileReader = {
target: { result: '' },
readAsDataURL: (blobInput) => {},
onloadend: () => {}
};
spyOn(chargeback, "loadFilePreviews");
spyOn(window, 'FileReader').and.returnValue(mockFileReader);
spyOn(mockFileReader, 'readAsDataURL').and.callFake((blobInput) => {
mockFileReader.onloadend({ target: { result: "data:image/jpeg;base64,/9j/4QAYRXh" } });
});
var readFileList = chargeback.getArrayFromFileInput([getImageFile1()]);
chargeback.loadFileDataIntoChargeback(readFileList);
expect(chargeback.loadFilePreviews).toHaveBeenCalled();
});
});

Related

FileReader onload and assign value in loop?

i'm having difficulties on getting file reader content assignment. Is there anyway to wait the file reader finish onload and assign the file content before it push to the array?
I have a list of input file type button as below:
#for (int i = 0; i < #Model.LetterList.Count(); i++)
{
<tr>
<td>
<input type="file" id="LetterAttachment" name="LetterAttachment" accept="application/pdf">
</td>
</tr>
}
When i click submit, i want to assign the file content value into my form list in loop, below is my javascript code :
var attach=""; // empty variable for assign file content
function ApproverAction(action) {
var formList = [];
$("input[name='LetterAttachment']").each(function () {
if (this.files && this.files[0]) {
// I perform file reader here to assign the file content into attach....
var FR = new FileReader();
FR.onload = function (e) {
attach = e.target.result;
}
FR.readAsDataURL(this.files[0]);
var form = {
ID: newGuid(),
FileContents: attach, <<< ---- However it showing empty
DocumentName: this.files[0].name,
DocumentSize: this.files[0].size,
DocumentContentType: 'application/pdf',
SourceType: 'OnlineAssessment',
CreatedDate: '#DateTime.Now'
}
formList.push(form);
}
});
console.log(formList);
}
However i can't get the result quite correctly for output :
Any help and tips is highly appreciated! Thanks!
Use a promise for each file that resolves in the onload function and push those promises into an array
Then use Promise.all() to send the data once all promises have resolved. Note that the error handling will need to be improved depending on process flow you want
function ApproverAction(action) {
var filePromises = [];
$("input[name='LetterAttachment']").each(function() {
if (this.files && this.files[0]) {
// reference to this to use inside onload function
var _input = this;
var promise = new Promise(function(resolve, reject) {
var FR = new FileReader();
FR.onload = function(e) {
var form = {
ID: newGuid(),
FileContents: e.target.result;,
DocumentName: _input.files[0].name,
DocumentSize: _input.files[0].size,
DocumentContentType: 'application/pdf',
SourceType: 'OnlineAssessment',
CreatedDate: '#DateTime.Now'
}
// resolve promise with object
resolve(form);
}
FR.readAsDataURL(this.files[0]);
if(FB.error){
// needs more robust error handling, for now just reject promise
reject(FB.error)
}
// push promise to array
filePromises.push(promise)
}
});
}
});
// return a new promise with all the data
return Promise.all(filePromises)
}
Usage with promise returned from function
ApproverAction(action).then(function(formList){
// do something with the data array
console.log(formList);
}).catch(function(err){
console.error("Ooops something went wrong')
});
This is because the function you provided within the FR.onload is executed asynchronously. So, the code after it will be executed before the function is called thus the value of FileContents in the JSON is empty.
What you can do is either do all the stuff you want to do within the function or use some other function like readAsText.
...
var FR = new FileReader();
FR.readAsDataURL(this.files[0]);
var form = {
ID: newGuid(),
FileContents: FR.readAsText(this.files[0),
DocumentName: this.files[0].name,
DocumentSize: this.files[0].size,
DocumentContentType: 'application/pdf',
SourceType: 'OnlineAssessment',
CreatedDate: '#DateTime.Now'
}
...
Refer to this example for onLoad and readAsText.

How to access a local variable in filereader onload callback

I am doing a file upload operation in React, and I need to read the file uploaded from the user and do some state changes according to this file. What I have right now is shown below and I need to need to access the variable startInt within the onload callback, but it is still not defined here using the IIFE
const file = document.getElementById("fileUpload").files[0];
if (file) {
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = ((theFile) => {
const form = document.getElementById('fileUploadForm');
const start = datetimeToISO(form.Start.value);
const startInt = new Date(start).getTime();
return (e) => {
console.log(e.target.result);
//startInt is not defined here
}
})(file);
}
I followed this guide if it helps: https://stackoverflow.com/a/16937439/6366329
If you could point out my mistake that would be great. Many thanks in advance
you can access local var (but not class const like this.state.* or this .props.*).
so something like this you need:
var file = document.getElementById('inputID').files[0]
var Images = this.props.motherState.Images // Images is array
var reader = new FileReader();
reader.readAsDataURL(file); //
reader.onload = function () {
//console.log(reader.result);
if (file.type.match(/image.*/))
Images.push(reader.result) // its ok
// but this.props.motherState.Images.push(reader.result)
// return error like this:
// Images not define in this.props.motherState.Images
};
reader.onerror = function (error) {
//console.log('Error: ', error);
};

FileReader.onload can not return successful in time

I want to get the image file stream and pass them to the background like asp.net,but every time I try to fire the onload event ,it always accomplish after the programming passed.
I tried to use setTimeout to prevent it and let it processing and waiting it Success ,but it failed .
the comment of below explains which step I failed.thanks.
$("#goodsImage").change(function (e) {
if ($("#goodsImage").val() == "")
{
alert("please choose the image you want to upload");
return;
}
var filepath = $("#goodsImage").val();
//$("#goodsImage").val();
var extStart=filepath.lastIndexOf(".");
var ext=filepath.substring(extStart,filepath.length).toUpperCase();
if(ext!=".BMP"&&ext!=".PNG"&&ext!=".GIF"&&ext!=".JPG"&&ext!=".JPEG"){
alert("only images could be uploaded");
return;
}
readFile(e.target.files[0]);
setTimeout(function () {
//I want to use setTimeOut to delay it
}, 1000);
//always undefined!!!
if ($("#hidImageStream").val() != "")
{
$.post("#Url.Action("UploadGoodImage")", { localPath: readerRs }, function (e)
{
if (e) {
$("#ImagePreviewUrl").val(e.data);
}
else {
}
});
}
});
function readFile(file) {
var reader = new FileReader();
reader.onload = readSuccess;
//always success after post request accomplished.
function readSuccess(evt) {
document.getElementById("hidImageStream").
value = evt.target.result;
};
reader.readAsText(file);
}
here is a few tips
<-- use accept and only allow image mimetype. It can accept extension to -->
<input type="file" name="pic" accept="image/*">
Also reading the file as readAsText is a horrible idea for binary. (All doe i saw you change to base64).
readAsDataURL isn't that grate either since it's ~3x larger upload and needs more cpu/memory.
Spoofing the filename is very easy so best is to actually test if it's a image
$("#goodsImage").change(function() {
// We use `this` to access the DOM element instead of target
for (let file of this.files) {
// Test if it's a image instead of looking at the filename
let img = new image
img.onload = () => {
// Success it's a image
// upload file
let fd = new FormData
fd.append('file', file)
$.ajax({
url: 'http://example.com',
data: fd,
processData: false, // so jquery can handle FormData
type: 'POST',
success: function( data ) {
alert( data )
}
})
}
img.onerror = () => {
// only images dude
}
img.src = URL.createObjectURL(file)
}
})
I have solved the question ,just put the post request into the onloaded funciton .
function readFile(file) {
var reader = new FileReader();
reader.onloadend = readSuccess;
//reader.onloadend = function (e) {
// console.log(e.target.result);
//};
function readSuccess(evt) {
$.post("#Url.Action("UploadGoodImage")", { localPath: evt.target.result}, function (e) {
if (e.IsSuccess) {
$("#ImagePreviewUrl").val(e.data);
}
else {
alert("Fail!");
}
});
}
reader.readAsDataURL(file);
}

File reader execute multiple times in javascript

I'm trying to load images in to page for preview before uploading with javascript.
I have following code:
holder.onclick = function(event) {
function chooseFile(name) {
var chooser = $(name);
chooser.unbind('change');
chooser.change(function(evt) {
function loadFile(file, callback) {
var reader = new FileReader();
(reader.onload = function(file) {
console.log(f);
var output = document.createElement('input');
output.type = 'image';
output.classList.add('image-responsive');
output.classList.add('col-xs-12');
output.name = f;
output.id = f;
output.src = reader.result;
var x = document.getElementById('OrigName');
x.appendChild(output);
return callback(output);
})(f = file.name);
reader.readAsDataURL(file);
}
for (var i = 0; i < evt.target.files.length; i++) {
console.log(i);
var file = evt.target.files[i];
loadFile(file, function(output) {
// console.log(output);
});
}
});
chooser.trigger('click');
}
chooseFile('#fileDialog');
}
Problem is, whenever i load image, code inside reader.onload method execute twice, and in console i 2x result of console.log(f) and 2 errors that 'localhost/null is not found'.
When i remove (f=file.name), script execute as it should be, but then i don't have file.name variable inside reader scope.
EDIT:
Here's JSFiddle of my problem:
https://jsfiddle.net/onedevteam/udmz34z0/6/
Can someone help me fix this?
Problem is, whenever i load image, code inside reader.onload method execute twice
This is because in your code you have this.
(reader.onload = function(file) {
//...
//...
})(f = file.name); // <---- self executing function.
reader.readAsDataURL(file);
Here you are using "Self Executing function" for the reader.onload, So what happens is it will execute once when it hits this line of code, And again when reader.readAsDataURL(file) has completed reading. So remove the "self executing function " and you logic will run only once
When i remove (f=file.name), script execute as it should be, but then i don't have file.name variable inside reader scope.
to get the file name just add it in a variable and use it like this.
var fileName = file.name;
reader.onload = function() {
//...
//...
output.name = fileName ;
output.id = fileName ;
}; // <-- self executing function REMOVED
Also I feel there is no need to save the file name into a variable because the variable file passed into function is sufficient to get the job done. So below would be the final code as per my suggestion.
function loadFile(file, callback) {
var reader = new FileReader();
reader.onload = function() {
console.log(file.name); //
var output = document.createElement('input');
output.type = 'image';
output.classList.add('image-responsive');
output.classList.add('col-xs-12');
output.name = file.name; //
output.id = file.name; //
output.src = reader.result;
var x = document.getElementById('OrigName');
x.appendChild(output);
return callback(output);
};
reader.readAsDataURL(file);
}
You're calling reader.onload at least twice. You have this function inside another function loadFile(), and you call it immediately (which is why you only see this behavior when you have (f=file.name) there), but then also inside the chooser.change function you have that for-loop that calls loadFile(). Perhaps ou could set the file.name variable somewhere other than (f=file.name) and then make reader.onload not execute automatically.
The way you have your code structured, your onload handler will be executed twice, once when you define it, and then again when the "load" event fires. When you wrap a function definition inside parens:
(reader.onload = function (file) { ... })(f = filename)
you're saying "define this function and execute it immediately."
What you really want is a function that returns a function, like this:
function makeOnLoadHandler (filename) {
return function (file) {
// ... do whatever you need to with file and filename
};
}
reader.onload = makeOnLoadHandler(someFileName);
The outer function, makeOnLoadHandler(), creates a closure around your filename variable, and when the inner function handles the reader's load event, it will see the filename that you passed in when you called makeOnLoadHandler.

Error: Firebase.set failed: First argument contains undefined in property 'achv_img'

I followed this post
Having form using angular and firebase achievementsapp.firebaseapp.com, you can register and then if you click on the green plus it should pop the form, upload button is not working and when click on Add button it gives me this error Error: Firebase.set failed: First argument contains undefined in property 'achv_img'
controller code:
myApp.controller('MeetingsController',
function($scope, $rootScope, $firebase, Uploader,
CountMeetings, FIREBASE_URL) {
var ref = new Firebase(FIREBASE_URL + '/users/' +
$rootScope.currentUser.$id + '/meetings');
var meetingsInfo = $firebase(ref);
var meetingsObj = meetingsInfo.$asObject();
meetingsObj.$loaded().then(function(data) {
$scope.meetings = data;
}); //make sure meetings data is loaded
$scope.addMeeting = function() {
meetingsInfo.$push({
name: $scope.meetingname,
description: $scope.meetingdescription,
achv_type: $scope.achvtype,
achv_date: $scope.achvdate,
achv_img : $scope.achvimg,
date: Firebase.ServerValue.TIMESTAMP
}).then(function() {
$scope.meetingname='';
$scope.achvtype = '';
$scope.meetingdescription='';
$scope.achvdate='';
$scope.achvimg= $scope.meetingsInfoImgData;
});
Uploader.create($scope.meetingsInfo);
$scope.handleFileSelectAdd = function(evt) {
var f = evt.target.files[0];
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
var filePayload = e.target.result;
$scope.meetingsInfoImgData = e.target.result;
document.getElementById('pano').src = $scope.meetingsInfoImgData;
};
})(f);
reader.readAsDataURL(f);
};
document.getElementById('file-upload').addEventListener('change', $scope.handleFileSelectAdd, false);
}; //addmeeting
$scope.deleteMeeting = function(key) {
meetingsInfo.$remove(key);
}; //deleteMeeting
}); //MeetingsController
In reader.onload() you take the image data from the file and put it into $scope.meetingsInfoImgData:
reader.onload = (function(theFile) {
return function(e) {
var filePayload = e.target.result;
$scope.meetingsInfoImgData = e.target.result;
document.getElementById('pano').src = $scope.meetingsInfoImgData;
};
})(f);
My original answer then uses its equivalent of this $scope.meetingsInfoImgData later to populate the JavaScript object that it sends to Firebase:
$scope.episode.img1 = $scope.episodeImgData;
var episodes = $firebase(ref).$asArray();
episodes.$add($scope.episode);
Your code does nothing with $scope.meetingsInfoImgData when it tries to create the object in Firebase. You'll probably need something akin to $scope.episode.img1 = $scope.episodeImgData; in your code too.
If for some reason you can not fix it at the source, to make sure your object does not contain any undefined props use this simple trick:
JSON.parse( JSON.stringify(ObjectToSave ) )
For more info take a look at this codePen: http://codepen.io/ajmueller/pen/gLaBLX

Categories