How to jasmine test jQuery post ajax properties (e.g url) - javascript

I have this basic jQuery post to a url which I would like to test it. I do Sinon create server test already, but I'd like to test the url and properties given in the method as well, but I got error because jQuery.post(url, data).always(callback) cannot be called on undefined object.
And also what else should I be testing?
Here's my jQuery code.
define(['jquery'], function (jQuery) {
var ProductListingService = function () {
var self = this;
self.create = function (sku, quantity, price, callback) {
var url = '/url';
var data = { 'sku': sku, 'quantity': quantity, 'price': price };
jQuery.post(url, data).always(callback);
};
};
return ProductListingService;
});
This is my test
define(['service/ProductListingService'], function (ProductListingService) {
describe('ProductListingService', function () {
var productListingService, server;
beforeEach(function () {
productListingService = new ProductListingService();
server = sinon.fakeServer.create();
});
it('posts and call to /url', function () {
server.respondWith('POST', '/url',
[201, { "Content-Type": "application/json" }, '']
);
var callback = sinon.spy();
productListingService.create('sku', '1', 10.0, callback);
server.respond();
expect(callback.calledOnce).toBeTruthy();
});
it('posts and call to /url', function () {
spyOn(jQuery, "post").andCallFake();
productListingService.create('sku', '1', 10.0, sinon.spy());
expect(jQuery.post.mostRecentCall.args[0]["url"]).toEqual("/api/listing/create");
});
});
});

I think the easier solution is replacing sinon with Jasmine's ajax.js (http://jasmine.github.io/2.0/ajax.html):
define(['service/ProductListingService'], function (ProductListingService) {
describe('ProductListingService', function () {
var productListingService;
beforeEach(function () {
productListingService = new ProductListingService();
jasmine.Ajax.install();
});
afterEach(function() {
jasmine.Ajax.uninstall();
});
it('posts and call to /url', function () {
var callback = sinon.spy();
productListingService.create('sku', '1', 10.0, callback);
var request = jasmine.Ajax.requests.mostRecent();
request.response({
"status": 201,
"contentType": "application/json",
"responseText": ""
});
expect(request.method).toEqual("POST");
expect(request.url).toEqual("/url");
expect(callback.calledOnce).toBeTruthy();
});
it('posts and call to /url', function () {
productListingService.create('sku', '1', 10.0, sinon.spy());
expect(jasmine.Ajax.requests.mostRecent().url).toEqual("/api/listing/create");
});
});
});

Related

JS works with .include but fails with .extend

I made this JS to add a functionality on a form (backend) that computes a field when the event click is triggered. So far the code recomputes when I use ".include" but the whole JS in all views fail since I'm using ".include". When I try to use extend my code does nothing. Looks like Odoo doesn't add the extended code to the JS engine so my question is, what am I doing wrong here? Is there something else I need to add so my code works as extended?
odoo.define('med_care.TestRenderer', function (require) {
"use strict";
var viewRegistry = require('web.view_registry');
var FormRenderer = require('web.FormRenderer');
var FormView = require('web.FormView');
var TestFormRenderer = FormRenderer.extend({
events: _.extend({}, FormRenderer.prototype.events, {
'click .sign_selector': '_onSignSelectorClicked',
}),
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.fields = state.fields;
this._onSignSelectorClicked = _.debounce(this._onSignSelectorClicked, 300, true);
},
confirmChange: function (state, id, fields, e) {
var self = this;
if (state.model == 'med.test') {
return this._super.apply(this, arguments).then(function () {
self.canBeSaved(self.state.id);
});
}
},
_onSignSelectorClicked: function (event) {
this.state.data.telephone = '333';
if (this.state.model == 'med.test') {
var info_test = {
dataPointID: this.state.id,
changes: {telephone: '333'},
viewType: "form",
notifyChange: true
};
var odoo_event = this.trigger_up('field_changed', info_test);
this.confirmChange(this.state, this.state.id, "telephone",
odoo_event)
}
},
});
var TestFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Renderer: TestFormRenderer,
}),
});
viewRegistry.add('test_form', TestFormView);
return TestFormView;
});

