/* jscoverage.js - code coverage for JavaScript Copyright (C) 2007, 2008 siliconforks.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /** Initializes the _$jscoverage object in a window. This should be the first function called in the page. @param w this should always be the global window object */ function jscoverage_init(w) { try { // in Safari, "import" is a syntax error Components.utils['import']('resource://gre/modules/jscoverage.jsm'); jscoverage_isInvertedMode = true; return; } catch (e) {} if (w.opener && w.opener.top._$jscoverage) { // we are in inverted mode jscoverage_isInvertedMode = true; if (! w._$jscoverage) { w._$jscoverage = w.opener.top._$jscoverage; } } else { // we are not in inverted mode jscoverage_isInvertedMode = false; if (! w._$jscoverage) { w._$jscoverage = {}; } } } var jscoverage_currentFile = null; var jscoverage_currentLine = null; var jscoverage_inLengthyOperation = false; /* Possible states: isInvertedMode isServer isReport tabs normal false false false Browser inverted true false false server, normal false true false Browser, Store server, inverted true true false Store report false false true */ var jscoverage_isInvertedMode = false; var jscoverage_isServer = false; var jscoverage_isReport = false; jscoverage_init(window); function jscoverage_createRequest() { // Note that the IE7 XMLHttpRequest does not support file URL's. // http://xhab.blogspot.com/2006/11/ie7-support-for-xmlhttprequest.html // http://blogs.msdn.com/ie/archive/2006/12/06/file-uris-in-windows.aspx //#JSCOVERAGE_IF if (window.ActiveXObject) { return new ActiveXObject("Microsoft.XMLHTTP"); } else { return new XMLHttpRequest(); } } // http://www.quirksmode.org/js/findpos.html function jscoverage_findPos(obj) { var result = 0; do { result += obj.offsetTop; obj = obj.offsetParent; } while (obj); return result; } // http://www.quirksmode.org/viewport/compatibility.html function jscoverage_getViewportHeight() { //#JSCOVERAGE_IF /MSIE/.test(navigator.userAgent) if (self.innerHeight) { // all except Explorer return self.innerHeight; } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode return document.documentElement.clientHeight; } else if (document.body) { // other Explorers return document.body.clientHeight; } else { throw "Couldn't calculate viewport height"; } //#JSCOVERAGE_ENDIF } /** Indicates visually that a lengthy operation has begun. The progress bar is displayed, and the cursor is changed to busy (on browsers which support this). */ function jscoverage_beginLengthyOperation() { jscoverage_inLengthyOperation = true; var progressBar = document.getElementById('progressBar'); progressBar.style.visibility = 'visible'; ProgressBar.setPercentage(progressBar, 0); var progressLabel = document.getElementById('progressLabel'); progressLabel.style.visibility = 'visible'; /* blacklist buggy browsers */ //#JSCOVERAGE_IF if (! /Opera|WebKit/.test(navigator.userAgent)) { /* Change the cursor style of each element. Note that changing the class of the element (to one with a busy cursor) is buggy in IE. */ var tabs = document.getElementById('tabs').getElementsByTagName('div'); var i; for (i = 0; i < tabs.length; i++) { tabs.item(i).style.cursor = 'wait'; } } } /** Removes the progress bar and busy cursor. */ function jscoverage_endLengthyOperation() { var progressBar = document.getElementById('progressBar'); ProgressBar.setPercentage(progressBar, 100); setTimeout(function() { jscoverage_inLengthyOperation = false; progressBar.style.visibility = 'hidden'; var progressLabel = document.getElementById('progressLabel'); progressLabel.style.visibility = 'hidden'; progressLabel.innerHTML = ''; var tabs = document.getElementById('tabs').getElementsByTagName('div'); var i; for (i = 0; i < tabs.length; i++) { tabs.item(i).style.cursor = ''; } }, 50); } function jscoverage_setSize() { //#JSCOVERAGE_IF /MSIE/.test(navigator.userAgent) var viewportHeight = jscoverage_getViewportHeight(); /* border-top-width: 1px padding-top: 10px padding-bottom: 10px border-bottom-width: 1px margin-bottom: 10px ---- 32px */ var tabPages = document.getElementById('tabPages'); var tabPageHeight = (viewportHeight - jscoverage_findPos(tabPages) - 32) + 'px'; var nodeList = tabPages.childNodes; var length = nodeList.length; for (var i = 0; i < length; i++) { var node = nodeList.item(i); if (node.nodeType !== 1) { continue; } node.style.height = tabPageHeight; } var iframeDiv = document.getElementById('iframeDiv'); // may not exist if we have removed the first tab if (iframeDiv) { iframeDiv.style.height = (viewportHeight - jscoverage_findPos(iframeDiv) - 21) + 'px'; } var summaryDiv = document.getElementById('summaryDiv'); summaryDiv.style.height = (viewportHeight - jscoverage_findPos(summaryDiv) - 21) + 'px'; var sourceDiv = document.getElementById('sourceDiv'); sourceDiv.style.height = (viewportHeight - jscoverage_findPos(sourceDiv) - 21) + 'px'; var storeDiv = document.getElementById('storeDiv'); if (storeDiv) { storeDiv.style.height = (viewportHeight - jscoverage_findPos(storeDiv) - 21) + 'px'; } //#JSCOVERAGE_ENDIF } /** Returns the boolean value of a string. Values 'false', 'f', 'no', 'n', 'off', and '0' (upper or lower case) are false. @param s the string @return a boolean value */ function jscoverage_getBooleanValue(s) { s = s.toLowerCase(); if (s === 'false' || s === 'f' || s === 'no' || s === 'n' || s === 'off' || s === '0') { return false; } return true; } function jscoverage_removeTab(id) { var tab = document.getElementById(id + 'Tab'); tab.parentNode.removeChild(tab); var tabPage = document.getElementById(id + 'TabPage'); tabPage.parentNode.removeChild(tabPage); } /** Initializes the contents of the tabs. This sets the initial values of the input field and iframe in the "Browser" tab and the checkbox in the "Summary" tab. @param queryString this should always be location.search */ function jscoverage_initTabContents(queryString) { var showMissingColumn = false; var parameters, parameter, i, index, url, name, value; if (queryString.length > 0) { // chop off the question mark queryString = queryString.substring(1); parameters = queryString.split(/&|;/); for (i = 0; i < parameters.length; i++) { parameter = parameters[i]; index = parameter.indexOf('='); if (index === -1) { // still works with old syntax url = parameter; } else { name = parameter.substr(0, index); value = parameter.substr(index + 1); if (name === 'missing' || name === 'm') { showMissingColumn = jscoverage_getBooleanValue(value); } else if (name === 'url' || name === 'u') { url = value; } } } } var checkbox = document.getElementById('checkbox'); checkbox.checked = showMissingColumn; if (showMissingColumn) { jscoverage_appendMissingColumn(); } // this will automatically propagate to the input field if (url) { frames[0].location = url; } // if the browser tab is absent, we have to initialize the summary tab if (! document.getElementById('browserTab')) { jscoverage_recalculateSummaryTab(); } } function jscoverage_body_load() { var progressBar = document.getElementById('progressBar'); ProgressBar.init(progressBar); function reportError(e) { jscoverage_endLengthyOperation(); var summaryThrobber = document.getElementById('summaryThrobber'); summaryThrobber.style.visibility = 'hidden'; var div = document.getElementById('summaryErrorDiv'); div.innerHTML = 'Error: ' + e; } if (jscoverage_isReport) { jscoverage_beginLengthyOperation(); var summaryThrobber = document.getElementById('summaryThrobber'); summaryThrobber.style.visibility = 'visible'; var request = jscoverage_createRequest(); try { request.open('GET', 'jscoverage.json', true); request.onreadystatechange = function (event) { if (request.readyState === 4) { try { if (request.status !== 0 && request.status !== 200) { throw request.status; } var response = request.responseText; if (response === '') { throw 404; } var json = eval('(' + response + ')'); var file; for (file in json) { var fileCoverage = json[file]; _$jscoverage[file] = fileCoverage.coverage; _$jscoverage[file].source = fileCoverage.source; } jscoverage_recalculateSummaryTab(); summaryThrobber.style.visibility = 'hidden'; } catch (e) { reportError(e); } } }; request.send(null); } catch (e) { reportError(e); } jscoverage_removeTab('browser'); jscoverage_removeTab('store'); } else { if (jscoverage_isInvertedMode) { jscoverage_removeTab('browser'); } if (! jscoverage_isServer) { jscoverage_removeTab('store'); } } jscoverage_initTabControl(); jscoverage_initTabContents(location.search); } function jscoverage_body_resize() { if (/MSIE/.test(navigator.userAgent)) { jscoverage_setSize(); } } // ----------------------------------------------------------------------------- // tab 1 function jscoverage_updateBrowser() { var input = document.getElementById("location"); frames[0].location = input.value; } function jscoverage_input_keypress(e) { if (e.keyCode === 13) { jscoverage_updateBrowser(); } } function jscoverage_button_click() { jscoverage_updateBrowser(); } function jscoverage_browser_load() { /* update the input box */ var input = document.getElementById("location"); /* sometimes IE seems to fire this after the tab has been removed */ if (input) { input.value = frames[0].location; } } // ----------------------------------------------------------------------------- // tab 2 function jscoverage_createLink(file, line) { var link = document.createElement("a"); var url; var call; var text; if (line) { url = file + ".jscoverage.html?" + line; call = "jscoverage_get('" + file + "', " + line + ");"; text = line.toString(); } else { url = file + ".jscoverage.html"; call = "jscoverage_get('" + file + "');"; text = file; } link.setAttribute('href', 'javascript:' + call); link.appendChild(document.createTextNode(text)); return link; } function jscoverage_recalculateSummaryTab(cc) { var checkbox = document.getElementById('checkbox'); var showMissingColumn = checkbox.checked; if (! cc) { cc = window._$jscoverage; } if (! cc) { //#JSCOVERAGE_IF 0 throw "No coverage information found."; //#JSCOVERAGE_ENDIF } var tbody = document.getElementById("summaryTbody"); while (tbody.hasChildNodes()) { tbody.removeChild(tbody.firstChild); } var totals = { files:0, statements:0, executed:0, coverage:0, skipped:0 }; var file; var files = []; for (file in cc) { files.push(file); } files.sort(); var rowCounter = 0; for (var f = 0; f < files.length; f++) { file = files[f]; var lineNumber; var num_statements = 0; var num_executed = 0; var missing = []; var fileCC = cc[file]; var length = fileCC.length; var currentConditionalEnd = 0; var conditionals = null; if (fileCC.conditionals) { conditionals = fileCC.conditionals; } for (lineNumber = 0; lineNumber < length; lineNumber++) { var n = fileCC[lineNumber]; if (lineNumber === currentConditionalEnd) { currentConditionalEnd = 0; } else if (currentConditionalEnd === 0 && conditionals && conditionals[lineNumber]) { currentConditionalEnd = conditionals[lineNumber]; } if (currentConditionalEnd !== 0) { continue; } if (n === undefined || n === null) { continue; } if (n === 0) { missing.push(lineNumber); } else { num_executed++; } num_statements++; } var percentage = ( num_statements === 0 ? 0 : parseInt(100 * num_executed / num_statements) ); var row = document.createElement("tr"); row.className = ( rowCounter++ % 2 == 0 ? "odd" : "even" ); var cell = document.createElement("td"); cell.className = 'leftColumn'; var link = jscoverage_createLink(file); cell.appendChild(link); row.appendChild(cell); cell = document.createElement("td"); cell.className = 'numeric'; cell.appendChild(document.createTextNode(num_statements)); row.appendChild(cell); cell = document.createElement("td"); cell.className = 'numeric'; cell.appendChild(document.createTextNode(num_executed)); row.appendChild(cell); // new coverage td containing a bar graph cell = document.createElement("td"); cell.className = 'coverage'; var pctGraph = document.createElement("div"), covered = document.createElement("div"), pct = document.createElement("span"); pctGraph.className = "pctGraph"; if( num_statements === 0 ) { covered.className = "skipped"; pct.appendChild(document.createTextNode("N/A")); } else { covered.className = "covered"; covered.style.width = percentage + "px"; pct.appendChild(document.createTextNode(percentage + '%')); } pct.className = "pct"; pctGraph.appendChild(covered); cell.appendChild(pctGraph); cell.appendChild(pct); row.appendChild(cell); if (showMissingColumn) { cell = document.createElement("td"); for (var i = 0; i < missing.length; i++) { if (i !== 0) { cell.appendChild(document.createTextNode(", ")); } link = jscoverage_createLink(file, missing[i]); cell.appendChild(link); } row.appendChild(cell); } tbody.appendChild(row); totals['files'] ++; totals['statements'] += num_statements; totals['executed'] += num_executed; totals['coverage'] += percentage; if( num_statements === 0 ) { totals['skipped']++; } // write totals data into summaryTotals row var tr = document.getElementById("summaryTotals"); if (tr) { var tds = tr.getElementsByTagName("td"); tds[0].getElementsByTagName("span")[1].firstChild.nodeValue = totals['files']; tds[1].firstChild.nodeValue = totals['statements']; tds[2].firstChild.nodeValue = totals['executed']; var coverage = parseInt(totals['coverage'] / ( totals['files'] - totals['skipped'] ) ); if( isNaN( coverage ) ) { coverage = 0; } tds[3].getElementsByTagName("span")[0].firstChild.nodeValue = coverage + '%'; tds[3].getElementsByTagName("div")[1].style.width = coverage + 'px'; } } jscoverage_endLengthyOperation(); } function jscoverage_appendMissingColumn() { var headerRow = document.getElementById('headerRow'); var missingHeader = document.createElement('th'); missingHeader.id = 'missingHeader'; missingHeader.innerHTML = 'Missing'; headerRow.appendChild(missingHeader); var summaryTotals = document.getElementById('summaryTotals'); var empty = document.createElement('td'); empty.id = 'missingCell'; summaryTotals.appendChild(empty); } function jscoverage_removeMissingColumn() { var missingNode; missingNode = document.getElementById('missingHeader'); missingNode.parentNode.removeChild(missingNode); missingNode = document.getElementById('missingCell'); missingNode.parentNode.removeChild(missingNode); } function jscoverage_checkbox_click() { if (jscoverage_inLengthyOperation) { return false; } jscoverage_beginLengthyOperation(); var checkbox = document.getElementById('checkbox'); var showMissingColumn = checkbox.checked; setTimeout(function() { if (showMissingColumn) { jscoverage_appendMissingColumn(); } else { jscoverage_removeMissingColumn(); } jscoverage_recalculateSummaryTab(); }, 50); return true; } // ----------------------------------------------------------------------------- // tab 3 function jscoverage_makeTable() { var coverage = _$jscoverage[jscoverage_currentFile]; var lines = coverage.source; // this can happen if there is an error in the original JavaScript file if (! lines) { lines = []; } var rows = ['
' + lineNumber + ' | '; var timesExecuted = coverage[lineNumber]; if (timesExecuted !== undefined && timesExecuted !== null) { if (currentConditionalEnd !== 0) { row += ''; } else if (timesExecuted === 0) { row += ' | '; } else { row += ' | '; } row += timesExecuted; row += ' | '; } else { row += ''; } row += ' | ' + lines[i] + ' | ';
row += '