I'm writing custom Angular directives for a new application and unit testing them using Jasmine. However, I can't for the life of me figure out how to get full code coverage (or even 80%) on the Kendo Grid Datasource.
I have a custom Angular Kendo grid directive that looks like this:
function customKendoGrid() {
return {
scope: {
hiPageSize: "="
},
template: "<div kendo-grid k-options='gridOptions' k-ng-delay='gridOptions'></div>",
controller: "hiKendoGridCtrl"
};
}
I did this so I could put a custom object on the scope for the grid directive. My controller looks like this:
function hiKendoGridCtrl($scope, $http, hiKendoGridSvc) {
var initialData = {
page: 1,
pageSize: 2,
type: "initial"
};
if(angular.isUndefined($scope.initialLoad)){
$scope.initialLoad = true;
}
var firstPageData = hiKendoGridSvc.getFirstPage(initialData);
firstPageData.then(function (result) {
var columnSet = result.ColumnSet;
var dataModel = result.Model;
var GridModel = kendo.data.Model.define(dataModel);
var firstPage = result.Data;
var totalResults = result.Total;
$scope.gridOptions = {
dataSource: {
schema: {
data: "Data",
total: function () { return totalResults; }, // NOT COVERED
model: GridModel
},
transport: {
read: function (options) {
if ($scope.initialLoad) {// NOT COVERED
$scope.initialLoad = false;// NOT COVERED
options.success({ Data: firstPage });// NOT COVERED
} else {
var requestData = {// NOT COVERED
page: options.data.page,
pageSize: options.data.pageSize,
type: "page"
};
$http({ method: 'POST', url: 'Home/DataSourceResult', data: requestData }).success(
function (data) {
options.success(data);// NOT COVERED
}).error(
function (data, status, headers, config) {
console.log(data);// NOT COVERED
console.log(status);// NOT COVERED
console.log(headers);// NOT COVERED
console.log(config);// NOT COVERED
});
}
}
},
serverPaging: true,
pageSize: $scope.hiPageSize
},
scrollable: true,
pageable: {
input: true,
numeric: false,
refresh: true
},
editable: true,
columns: columnSet,
sortable: true,
groupable: true
};
});
}
Explanation of above
An initial call is made to the server to get all grid configuration (schema, columns, first page of data, and total). All subsequent calls go to the same URL with different post parameters just retreiving a page of data from the server.
My problem is that I can't seem to find ways to traverse the code paths shown above as "NOT COVERED" in a comment.
I invoke both the grid and the controller in separate unit tests, but can't seem to get a Kendo Grid to compile and invoke the different paths above.
My current two tests for the controller and the directives are:
Controller
beforeEach(inject(function($http){
ctrl = $controller("hiKendoGridCtrl", {
$scope: $scope,
$http: $http,
hiKendoGridSvc: hiKendoGridSvcMOCK
});
$scope.$digest();
}));
And with that I can assert all kinds of things for the controller, including that the correct '$scope.gridOptions' are defined.
Directive Test
// I set up the scope as new rootScope and set compile = $compile in the beforeEach of this test.
it("should output the correct HTML", function () {
catchPOST.respond({
data : responseDataMOCK
});
element = '';
element = compile(element)(scope);
scope.$digest();
expect(element[0].innerHTML).toContain('');
}
But this does not catch the above descriptions in the controller above.
I have also tried something that I consider quirky, but something similar to:
// Execute the above directive test compiling my custom directive to a kendo directive
it("Should have the correct scope", function() {
var gridElement = '<div kendo-grid k-options="gridOptions"></div>';
gridElement = compile(gridElement)($scope);
console.log($scope);
console.log(gridElement);
});
So I thought perhaps I could get the GridOptions on scope and then compile the kendo directive manually, but this doesn't resuly in gridElement having any innerHTML at all.
So... the question is:
How can I add new tests/change existing tests to get full code coverage?
How could I/Should I change my code to make it more etstable? I'm hesitant to do this since it took A LOT of effort to get the grid working correctly with a dynamic configuration.
Thanks!
Related
I'd like to know a way of how to update the list values of a ExtJs ComboBox. For instance, I have two comboboxs.
One Combobox determine what values the another ComboBox should have. So, after selecting some of those,
I click the drowndown list (combobox) to see the values. But i dont get reflected.
change: function (combofirst, record) {
Ext.Ajax.request({
-- -- --
-- -- --
success: function (response) {
var combosecond = Ext.getCmp('defaultPackageType');
//I am unable to update the combosecond from below snippet.
combosecond.store = Ext.create('Ext.data.Store', {
fields: ['value', 'display'],
data: [
["N", "No"],
["A", "All accounts"]
] //json response
});
},
failure: function (record, action) {}
});
});
In short, how can I change the values of a ComboBox already has with ajax only.
Hope someone can help me
Thanks
I would also agree to the comment, that creating every time a new store and bind it to the combobox is not the optimal solution. I don't know really the reason why this should be done, but nevertheless here is a working example by using bindStore:
https://fiddle.sencha.com/#view/editor&fiddle/3ci0
Ext.create('Ext.form.field.ComboBox', {
// ...
listeners: {
change: {
fn: function (cb) {
Ext.Ajax.request({
url: 'https://jsonplaceholder.typicode.com/albums',
method: 'GET',
timeout: 60000,
success: function (response) {
var jsonResp = response.responseText;
let jsonObj = Ext.JSON.decode(jsonResp, true)
var combo2 = Ext.getCmp('myCombo2');
combo2.bindStore(Ext.create('Ext.data.Store', {
fields: ['id', 'title'],
data: jsonObj
}));
}
});
}
}
}
});
For selection of value 1 the data is loaded from a different url.
But I would think about whether a new proxy call is necessary and whether you can achieve your requirements by using filters or something else.
I'm having trouble binding my JavaScript kendo ui grid to model data from an action method. All the examples i see are mostly MVC wrappers and the JavaScript examples are all different and none seem to work for me.
Here is where i'm at below.
I did a generic test with static data that works.
var dataSource_Test = new kendo.data.DataSource({
data: [{ LeagueDetailGroupId: "15", GroupName: "Best Team 5"}]
});
Here is the datasource object im trying to create with the controller action:
var dataSource = new kendo.data.DataSource({
transport: {
read: {
url: "#Url.Action("LeagueDetailGroup_Read", "Configuration")?_leagueTypeId=" + leagueTypeId,
// i have tried all kinds of variants here, and not sure what to put
// my action method is returning json using kendo's DataSourceResult method
//contentType: "application/json",
type: "POST"
//dataType: "odata"
},
schema: {
data: "Data", // seen this in examples, dunno what it does
total: "Total", // seen this in examples, dunno what it does
model: {
id: "LeagueDetailGroupId",
fields: {
LeagueDetailGroupId: { editable: false, nullable: true },
GroupName: { validation: { required: true } }
}
}
},
// i seen this is an example from telerik but dont understand the use case for it
parameterMap: function (data, operation) {
// this prints no data before i even start so its a moot point configuring it from products to my stuff at this moment
// but not sure what todo here of if i need this anyways
console.log(data);
if (operation != "read") {
// post the products so the ASP.NET DefaultModelBinder will understand them
var result = {};
for (var i = 0; i < data.models.length; i++) {
var product = data.models[i];
for (var member in product) {
result["products[" + i + "]." + member] = product[member];
}
}
return result;
} else {
return JSON.stringify(data)
}
}
}
});
Here is the grid which works ok with the generic static datasouce object.
var grid = $("#leagueEdit_ldg_grid").kendoGrid({
dataSource: dataSource,
sortable: true,
pageable: true,
autobind: false,
//detailInit: leagueEdit_ldg_detailInit,
dataBound: function () {
this.expandRow(this.tbody.find("tr.k-master-row").first());
},
columns: [
{
field: "LeagueDetailGroupId",
title: "Group Id",
width: "110px"
},
{
field: "GroupName",
title: "Group Name",
width: "110px"
}
]
});
Delayed read, autobind set to false.
dataSource.read();
Here is my simplified Controller action. It runs and gets data, and works fine for my MVC wrapper grids.
[Route("LeagueDetailGroup_Read/{_leagueTypeId:int}")]
public ActionResult LeagueDetailGroup_Read([DataSourceRequest]DataSourceRequest request, int _leagueTypeId = -1)
{
DataSourceResult result =
_unitOfWork.FSMDataRepositories.LeagueDetailGroupRepository.Get(
ld => ld.LeagueTypeId == _leagueTypeId
)
.ToDataSourceResult(request,
ld => new LeagueDetailGroupViewModel
{
LeagueDetailGroupId = ld.LeagueDetailGroupId,
LeagueTypeId = ld.LeagueTypeId,
GroupName = ld.GroupName,
DateCreated = ld.DateCreated,
DateLastChanged = ld.DateLastChanged
}
);
// data looks fine here
return Json(result, JsonRequestBehavior.AllowGet);
}
Currently i'm getting this error:
Uncaught TypeError: e.slice is not a function
at init.success (kendo.all.js:6704)
at success (kendo.all.js:6637)
at Object.n.success (kendo.all.js:5616)
at i (jquery-3.1.1.min.js:2)
at Object.fireWith [as resolveWith] (jquery-3.1.1.min.js:2)
at A (jquery-3.1.1.min.js:4)
at XMLHttpRequest.<anonymous> (jquery-3.1.1.min.js:4)
It's hard to know without testing but let me know how this works.
Change your controller so that you are just returning a json string.
Also, try removing your schema and the parameter map, and set your dataType to json:
var dataSource = new kendo.data.DataSource({
transport: {
read: {
url: "#Url.Action("LeagueDetailGroup_Read", "Configuration")?_leagueTypeId=" + leagueTypeId,
dataType: "json"
}
}
});
For the grid I find simple json data does not usually need a schema/model defined. Kendo is super annoying and hard to debug. Let me know how it goes.
In my experience, an e.slice error happens when you have a record that has a null value in it somewhere. The kendo grid is not really smart enough to deal with this so you either have to make sure your datasource returns empty strings instead of nulls for string fields, or put a client template on the columns that translates a null into an empty string. It's possible that the kendo todatasourceresult made the problem come to light. Note that that is usually the last step before returning your dataset since it can modify the entity queries to give paging, so that you never query more than a single page of data (for ajax grids).
I've wrapped bootstrapTable (https://github.com/wenzhixin/bootstrap-table) into a directive, like this:
Vue.directive('bootstraptable', {
priority: 1000,
params: ['url', 'resource-name'],
bind: function () {
var _self = this;
$(this.el)
.bootstrapTable({
pagination: true,
pageSize: 15,
pageList: [],
sidePagination: 'server',
url: this.params.url,
queryParams: function (params) {
return params;
},
cookie: true,
cookieExpire: '24h',
cookieIdTable: this.params.resourceName + '-table',
locale: 'it-IT'
}).on('load-success.bs.table', function (e, data) {
$('[data-toggle="tooltip"]').tooltip();
_self.vm.$compile(_self.vm.$el);
});
},
update: function (value) {
$(this.el).val(value)
},
unbind: function () {
$(this.el).off().bootstrapTable('destroy')
}
});
The JSON returned from the server contains a button with a v-on directive so I have to recompile the injected HTML rows to have the button directives properly working.
Anyway, it seems that the following code isn't working:
_self.vm.$compile(_self.vm.$el);
Am I missing something obvious?
The $compile method needs to be called on the elements that have to be compiled, not on the vm root element.
I changed the line:
_self.vm.$compile(_self.vm.$el);
with:
_.each($('[recompile]'), function(el){
_self.vm.$compile(el);
});
and added the attribute "recompile" to all the HTML elements that need to be recompiled.
This seems to be working as expected, do not hesitate to answer if there is a more conventional way to do that.
Using ng-grid with server side sorting and paging. It works great, with one caveat: the initial rendering makes two calls to get data from my service.
I'm not sure how easy (or hard) this would be to replicate in a jsFiddle or plunker.
Here is my controller code:
function reportQueueController($scope, $location, reportDataService) {
function init() {
$scope.state = {};
}
$scope.setPagingData = function (data) {
$scope.reportQueueList = data.Data;
$scope.totalServerItems = data.TotalItems;
};
$scope.$watch('pagingOptions', function(newVal, oldVal) {
if (newVal === oldVal) return;
getPagedDataAsync();
}, true);
$scope.pagingOptions = {
pageSizes: [25, 50, 100, 'All'],
pageSize: 25,
currentPage: 1
};
$scope.$watch('gridOptions.ngGrid.config.sortInfo', function (newVal, oldVal) {
if (newVal === oldVal) return;
$scope.state.sortField = newVal.fields[0];
$scope.state.sortDirection = newVal.directions[0];
$scope.pagingOptions.currentPage = 1;
getPagedDataAsync();
}, true);
$scope.gridOptions = {
data: 'reportQueueList',
enablePaging: true,
enableRowSelection: false,
showFooter: true,
pagingOptions: $scope.pagingOptions,
totalServerItems: 'totalServerItems',
enableSorting: true,
useExternalSorting: true,
sortInfo: { fields: ['CustomerName'], directions: ['asc'] },
filterOptions: $scope.filterOptions,
columnDefs: [
{ field: 'CustomerName', displayName: 'Customer' },
{ field: 'ParentCustomerName', displayName: 'Parent' },
{ field: 'Name', displayName: 'Report Name' },
{ field: 'Emails', displayName: 'Email Recipients', cellTemplate: emailCellTemplate },
{ cellTemplate: editCellTemplate, width: '50px' }
]
};
function getPagedDataAsync() {
console.log('in get data'); //this get logged twice
reportDataService.getReportQueueList($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.state.emailAddress, $scope.state.reportSearch, $scope.state.sortField, $scope.state.sortDirection).then(function(data) {
$scope.setPagingData(data);
});
};
init();
}
Since Angular is going to call your watch at least twice, maybe more due to dirty processing, per $digest cycle you could use debounce. This is similar to what windows event listeners sometimes do. underscore (http://underscorejs.org) and lo-dash (http://lodash.com) both offer a _.debounce() you can use right out of the box.
_.debounce() allows you to say that a function should run, at most, once every per the specified number of milliseconds- no matter how many times the function is actually called. So you might do something like:
var checkSortData = _.debounce(function(e) {
$scope.state.sortField = newVal.fields[0];
$scope.state.sortDirection = newVal.directions[0];
$scope.pagingOptions.currentPage = 1;
getPagedDataAsync();
}, 500); // Run no more than once every 500 milliseconds
As you'd imagine underscore uses $timeout to do this, so you could write your own debounce if you preferred.
Using debounce could help with performance/server load too by minimizing server calls.
But rather than paying the performance price of polling the server to see if it has updated you might also consider using something like http://socket.io. Then you wouldn't have to poll using a watch, you can just attach an event listener on the client side. Here's an article on using socket.io with Angular written by Brian Ford: http://www.html5rocks.com/en/tutorials/frameworks/angular-websockets/
Your code looks correct, check if you are using unobtrusive js file twice, like
jquery.validate.unobtrusive.js
jquery.validate.unobtrusive.min.js
Or the same file is adding twice.
I have the following problem. I have a JS-file which has a handful of variables. Those I initialize in a function:
var currentYear;
var previousYear;
var urlQuarterDates;
var urlHalfYear;
var urlYear;
var urlMonth;
var urlProposalsSentAndReceived; //= '/Marketing/ListProposalsSentAndReceived';
var urlProposalsResponsibleMonth;
function initTabReportProposalsMonth(_currentYear, _previousYear, _urlViewProposal,
_urlQuarterDates, _urlHalfYear, _urlYear, _urlMonth, _urlProposalsSentAndReceived,
_urlProposalsResponsibleMonth) {
currentYear = _currentYear;
previousYear = _previousYear;
urlQuarterDates = _urlQuarterDates;
urlHalfYear = _urlHalfYear;
urlYear = _urlYear;
urlMonth = _urlMonth;
urlProposalsSentAndReceived = _urlProposalsSentAndReceived;
urlProposalsResponsibleMonth = _urlProposalsResponsibleMonth;
}
I have defined an event handler in the same JS-file:
function onPeriodSelect(combo, rec, i) {
var conn = new Ext.data.Connection();
var params = { };
switch(rec.get('myId'))
{
case _currentQuarter1:
conn.url = urlQuarterDates;
params.y = currentYear;
params.index = 1;
break;
}
reload(); //
}
The variables urlQuarterDates and currentYear are readily accessible. So far, so good...
I also have an ExtJs Grid with a data store which is declared inline:
var gridSentAndReceived = new Ext.grid.GridPanel({
title: 'Totaal',
autoHeight: true,
autoWidth: true,
store: new Ext.data.Store({
id: 'idStoreSentAndReceived',
proxy: new Ext.data.HttpProxy({ url: urlProposalsSentAndReceived,
timeout: 1800000 }),
reader: new Ext.data.JsonReader(
{
root: 'rows'
},
[
{ name: 'Status' },
{ name: 'nrOfProposals' },
{ name: 'TotalRevenueHardware' },
{ name: 'TotalRevenueYearly' },
{ name: 'TotalRevenueHours' }
]),
remoteSort: false
}),
frame: true,
iconCls: 'icon-grid',
columns: [
...
],
viewConfig: {
forceFit: true
}
});
The reload() function calls the load of the store of gridSentAndReceived. This generates an exception: the url is not defined at all. If I initialize the url right at its declaration (which is currently commented out' it works fine. When I browse using the debugger it shows that urlProposalsSentAndReceived is initialized. Still, it claims there is no URL.
This seems to be a scope problem, since variables are accessible from the event handler but obviously not elsewhere. Anybody knows how to fix it? The URLs are created using server tags and those cannot be put in JS files. I wouldn't enjoy putting them directly in the JS file as a text string. Is there a possible solution?
Update
I have tried a few more things but nothing works.
I have tried:
'beforeload': function (store, options) {
store.proxy.setUrl('/Marketing/ListProposalsSentAndReceived');
}
but even that didn't work. Still got the same exception. I really have no clue why that failed though, I took the code from the ExtJs Documentation under 'api'.
Now I have no choice but hardcoding the urls in my js-file though I'd very much prefer to use servertags and add them dynamically. Hopefully, one day, I'll find a solution rather than getting runtime errors when I change the location of a controller action.
This is not a scope issue. At the time you run your code urlProposalsSentAndReceived is not defined. If you set that variable via an event handler, the value is always set after gridSentAndReceived is initialized.