Creating a jQuery container to hold other functions

I am trying to create a simple class or a container to hold few JavaScript methods as shown below.
var Core = (function () {
var Error = {
alert: function() {
alert('Error => alert called');
},
display: function() {
alert('Error => display called');
}
};
var ajax = {
view: function(){
alert('Ajax => view called');
},
update: function(){
alert('Ajax => update called');
}
};
var call = function(){
Error.alert();
Error.display();
ajax.view();
ajax.update();
};
$(document).ready(function(){
call(); // This works fine.
}());
But for some reason I am not able to call these methods outside the container. For instance calling functions as shown below throws error.
$(document).ready(function(){
Core.Error.alert(); // This doesn't work.
Core.Call(); // This doesn't work.
});
Error: Uncaught TypeError: Cannot read property 'Error' of undefined
I can call the functions from within the container. I am new to the concept of jQuery plugins and would appreciate if someone can help me with this.
You can export these methods (read more about Module Pattern), like so
var Core = (function () {
...
return {
Error: Error,
Ajax: ajax,
Call: call
};
})();
Core.Error.alert();
Core.Call();
Example
Change it in:
var Core = {
error: {
alert: function () {
alert('Error => alert called');
},
display: function () {
alert('Error => display called');
}
},
ajax: {
view: function () {
alert('Ajax => view called');
},
update: function () {
alert('Ajax => update called');
}
},
call: function () {
Core.error.alert();
Core.error.display();
Core.ajax.view();
Core.ajax.update();
}
}
$(document).ready(function () {
Core.call(); // This works
}());
Working fiddle

How to update Backbone JS model attribute?

UPDATE: I've updated my views to show how I resolved this question using information from the accepted answer.
I'd like to update/increment an attribute ('video_views') of my Backbone JS model via a click event from my view. But, as a Backbone rookie, I'm not sure how to accomplish this exactly.
I'd like the 'video_views' attribute to increment by one with the playVideo event (click).
Thanks for the help!
Here is the structure of my JSON from my API:
{
"id": 8,
"name": "Bike to work day",
"slug": "bike-work-day",
"tagline": "A brief tagline about the video.",
"description": "This is a test.",
"created": "2015-02-06T15:22:26.342658Z",
"website": "http://thevariable.com/",
"logo": "http://dev.thevariable.com/media/brands/logos/test_logo.jpeg",
"video": "http://dev.thevariable.com/media/brands/videos/3D463BC3-38B8-4A6F-BE93-3F53E918EC3B-3533-00000118880074BA_1.1.mp4",
"video_thumbnail": "http://dev.thevariable.com/media/brands/video_thumbnails/3D463BC3-38B8-4A6F-BE93-3F53E918EC3B-3533-00000118880074BA_1.1.mp4.jpg",
"links": {
"self": "http://dev.thevariable.com/api/brands/bike-work-day"
},
"status_display": "published",
"video_views": 0
}
Here are my Backbone views:
var TemplateView = Backbone.View.extend({
templateName: '',
initialize: function () {
this.template = _.template($(this.templateName).html());
},
render: function () {
var context = this.getContext(), html = this.template(context);
this.$el.html(html);
},
getContext: function () {
return {};
}
});
var HomePageView = TemplateView.extend({
templateName: '#home-template',
events: {
'click video': 'updateCounter',
'click .video video': 'playVideo',
'click .sound': 'muteVideo',
'click .js-open-card': 'openCard'
},
initialize: function (options) {
var self = this;
TemplateView.prototype.initialize.apply(this, arguments);
app.collections.ready.done(function () {
app.brands.fetch({success: $.proxy(self.render, self)});
});
},
getContext: function () {
return {brands: app.brands || null};
},
updateCounter: function (e) {
var id = $(e.currentTarget).data('id');
var item = self.app.brands.get(id);
var views = item.get('video_views');
var video = this.$('.video video');
// Only update the counter if the video is in play state
if (video.prop('paused')) {
item.save({video_views: views + 1}, {patch: true});
this.render();
}
},
playVideo: function () {
var video = this.$('.video video');
if (video.prop('paused')) {
video[0].play();
} else {
video.get(0).pause();
}
},
muteVideo: function (e) {
e.preventDefault();
var video = this.$el.parent().find('video');
video.prop('muted', !video.prop('muted'));
this.$('.sound').toggleClass('is-muted');
},
openCard: function (e) {
e.preventDefault();
this.$el.toggleClass('is-open');
this.$el.closest('.card-container').toggleClass('is-open');
}
});
And my Backbone models:
var BaseModel = Backbone.Model.extend({
url: function () {
var links = this.get('links'),
url = links && links.self;
if (!url) {
url = Backbone.Model.prototype.url.call(this);
}
return url;
}
});
app.models.Brand = BaseModel.extend({
idAttributemodel: 'slug'
});
var BaseCollection = Backbone.Collection.extend({
parse: function (response) {
this._next = response.next;
this._previous = response.previous;
this._count = response.count;
return response.results || [];
},
getOrFetch: function (id) {
var result = new $.Deferred(),
model = this.get(id);
if (!model) {
model = this.push({id: id});
model.fetch({
success: function (model, response, options) {
result.resolve(model);
},
error: function (model, response, options) {
result.reject(model, response);
}
});
} else {
result.resolve(model);
}
return result;
}
});
app.collections.ready = $.getJSON(app.apiRoot);
app.collections.ready.done(function (data) {
app.collections.Brands = BaseCollection.extend({
model: app.models.Brand,
url: data.brands
});
app.brands = new app.collections.Brands();
});
Just increment that attribute on the model and save it.
var views = model.get('video_views');
model.set({video_views: views + 1});
model.save();

GUI component for display async request states

Which interface or component do you suggest to display the state of parallel async calls? (The language is not so important for me, just the pattern, I can rewrite the same class / interface in javascript...)
I load model data from REST service, and I want to display pending label before the real content, and error messages if something went wrong... I think this is a common problem, and there must be an already written component, or best practices, or a pattern for this. Do you know something like that?
Here is a spaghetti code - Backbone.syncParallel is not an existing function yet - which has 2 main states: updateForm, updated. Before every main state the page displays the "Please wait!" label, and by error the page displays an error message. I think this kind of code is highly reusable, so I think I can create a container which automatically displays the current state, but I cannot decide what kind of interface this component should have...
var content = new Backbone.View({
appendTo: "body"
});
content.render();
var role = new Role({id: id});
var userSet = new UserSet();
Backbone.syncParallel({
models: [role, userSet],
run: function (){
role.fetch();
userSet.fetch();
},
listeners: {
request: function (){
content.$el.html("Please wait!");
},
error: function (){
content.$el.html("Sorry, we could not reach the data on the server!");
},
sync: function (){
var form = new RoleUpdateForm({
model: role,
userSet: userSet
});
form.on("submit", function (){
content.$el.html("Please wait!");
role.save({
error: function (){
content.$el.html("Sorry, we could not save your modifications, please try again!");
content.$el.append(new Backbone.UI.Button({
content: "Back to the form.",
onClick: function (){
content.$el.html(form.$el);
}
}));
},
success: function (){
content.$el.html("You data is saved successfully! Please wait until we redirect you to the page of the saved role!");
setTimeout(function (){
controller.read(role.id);
}, 2000);
}
});
}, this);
form.render();
content.$el.html(form.$el);
}
}
});
I created a custom View to solve this problem. (It is in beta version now.)
Usage: (Form is a theoretical form generator)
var content = new SyncLabelDecorator({
appendTo: "body",
});
content.load(function (){
this.$el.append("normal html without asnyc calls");
});
var User = Backbone.Model.extend({
urlRoot: "/users"
});
var UserSet = Backbone.Collection.extend({
url: "/users",
model: User
});
var Role = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'members',
relatedModel: User
}]
});
var administrator = new Role({id :1});
var users = new UserSet();
content.load({
fetch: [role, users],
sync: function (){
var form = new Form({
title: "Update role",
model: role,
fields: {
id: {
type: "HiddenInput"
},
name: {
type: "TextInput"
},
members: {
type: "TwoListSelection",
alternatives: users
}
},
submit: function (){
content.load({
tasks: {
save: role
},
sync: function (){
this.$el.html("Role is successfully saved.");
}
});
}
});
this.$el.append(form.render().$el);
}
});
Code:
var SyncLabelDecorator = Backbone.View.extend({
options: {
pendingMessage: "Sending request. Please wait ...",
errorMessage: "An unexpected error occured, we could not process your request!",
load: null
},
supported: ["fetch", "save", "destroy"],
render: function () {
if (this.options.load)
this.load();
},
load: function (load) {
if (load)
this.options.load = load;
this._reset();
if (_.isFunction(this.options.load)) {
this.$el.html("");
this.options.load.call(this);
return;
}
_(this.options.load.tasks).each(function (models, method) {
if (_.isArray(models))
_(models).each(function (model) {
this._addTask(model, method);
}, this);
else
this._addTask(models, method);
}, this);
this._onRun();
_(this.tasks).each(function (task) {
var model = task.model;
var method = task.method;
var options = {
beforeSend: function (xhr, options) {
this._onRequest(task, xhr);
}.bind(this),
error: function (xhr, statusText, error) {
this._onError(task, xhr);
}.bind(this),
success: function (data, statusText, xhr) {
this._onSync(task, xhr);
}.bind(this)
};
if (model instanceof Backbone.Model) {
if (method == "save")
model[method](null, options);
else
model[method](options);
}
else {
if (method in model)
model[method](options);
else
model.sync(method == "fetch" ? "read" : (method == "save" ? "update" : "delete"), model, options);
}
}, this);
},
_addTask: function (model, method) {
if (!_(this.supported).contains(method))
throw new Error("Method " + method + " is not supported!");
this.tasks.push({
method: method,
model: model
});
},
_onRun: function () {
this.$el.html(this.options.pendingMessage);
if (this.options.load.request)
this.options.load.request.call(this);
},
_onRequest: function (task, xhr) {
task.abort = function () {
xhr.abort();
};
},
_onError: function (task, xhr) {
this._abort();
this.$el.html(this.options.errorMessage);
if (this.options.load.error)
this.options.load.error.call(this);
},
_onSync: function (task, xhr) {
++this.complete;
if (this.complete == this.tasks.length)
this._onEnd();
},
_onEnd: function () {
this.$el.html("");
if (this.options.load.sync)
this.options.load.sync.call(this);
},
_reset: function () {
this._abort();
this.tasks = [];
this.complete = 0;
},
_abort: function () {
_(this.tasks).each(function (task) {
if (task.abort)
task.abort();
});
}
});

