I am trying to write unittests for a loopback model using jasmine. My model has the usual CRUD endpoints but I have defined a custom '/products/:id/upload' endpoint which expects a form with files.
My model looks like
'use strict';
var loopback = require('loopback');
var ProductSchema = {
location: {
type: String,
required: true
},
version: {
type: String,
required: true
},
id: { type: Number, id: 1, generated: true }
};
var opts = {
strict: true
};
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
var Product = dataSource.createModel('Product', ProductSchema, opts);
Product.beforeRemote('upload', function(ctx){
var uploader = function(req, res){
// parse a multipart form
res({
result:'success'
});
};
function createProduct(uploaderResult){
// create a product out of the uploaded file
ctx.res.send({
result: uploaderResult.result
});
}
uploader.upload(ctx.req, createProduct);
});
Product.upload = function () {
// empty function - all the logic takes place inside before remote
};
loopback.remoteMethod(
Product.upload,
{
accepts : [{arg: 'uploadedFiles', http: function(ctx){
return function() {
return { files : ctx.req.body.uploadedFiles, context : ctx };
};
}},
{arg: 'id', type: 'string'}],
returns : {arg: 'upload_result', type: String},
http: {path:'/:id/upload', verb: 'post'}
}
);
module.exports = Product;
My end goal is to test the logic of the "createProduct".
My test looks like
'use strict';
describe('Product Model', function(){
var app = require('../../app');
var loopback = require('loopback');
var ProductModel;
beforeEach(function(){
app = loopback();
app.boot(__dirname+'/../../'); // contains a 'models' folder
ProductModel = loopback.getModel('Product');
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
ProductModel.attachTo(dataSource);
});
it('should load file ', function(){
console.log(ProductModel.beforeRemote.toString());
console.log(ProductModel);
ProductModel.upload();
});
});
By calling ProductModel.upload(); I was hoping to trigger the before remote hook which would exercise the the createProduct. I could test "createProduct" in isolation but then I would omit the fact that createProduct ends up being called as a result of upload.
To be perfectly clear, the core question is:
How do I exercise remote method hooks inside unittests ?
It was suggested to use supertest as an http server. Below there is a code snippet illustrating how to do it in jasmine
describe('My product suite', function(){
var request = require('supertest');
var app;
beforeEach(function(){
app = loopback();
// don't forget to add REST to the app
app.use(app.rest());
});
it('should load file', function() {
request(app).post('/products/id-of-existing-product/upload')
.attach('file', 'path/to/local/file/to/upload.png')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
// res is the HTTP response
// you can assert on res.body, etc.
});
});
});
Related
I've created two components to send an image in base-64 encoded format to a server. When the parent component is mounted it's supposed to set the child reference to file.
Vue.component('some-form', {
template: '#some-form',
data: function() {
return {
logoImage: '',
coverImage: ''
}
},
methods: {
onSubmit: function(event) {
var dataForm = {};
var that = this;
dataForm['logo-image'] = this.logoImage;
dataForm['cover-image'] = this.coverImage;
// AJAX REQUEST HERE with posting data
},
},
mounted: function(){
var $this = this;
// AJAX REQUEST HERE with getting data
}
});
Vue.component('upload-photo', {
template: '#upload-photo',
data: function () {
return {
image: {
body: '',
'content-type': '',
'content-length': '',
url: ''
},
imageBody: ''
}
},
props: ['logoImage', 'title', 'description'],
watch: {
'image': function() {
this.$emit('input', this.image);
}
},
created: function(){
this.image = this.logoImage;
},
mounted: function () {
var that = this;
//AJAX REQUEST HERE to get data
},
methods: {
onFileChange: function(e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length)
return;
this.createImage(files[0]);
},
createImage: function(file){
var image = new Image();
var reader = new FileReader();
var vm = this;
vm.image = {};
reader.onload = function(e) {
vm.image.body = e.target.result;
vm.imageBody = e.target.result;
};
vm.$set(vm.image, 'content-type', file.type);
vm.$set(vm.image, 'content-length', file.size);
reader.readAsDataURL(file);
},
removeImage: function (e) {
this.image = '';
}
}
});
var app = new Vue({
el: '#app',
data: function() {
},
methods: {
},
mounted: function() {
}
});
Full example https://codepen.io/anon/pen/ZvzwzO
How can it be implemented?
P.S. I have no idea how to implement it in the same component. I send data as a string with two more property, however get as a string to, however it's link.
P.S.S. need just way to search.
It is difficult to tell exactly what you are asking but it sounds like you want to pass data from the parent component to the child. If you haven't already, read about Composing components and Dynamic Props for passing properties from a parent component to a child component.
One way to do this is to make the imageBody a property of the upload-photo component instead of part of the data.
props: ['logoImage', 'title', 'description', 'imageBody'],
Then have the parent supply a value for that property:
<upload-photo v-model="logoImage" title="TITLE 1" description="description_1" v-bind:image-body="imageBody">
Take a look at this phpfiddle. When the form is mounted, it sends an AJAX call back to the server to retrieve a URL, then sets the property on that first upload-photo child element to the URL sent back from the server in the AJAX response. Note that the upload-photo template was changed to show the image if imageBody is truthy instead of image.
I need to pass an arguments in methods using ajax axios.
var app = new Vue({
el: '#app',
data: {
urlAdmission:
admissions: [
{ name : 'asdf'},
{ name : 'sd'}
]
},
mounted: function(){
this.allAdmissions()
},
methods: {
allAdmissions: _.debounce( function(){
var app = this
axios.get('http://localhost/school/api/hello')
.then( function(response ){
app.admissions = response.data.admissions
})
.catch( function(error){
console.log(error)
})
})
}
});
As you can see in mounted I call the methods this.allAdmissions() I need to pass an argument so that I can reuse the function. For example this.allAdmissions('http://localhost/school/api/hello'). Then use it in axios.get('url'). Thanks
It looks like what you're trying to do is make a function that can accept a url and bind the results of the url to a variable value in your data. Here is how you might do that.
methods: {
allAdmissions: _.debounce(function(url, value){
axios.get(url)
.then(function(response){
this[value] = response.data.admissions
}.bind(this))
.catch(function(error){
console.log(error)
})
})
}
Then, if you call that method like this,
this.allAdmissions('http://localhost/school/api/admissions', "admissions")
allAdmissions will set the admissions property on your data to the result of your call. This works if you always want to use response.data.admissions because you hardcoded that. If you wanted that to be variable as well, you might pass in a third value like so
methods: {
getSomeData: _.debounce(function(url, value, responseValue){
axios.get(url)
.then(function(response){
this[value] = response.data[responseValue]
}.bind(this))
.catch(function(error){
console.log(error)
})
})
}
In case some will need multiple ajax request. Here is an example.
var app = new Vue({
el: '#app',
data: {
value: '',
admissions: [],
schoolyear: []
},
created: function(){
this.ajaxAll()
},
methods: {
ajaxAll: _.debounce( function(){
var app = this
var admissions = 'admissions'
var schoolYear = 'schoolyear'
axios.all([this.getAllData('http://localhost/school/api/admissions', 'admissions'), this.getAllData('http://localhost/school/api/schoolyear', 'schoolyear')]);
}),
getAllData: function(url, value){
var app = this
return axios.get(url)
.then(function(response){
app[value] = response.data[value]
console.log(response.data.admissions)
})
}
}
})
Credit to #Bert Evans.
POST-EDIT: I've just solved the issue, though maybe someone has a better solution. I'll post my solution soon, but if someone has a correct solution I'll accept their answer.
I'm migrating my application from 1.4 to 1.5 and changing all my controllers and directives to components. I now have a test that once worked not working and I'd like some guidance.
I'm trying to spy on a service method and according to the unit test it's not calling. This is not the case as an API call is made when the application is run. Here is the message I receive:
This is my component file:
(function(){
"use strict";
angular.module("app").component("profileComponent", {
templateUrl: "/templates/profile.component.html",
controllerAs: "vm",
bindings: {
resolvedUser: "<"
},
controller: function(ImageService, $state){
const vm = this;
const resolvedUser = this.resolvedUser;
resolvedUser ? vm.user = resolvedUser : $state.go("404");
vm.$onInit = function(){
ImageService.findByName(vm.user.pokemon.name)
.then(function(res){
vm.user.pokemon.id = res.id;
vm.user.pokemon.image = res.sprites.front_default;
vm.user.pokemon.type = res.types[0].type.name;
})
.catch(function(res){
vm.user.pokemon.image = "https://www.native-instruments.com/forum/data/avatars/m/328/328352.jpg?1439377390";
});
}
}
});
})();
And here is the relevant parts from my spec file. I've made a comment where the test is failing:
describe("profile.component", function(){
var profileComponent, ImageService, $q, $httpBackend, $state, resolvedUser, jazzSpy, IS,
API = "http://pokeapi.co/api/v2/pokemon/";
var RESPONSE_SUCCESS = // very large variable I've omitted for brevity.
beforeEach(angular.mock.module("app"));
beforeEach(angular.mock.module("ui.router"));
beforeEach(inject(function(_ImageService_, _$q_, _$httpBackend_, _$state_, _$rootScope_){
ImageService = _ImageService_;
$q = _$q_;
$httpBackend = _$httpBackend_;
$state = _$state_;
$rootScope = _$rootScope_;
$rootScope.$new();
}));
describe("profileComponent with a valid user and valid Pokemon", function(){
beforeEach(inject(function(_$componentController_){
singleUser = { id: 2, name: "Erlich Bachman", email: "erlich#aviato.com", phone: 4155552233, pokemon: { isPresent: true, name: "celebi"}, icon: { isPresent: false, name: null} };
let bindings = {resolvedUser: singleUser, ImageService: ImageService, $state: $state };
profileComponent = _$componentController_("profileComponent", { $scope: {} }, bindings);
profileComponent.$onInit();
}));
beforeEach(function(){
spyOn(ImageService, "findByName").and.callThrough();
});
it("should set state to resolvedUser", function(){
expect(profileComponent.user).toEqual(singleUser);
});
it("should expect ImageService to be defined", function(){
expect(ImageService.findByName).toBeDefined();
});
it("should call ImageService.findByName() and return Pokemon icon", function(){
expect(profileComponent.user.pokemon.name).toEqual("celebi");
$httpBackend.whenGET(API + "celebi").respond(200, $q.when(RESPONSE_SUCCESS));
$httpBackend.flush();
// This is where the test fails
expect(ImageService.findByName).toHaveBeenCalledWith("celebi");
});
});
As mentioned before, $onInit needs to be called after spyOn:
beforeEach(function(){
spyOn(ImageService, "findByName").and.callThrough();
profileComponent.$onInit();
});
Below are what I believe are the relevant files:
compareGroup model
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* CompareGroup Schema
*/
var CompareGroupSchema = new Schema({
created: {
type: Date,
default: Date.now
},
optionName: {
type: String,
default: '',
trim: true
},
optionAttributes: [{
attributeName: String,
optionImportance: Number,
optionScore: Number,
optionuom: String
}]
});
mongoose.model('CompareGroup', CompareGroupSchema);
compareGroup route
'use strict';
var index = require('../controllers/compareGroups');
module.exports = function(app) {
app.post('/compareGroups', compareGroups.create);
};
backend controller
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
CompareGroup = mongoose.model('CompareGroup'),
_ = require('lodash');
exports.create = function(req, res) {
var CompareGroup = new CompareGroup(req.body);
compareGroup.save(function(err) {
if (err) {
return res.send('users/signup', {
errors: err.errors,
compareGroup: compareGroup
});
} else {
res.jsonp(compareGroup);
}
});
};
And my front end controller
'use strict';
angular.module('mean.compareGroups').controller('CompareGroupsController', ['$scope', '$stateParams', '$location', 'Global', 'CompareGroups', function ($scope, $stateParams, $location, Global, CompareGroups) {
$scope.global = Global;
$scope.create = function() {
var compareGroup = new CompareGroups({
//"this" is the same thing as scope
optionName: this.optionName,
optionAttributes: this.optionName.optionAttributes,
attributeName: this.optionAttributes.attributeName,
optionImportance: this.optionAttributes.optionImportance,
optionScore: this.optionAttributes.optionScore,
optionuom: this.optionAttributes.optionuom
});
compareGroup.$save(function(response) {
$location.path('compareGroups/');
});
};
}]);
When I try to run this code, my terminal says: ".../Mean/app/routes/compareGroups app.post('/compareGroups', compareGroups.create);
^
ReferenceError: compareGroups is not defined."
Let me know what other information you need. Thanks!
In your route file change this:
var index = require('../controllers/compareGroups');
to this:
var compareGroups = require('../controllers/compareGroups');
then you'll be able to call the controller properly.
I am rather new to backbone and wanted to test a simple script that handles a to do list. Here is the code i used so far:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {}
};
window.template = function(id) {
return _.template($('#' + id).html());
}
App.Models.Task = Backbone.Model.extend({
validate: function(attributes) {
if ( !$.trim(attributes.title) ) {
return 'Invalid title';
}
}
});
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task
});
App.Views.Task = Backbone.View.extend({
tagName: 'li',
template: template('taskTemplate'),
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .edit': 'editTask',
'click .delete': 'destroy'
},
destroy: function() {
if (confirm('Are you sure?')) {
this.model.destroy();
}
},
remove: function() {
this.$el.remove();
},
editTask: function() {
var newTaskTitle = prompt('New title:', this.model.get('title'));
this.model.set('title', newTaskTitle, {validate: true});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
App.Views.AddTask = Backbone.View.extend({
el: 'form#addTask',
initialize: function() {
},
events: {
'submit': 'submit'
},
submit: function(event) {
event.preventDefault();
var newTaskTitle = $(event.currentTarget).find('input[type=text]').val();
var task = new App.Models.Task({ title: newTaskTitle });
this.collection.add(task, {add: true, merge: false, remove: false});
}
});
App.Views.Tasks = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('add', this.addOne, this);
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.Task({ model: task });
this.$el.append(taskView.render().el);
}
});
var tasks = new App.Collections.Tasks([
{
title: 'Go to store',
priority: 4
},
{
title: 'Go to mall',
priority: 3
},
{
title: 'Get to work',
priority: 5
}
]);
var addTaskView = new App.Views.AddTask({ collection: tasks });
var tasksView = new App.Views.Tasks({ collection: tasks });
$('div.tasks').append(tasksView.render().el);
})();
So the model validation works fine ... the only pb is that collection.add does not validate the newly added model .... is the a way to force the validation?
Thanks,
Rares
From the fine manual:
validate model.validate(attributes, options)
[...] By default validate is called before save, but can also be
called before set if {validate:true} is passed.
Collection#add does not call save nor does it call set with the validate: true option. If you want to validate during add, say so:
collection.add(models, { validate: true });
That will get validate:true all that way down to Model#set.
A quick look at a simplified example may be helpful:
var M = Backbone.Model.extend({
set: function() {
console.log('setting...');
Backbone.Model.prototype.set.apply(this, arguments);
},
validate: function() {
console.log('validating...');
return 'Never!';
}
});
var C = Backbone.Collection.extend({
model: M
});
var c = new C;
c.on('add', function() {
console.log('Added: ', arguments);
});
c.on('invalid', function() {
console.log('Error: ', arguments);
});
Now if we do this (http://jsfiddle.net/ambiguous/7NqPg/):
c.add(
{ where: 'is', pancakes: 'house?' },
{ validate: true }
);
You'll see that set is called with validate: true, validate will be called, and you'll get an error. But if you say this (http://jsfiddle.net/ambiguous/7b2mn/):
c.add(
{ where: 'is', pancakes: 'house?' },
{add: true, merge: false, remove: false} // Your options
);
You'll see that set is called without validate: true, validate will not be called, and the model will be added to the collection.
The above behavior is quite strongly implied but not explicitly specified so you may not want to trust it. Model#initialize does say:
you can pass in the initial values of the attributes, which will be set on the model.
and set does explicitly mention the validate option. However, there is no guarantee that Collection#add will send options to the model constructor or set or that the model's constructor will send the options to set. So if you want to be really paranoid and future proof, you could add a quick check for this "options get all the way down to set" behavior to your test suite; then, if it changes you'll know about it and you can fix it.
if you pass options to your collection add method, the validation method will not be called and as your arguments in this case are all set to the default value, there is not need to pass them
this.collection.add(task);
you may want to take a look at this question.
Prevent Backbone.js model from validating when first added to collection