/* 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 = ['']; var i = 0; var progressBar = document.getElementById('progressBar'); var tableHTML; var currentConditionalEnd = 0; function joinTableRows() { tableHTML = rows.join(''); ProgressBar.setPercentage(progressBar, 60); /* This may be a long delay, so set a timeout of 100 ms to make sure the display is updated. */ setTimeout(appendTable, 100); } function appendTable() { var sourceDiv = document.getElementById('sourceDiv'); sourceDiv.innerHTML = tableHTML; ProgressBar.setPercentage(progressBar, 80); setTimeout(jscoverage_scrollToLine, 0); } while (i < lines.length) { var lineNumber = i + 1; if (lineNumber === currentConditionalEnd) { currentConditionalEnd = 0; } else if (currentConditionalEnd === 0 && coverage.conditionals && coverage.conditionals[lineNumber]) { currentConditionalEnd = coverage.conditionals[lineNumber]; } var row = ''; row += ''; var timesExecuted = coverage[lineNumber]; if (timesExecuted !== undefined && timesExecuted !== null) { if (currentConditionalEnd !== 0) { row += ''; } else { row += ''; } row += ''; row += ''; row += '\n'; rows[lineNumber] = row; i++; } rows[i + 1] = '
' + lineNumber + ''; } else if (timesExecuted === 0) { row += ''; } else { row += ''; } row += timesExecuted; row += '
' + lines[i] + '
'; ProgressBar.setPercentage(progressBar, 40); setTimeout(joinTableRows, 0); } function jscoverage_scrollToLine() { jscoverage_selectTab('sourceTab'); if (! window.jscoverage_currentLine) { jscoverage_endLengthyOperation(); return; } var div = document.getElementById('sourceDiv'); if (jscoverage_currentLine === 1) { div.scrollTop = 0; } else { var cell = document.getElementById('line-' + jscoverage_currentLine); // this might not be there if there is an error in the original JavaScript if (cell) { var divOffset = jscoverage_findPos(div); var cellOffset = jscoverage_findPos(cell); div.scrollTop = cellOffset - divOffset; } } jscoverage_currentLine = 0; jscoverage_endLengthyOperation(); } /** Loads the given file (and optional line) in the source tab. */ function jscoverage_get(file, line) { if (jscoverage_inLengthyOperation) { return; } jscoverage_beginLengthyOperation(); setTimeout(function() { var sourceDiv = document.getElementById('sourceDiv'); sourceDiv.innerHTML = ''; jscoverage_selectTab('sourceTab'); if (file === jscoverage_currentFile) { jscoverage_currentLine = line; jscoverage_recalculateSourceTab(); } else { if (jscoverage_currentFile === null) { var tab = document.getElementById('sourceTab'); tab.className = ''; tab.onclick = jscoverage_tab_click; } jscoverage_currentFile = file; jscoverage_currentLine = line || 1; // when changing the source, always scroll to top var fileDiv = document.getElementById('fileDiv'); fileDiv.innerHTML = jscoverage_currentFile; jscoverage_recalculateSourceTab(); return; } }, 50); } /** Calculates coverage statistics for the current source file. */ function jscoverage_recalculateSourceTab() { if (! jscoverage_currentFile) { jscoverage_endLengthyOperation(); return; } var progressLabel = document.getElementById('progressLabel'); progressLabel.innerHTML = 'Calculating coverage ...'; var progressBar = document.getElementById('progressBar'); ProgressBar.setPercentage(progressBar, 20); setTimeout(jscoverage_makeTable, 0); } // ----------------------------------------------------------------------------- // tabs /** Initializes the tab control. This function must be called when the document is loaded. */ function jscoverage_initTabControl() { var tabs = document.getElementById('tabs'); var i; var child; var tabNum = 0; for (i = 0; i < tabs.childNodes.length; i++) { child = tabs.childNodes.item(i); if (child.nodeType === 1) { if (child.className !== 'disabled') { child.onclick = jscoverage_tab_click; } tabNum++; } } jscoverage_selectTab(0); } /** Selects a tab. @param tab the integer index of the tab (0, 1, 2, or 3) OR the ID of the tab element OR the tab element itself */ function jscoverage_selectTab(tab) { if (typeof tab !== 'number') { tab = jscoverage_tabIndexOf(tab); } var tabs = document.getElementById('tabs'); var tabPages = document.getElementById('tabPages'); var nodeList; var tabNum; var i; var node; nodeList = tabs.childNodes; tabNum = 0; for (i = 0; i < nodeList.length; i++) { node = nodeList.item(i); if (node.nodeType !== 1) { continue; } if (node.className !== 'disabled') { if (tabNum === tab) { node.className = 'selected'; } else { node.className = ''; } } tabNum++; } nodeList = tabPages.childNodes; tabNum = 0; for (i = 0; i < nodeList.length; i++) { node = nodeList.item(i); if (node.nodeType !== 1) { continue; } if (tabNum === tab) { node.className = 'selected TabPage'; } else { node.className = 'TabPage'; } tabNum++; } } /** Returns an integer (0, 1, 2, or 3) representing the index of a given tab. @param tab the ID of the tab element OR the tab element itself */ function jscoverage_tabIndexOf(tab) { if (typeof tab === 'string') { tab = document.getElementById(tab); } var tabs = document.getElementById('tabs'); var i; var child; var tabNum = 0; for (i = 0; i < tabs.childNodes.length; i++) { child = tabs.childNodes.item(i); if (child.nodeType === 1) { if (child === tab) { return tabNum; } tabNum++; } } //#JSCOVERAGE_IF 0 throw "Tab not found"; //#JSCOVERAGE_ENDIF } function jscoverage_tab_click(e) { if (jscoverage_inLengthyOperation) { return; } var target; //#JSCOVERAGE_IF if (e) { target = e.target; } else if (window.event) { // IE target = window.event.srcElement; } if (target.className === 'selected') { return; } jscoverage_beginLengthyOperation(); setTimeout(function() { if (target.id === 'summaryTab') { var tbody = document.getElementById("summaryTbody"); while (tbody.hasChildNodes()) { tbody.removeChild(tbody.firstChild); } } else if (target.id === 'sourceTab') { var sourceDiv = document.getElementById('sourceDiv'); sourceDiv.innerHTML = ''; } jscoverage_selectTab(target); if (target.id === 'summaryTab') { jscoverage_recalculateSummaryTab(); } else if (target.id === 'sourceTab') { jscoverage_recalculateSourceTab(); } else { jscoverage_endLengthyOperation(); } }, 50); } // ----------------------------------------------------------------------------- // progress bar var ProgressBar = { init: function(element) { element._percentage = 0; /* doing this via JavaScript crashes Safari */ /* var pctGraph = document.createElement('div'); pctGraph.className = 'pctGraph'; element.appendChild(pctGraph); var covered = document.createElement('div'); covered.className = 'covered'; pctGraph.appendChild(covered); var pct = document.createElement('span'); pct.className = 'pct'; element.appendChild(pct); */ ProgressBar._update(element); }, setPercentage: function(element, percentage) { element._percentage = percentage; ProgressBar._update(element); }, _update: function(element) { var pctGraph = element.getElementsByTagName('div').item(0); var covered = pctGraph.getElementsByTagName('div').item(0); var pct = element.getElementsByTagName('span').item(0); pct.innerHTML = element._percentage.toString() + '%'; covered.style.width = element._percentage + 'px'; } }; // ----------------------------------------------------------------------------- // reports function jscoverage_pad(s) { return '0000'.substr(s.length) + s; } function jscoverage_quote(s) { return '"' + s.replace(/[\u0000-\u001f"\\\u007f-\uffff]/g, function (c) { switch (c) { case '\b': return '\\b'; case '\f': return '\\f'; case '\n': return '\\n'; case '\r': return '\\r'; case '\t': return '\\t'; // IE doesn't support this /* case '\v': return '\\v'; */ case '"': return '\\"'; case '\\': return '\\\\'; default: return '\\u' + jscoverage_pad(c.charCodeAt(0).toString(16)); } }) + '"'; } function jscoverage_serializeCoverageToJSON() { var json = []; for (var file in _$jscoverage) { var coverage = _$jscoverage[file]; var array = []; var length = coverage.length; for (var line = 0; line < length; line++) { var value = coverage[line]; if (value === undefined || value === null) { value = 'null'; } array.push(value); } json.push(jscoverage_quote(file) + ':[' + array.join(',') + ']'); } return '{' + json.join(',') + '}'; } function jscoverage_storeButton_click() { if (jscoverage_inLengthyOperation) { return; } jscoverage_beginLengthyOperation(); var img = document.getElementById('storeImg'); img.style.visibility = 'visible'; var request = jscoverage_createRequest(); request.open('POST', '/jscoverage-store', true); request.onreadystatechange = function (event) { if (request.readyState === 4) { var message; try { if (request.status !== 200 && request.status !== 201 && request.status !== 204) { throw request.status; } message = request.responseText; } catch (e) { if (e.toString().search(/^\d{3}$/) === 0) { message = e + ': ' + request.responseText; } else { message = 'Could not connect to server: ' + e; } } jscoverage_endLengthyOperation(); var img = document.getElementById('storeImg'); img.style.visibility = 'hidden'; var div = document.getElementById('storeDiv'); div.appendChild(document.createTextNode(new Date() + ': ' + message)); div.appendChild(document.createElement('br')); } }; request.setRequestHeader('Content-Type', 'application/json'); var json = jscoverage_serializeCoverageToJSON(); request.setRequestHeader('Content-Length', json.length.toString()); request.send(json); }