angular-ui/bootstrap: Testing a controller that uses a dialog

I've a controller that uses a Dialog from angular-ui/bootstrap:
function ClientFeatureController($dialog, $scope, ClientFeature, Country, FeatureService) {
//Get list of client features for selected client (that is set in ClientController)
$scope.clientFeatures = ClientFeature.query({clientId: $scope.selected.client.id}, function () {
console.log('getting clientfeatures for clientid: ' + $scope.selected.client.id);
console.log($scope.clientFeatures);
});
//Selected ClientFeature
$scope.selectedClientFeature = {};
/**
* Edit selected clientFeature.
* #param clientFeature
*/
$scope.editClientFeature = function (clientFeature) {
//set selectedClientFeature for data binding
$scope.selectedClientFeature = clientFeature;
var dialogOpts = {
templateUrl: 'partials/clients/dialogs/clientfeature-edit.html',
controller: 'EditClientFeatureController',
resolve: {selectedClientFeature: function () {
return clientFeature;
} }
};
//open dialog box
$dialog.dialog(dialogOpts).open().then(function (result) {
if (result) {
$scope.selectedClientFeature = result;
$scope.selectedClientFeature.$save({clientId: $scope.selectedClientFeature.client.id}, function (data, headers) {
console.log('saved.');
}, null);
}
});
};
});
I'm almost completely new to testing, and figured that maybe I need to test two things:
That a dialog opens when $scope.editClientFeature() is called
That $save is called successfully after a dialog is closed and returns a 'result'
My really messed up test now looks like this:
describe('ClientFeatureController', function () {
var scope, $dialog, provider;
beforeEach(function () {
inject(function ($controller, $httpBackend, $rootScope, _$dialog_) {
scope = $rootScope;
$dialog = _$dialog_;
//mock client
scope.selected = {};
scope.selected.client = {
id: 23805
};
$httpBackend.whenGET('http://localhost:3001/client/' + scope.selected.client.id + '/clientfeatures').respond(mockClientFeatures);
$controller('ClientFeatureController', {$scope: scope});
$httpBackend.flush();
});
});
it('should inject dialog service from angular-ui-bootstrap module', function () {
expect($dialog).toBeDefined();
console.log($dialog); //{}
});
var dialog;
var createDialog = function (opts) {
dialog = $dialog.dialog(opts);
};
describe('when editing a clientfeature', function () {
createDialog({});
console.log(dialog); //undefined
// var res;
// var d;
// beforeEach(function () {
// var dialogOpts = {
// template: '<div>dummy template</div>'
// };
// console.log(dialog);
// d = $dialog.dialog(dialogOpts);
// d.open();
// });
//
// it('should open a dialog when editing a client feature', function () {
// expect(d.isOpen()).toBe(true);
// });
});
});
The immediate problem now is that I'm unable to create/open a dialog. I get the following error:
Chrome 25.0 (Mac) ClientFeatureController when editing a clientfeature encountered a declaration exception FAILED
TypeError: Cannot call method 'dialog' of undefined
It would be great if someone has already written a test for a similar use case and can provide me with an example as I'm pretty lost.
Thanks,
Shaun
I was struggling with the same problem until right now, after trolling the the github repo i found the dialog tests and used that as a starting point :
var $dialog,$scope,$httpBackend;
beforeEach(module('ui.bootstrap.dialog'));
beforeEach(function(){
inject(function (_$dialog_, _$httpBackend_, $controller){
$dialog = _$dialog_;
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/appServer/list')
.respond([{
id:1,
name:'test1'
},
{
id:2,
name:'test2'
},
{
id:3,
name:'test3'
}]);
//setup controller scope
scope = {};
ServerCtrl = $controller('ServerCtrl', {
$scope: scope,
$dialog:$dialog
});
});
});
I also prefer a proper mock. When it is not available, i patch the service
To test this:
$dialog.messageBox(title, msg, btns)
.open()
.then(function (result) {
if (result == 'ok') {
// block executed if user click OK
}
});
You can patch $dialog like this:
$dialog.messageBox = function (title, msg, btns) {
return {
open: function () {
return {
then: function (callback) {
callback('ok'); // 'ok' will be set to param result
}
}
}
}
};
Personally I try to mock all services out. If the ui-bootstrap project does not provide a $dialog mock, you should open a bug ticket there and ask them for one. However creating one is as easy.
The mock service should have fake methods that do nothing but return promises. It should also give you a method to flush all asynchronous methods to make it easier to do synchronous testing.
I find it clearest to write my own mock of the dialog. Here's an example of mocking out a dialog to simulate "yes" being chosen.
Code under test
.controller('AdminListingCtrl', function AdminListingController($scope, $dialog, houseRepository) {
$scope.houses = houseRepository.query();
$scope.remove = function (house) {
var dlg = $dialog.messageBox('Delete house', 'Are you sure?', [
{label: 'Yep', result: 'yes'},
{label: 'Nope', result: 'no'}
]);
dlg.open().then(function (result) {
if (result == 'yes') {
houseRepository.remove(house.id);
$scope.houses = houseRepository.query();
}
});
};
}
Tests
describe('when deleting a house', function () {
var fakeDialog = {
open: function()
{
return {
then: function(callback) {
callback("yes");
}
};
}
};
beforeEach(inject(function($dialog) {
spyOn($dialog, 'messageBox').andReturn(fakeDialog);
}));
it('should call the remove method on the houseRepository', function () {
scope.remove({id: 99});
expect(houseRepository.remove).toHaveBeenCalledWith(99);
});
// etc
});

Categories

Resources