Since salesforce moved code coverage to developer console, I somehow didn't like it,waited for a while.... and again for like months and no go :( , Salesforce never brought back the old listviews, I was quite missing this little feature that lets you to have a quick glance of the code coverage. After few weeks finally planned to build something for myself.
This was a great chance to learn a thing or two about Tooling API and a chance to mix some js libraries together to brew something really cool.
After thinking for a while I think these are things I will need to build the page
- Tooling API : To bring the org code coverage
- JSRemoting : To bring data to page without the viewstate and in a fast manner
- JSRender : JsRender is jQuery Templating plugin that lets you create HTML from predefined templates
- VisualStrap : And the VisualStrap to generate a BootStrap responsive UI for both mobile and desktop
|
The page in action |
Tooling API
The page uses the Tooling API REST service to retrieve the code coverage result.
private static String sendToolingQueryRequest(String queryStr){
HttpRequest req = new HttpRequest();
req.setEndpoint(TOOLINGAPI_ENDPOINT+'query/?q='+queryStr);
/*Set authorization by using current users session Id*/
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
req.setHeader('Content-Type', 'application/json');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
return res.getBody();
}
To get the data from the endpoint the method sends the query along with the session id to get the response as JSON string which again used in the JS to render the UI.
{
"size": 1,
"totalSize": 1,
"done": true,
"records": [{
"attributes": {
"type": "ApexCodeCoverage",
"url": "/services/data/v29.0/tooling/sobjects/ApexCodeCoverage/71490000002RqOVAA0"
},
"NumLinesCovered": 2,
"ApexClassOrTriggerId": "01p90000001MTXTAA4",
"ApexClassOrTrigger": {
"attributes": {
"type": "Name",
"url": "/services/data/v29.0/tooling/sobjects/ApexClass/01p90000001MTXTAA4"
},
"Name": "jQueryUIBlockDemo_Con"
},
"NumLinesUncovered": 0
}],
"queryLocator": null,
"entityTypeName": "ApexCodeCoverage"
}
JSRemoting
JSRemoting does the job of bringing the data from controller
JSRender
JSRender takes the job of rendering the data received from the service and use them to generate the table. The templates are pretty easy to handle once you have the data and the decided upon the HTML structure, you can easily create them.
I wanted my page to look like a list so the obvious choice was a table and all the data received should be represented as row, and hence we need a template to generate the rows or "<tr>" for the table. In JSRender JSON data are binded by {{>MY_JSON_FIELDNAME}}
So the template should be
<script id="coverageRowTemplate" type="text/x-jsrender">
<tr>
<td width="20px">
<a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>
</td>
<td>
{{>ApexClassOrTrigger.Name}}
</td>
<td>
{{>NumLinesUncovered}}
</td>
<td>
{{>NumLinesCovered}}
</td>
</tr>
</script>
The above template just displays the data received from the remoting method, lets extend the template to show more info like percentage, totalNumber of lines and may be a background color ?
So to do that we will need some helper methods for the template
$.views.helpers({
calculatePercentage: function(NumLinesUncovered,NumLinesCovered){
return ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);
},
totalLines:function(NumLinesUncovered,NumLinesCovered){
return NumLinesUncovered + NumLinesCovered;
},
rowStatusClass: function(NumLinesUncovered,NumLinesCovered){
var sclass='danger';
var percentG = ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);
if(percentG >= 90){
sclass = 'success'
}
else if(percentG >= 75){
sclass = 'warning';
}
return sclass;
}
});
The above code piece defines some helper methods and register them so that they can be used with JSRender templates. So the final template will look like
<script id="coverageRowTemplate" type="text/x-jsrender">
<tr class="{{:~rowStatusClass(NumLinesUncovered,NumLinesCovered)}}">
<td width="20px">
<a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>
</td>
<td>
{{>ApexClassOrTrigger.Name}}
</td>
<td>
{{>NumLinesUncovered}}
</td>
<td>
{{>NumLinesCovered}}
</td>
<td>
{{:~totalLines(NumLinesUncovered,NumLinesCovered)}}
</td>
<td>
{{:~calculatePercentage(NumLinesUncovered,NumLinesCovered )}}
</td>
</tr>
</script>
Now the template serves most of the fields, to generate the HTML
var html = $( "#JSRENDER_TEMPLATEID" ).render(JSON_DATA );
and this html can be appended to an existing table in the page to generate a table with the data.
VisualStrap
VisualStrap is used to generate the Mobile friendly good looking responsive layout along with status classes displayed based on the code coverage percentage
- Above 90 : Green (css class = "success")
- Above 75 : Yellow (css class = "warning")
- For everything else : Red (css class = "danger")
|
The mobile layout |
So the final product a fast good looking page to view the org code coverage.
Installation : You can follow the project detail link to install a unmanaged package of this page. If you already have visualstrap unmanaged package installed you may have to remove it or you can use source from github to install the same.
VisualForce Page
<apex:page controller="ApexCodeCoverageList_Con" sidebar="false">
<c:importvisualstrap />
<apex:includeScript value="{!$Resource.JSRender}"/>
<script>
function getCodeCoverage(){
var rBtn = $('#refreshBtn').button('loading');
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.ApexCodeCoverageList_Con.fetchCodeCoverage}',
function(result,event){
if(event.status){
console.log(result);
var parsedResult = jQuery.parseJSON(result);
/*render html using jsrender and attach it to the table*/
$('#coverageTableBody').html($( "#coverageRowTemplate" ).render( parsedResult.records ));
}
else{
alert(event.message);
}
rBtn.button('reset');
},
{escape: false}
);
}
function getOrgCoverage(){
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.ApexCodeCoverageList_Con.fetchOrgCoverage}',
function(result,event){
if(event.status){
var parsedResult = jQuery.parseJSON(result);
$('#orgCoverage').html(parsedResult.records[0].PercentCovered);
}
else{
alert(event.message);
}
},
{escape: false}
);
}
function getCoverage(){
getOrgCoverage();
getCodeCoverage();
}
/*JSrender helper methods*/
function initHelperMethods(){
$.views.helpers({
calculatePercentage: function(NumLinesUncovered,NumLinesCovered){
return ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);
},
totalLines:function(NumLinesUncovered,NumLinesCovered){
return NumLinesUncovered + NumLinesCovered;
},
rowStatusClass: function(NumLinesUncovered,NumLinesCovered){
var sclass='danger';
var percentG = ((NumLinesCovered/(NumLinesCovered+NumLinesUncovered))*100).toFixed(2);
if(percentG >= 90){
sclass = 'success'
}
else if(percentG >= 75){
sclass = 'warning';
}
return sclass;
}
});
}
$(function(){
initHelperMethods();
getCoverage();
})
</script>
<!-- JS render template -->
<script id="coverageRowTemplate" type="text/x-jsrender">
<tr class="{{:~rowStatusClass(NumLinesUncovered,NumLinesCovered)}}">
<td width="20px">
<a href="/{{>ApexClassOrTriggerId}}" target="_blank" class="btn btn-xs btn-info"> <span class="glyphicon glyphicon-export"/> view </a>
</td>
<td>
{{>ApexClassOrTrigger.Name}}
</td>
<td>
{{>NumLinesUncovered}}
</td>
<td>
{{>NumLinesCovered}}
</td>
<td>
{{:~totalLines(NumLinesUncovered,NumLinesCovered)}}
</td>
<td>
{{:~calculatePercentage(NumLinesUncovered,NumLinesCovered )}}
</td>
</tr>
</script>
<c:visualstrapblock >
<c:panel type="primary">
<center>
<c:pageheader icon="cog" title="Apex Code Coverage" subtitle="All Classes"/>
<div class="text-muted" style="position:absolute;top:20px;right:20px">Using Tooling API, JS Remoting, JSRender and VisualStrap</div>
</center>
<apex:outputPanel layout="block" styleClass="well well-sm">
<center>
<button id="refreshBtn" onclick="getCoverage();return false;" class="btn btn-success" data-loading-text="Refreshing...">
<c:glyph icon="refresh"/> Refresh
</button>
</center>
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="row">
<apex:outputPanel layout="block" styleClass="col-md-10">
<table class="table table-bordered table-striped table-hover table-condensed">
<thead>
<tr>
<th>
Action
</th>
<th>
Apex Class/ Trigger
</th>
<th>
Lines Not Covered
</th>
<th>
Lines Covered
</th>
<th>
Total Lines
</th>
<th>
Coverage Percentage
</th>
</tr>
</thead>
<tbody id="coverageTableBody">
</tbody>
</table>
</apex:outputPanel>
<apex:outputPanel layout="block" styleClass="col-md-2">
<vs:panel type="primary" title="Overall Coverage" >
<center>
<h2 style="font-size:54"><span id="orgCoverage"/> %</h2>
<p class="text-muted infolabel">Across all apex classes and triggers</p>
</center>
</c:panel>
</apex:outputPanel>
</apex:outputPanel>
</c:panel>
</c:visualstrapblock>
</apex:page>
Apex Class
/*
* @Author : Avi (avidev9@gmail.com)
* @Description : Controller class for ApexCodeCoverageList page, Contains remoted method and method to call tooling api
*
**/
public class ApexCodeCoverageList_Con{
private static FINAL String ORG_INSTANCE;
private static FINAL String TOOLINGAPI_ENDPOINT;
static{
ORG_INSTANCE = getInstance();
TOOLINGAPI_ENDPOINT = 'https://'+ORG_INSTANCE+'.salesforce.com/services/data/v29.0/tooling/';
}
@RemoteAction
public static String fetchCodeCoverage(){
return sendToolingQueryRequest('SELECT+NumLinesCovered,ApexClassOrTriggerId,ApexClassOrTrigger.Name,NumLinesUncovered+FROM+ApexCodeCoverage');
}
@RemoteAction
public static String fetchOrgCoverage(){
return sendToolingQueryRequest('SELECT+PercentCovered+FROM+ApexOrgWideCoverage');
}
/*Method to send query request to tooling api endpoint*/
private static String sendToolingQueryRequest(String queryStr){
HttpRequest req = new HttpRequest();
req.setEndpoint(TOOLINGAPI_ENDPOINT+'query/?q='+queryStr);
/*Set authorization by using current users session Id*/
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
req.setHeader('Content-Type', 'application/json');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
return res.getBody();
}
/*Method to get org instance*/
private static String getInstance(){
String instance;
List<String> parts = System.URL.getSalesforceBaseUrl().getHost().replace('-api','').split('\\.');
if (parts.size() == 3 ) Instance = parts[0];
else if (parts.size() == 5 || parts.size() == 4) Instance = parts[1];
else Instance = null;
return instance;
}
}