/* jscoverage.js - code coverage for JavaScript Copyright (C) 2007, 2008, 2009, 2010 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. */ function jscoverage_openWarningDialog() { var id; if (jscoverage_isReport) { id = 'reportWarningDialog'; } else { id = 'warningDialog'; } var dialog = document.getElementById(id); dialog.style.display = 'block'; } function jscoverage_closeWarningDialog() { var id; if (jscoverage_isReport) { id = 'reportWarningDialog'; } else { id = 'warningDialog'; } var dialog = document.getElementById(id); dialog.style.display = 'none'; } /** 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://app/modules/jscoverage.jsm'); jscoverage_isInvertedMode = true; return; } catch (e) {} // check if we are in inverted mode if (w.opener) { try { if (w.opener.top._$jscoverage) { jscoverage_isInvertedMode = true; if (! w._$jscoverage) { w._$jscoverage = w.opener.top._$jscoverage; } } else { jscoverage_isInvertedMode = false; } } catch (e) { try { if (w.opener._$jscoverage) { jscoverage_isInvertedMode = true; if (! w._$jscoverage) { w._$jscoverage = w.opener._$jscoverage; } } else { jscoverage_isInvertedMode = false; } } catch (e2) { jscoverage_isInvertedMode = false; } } } else { jscoverage_isInvertedMode = false; } if (! jscoverage_isInvertedMode) { 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); } function jscoverage_isValidURL(url) { // RFC 3986 var matches = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/.exec(url); if (matches === null) { return false; } var scheme = matches[1]; if (typeof scheme === 'string') { scheme = scheme.toLowerCase(); return scheme === '' || scheme === 'file:' || scheme === 'http:' || scheme === 'https:'; } return true; } /** 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 url = null; var windowURL = null; var parameters, parameter, i, index, 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 = decodeURIComponent(parameter); } else { name = parameter.substr(0, index); value = decodeURIComponent(parameter.substr(index + 1)); if (name === 'missing' || name === 'm') { showMissingColumn = jscoverage_getBooleanValue(value); } else if (name === 'url' || name === 'u' || name === 'frame' || name === 'f') { url = value; } else if (name === 'window' || name === 'w') { windowURL = value; } } } } var checkbox = document.getElementById('checkbox'); checkbox.checked = showMissingColumn; if (showMissingColumn) { jscoverage_appendMissingColumn(); } var isValidURL = function (url) { var result = jscoverage_isValidURL(url); if (! result) { alert('Invalid URL: ' + url); } return result; }; if (url !== null && isValidURL(url)) { // this will automatically propagate to the input field frames[0].location = url; } else if (windowURL !== null && isValidURL(windowURL)) { window.open(windowURL); } // if the browser tab is absent, we have to initialize the summary tab if (! document.getElementById('browserTab')) { jscoverage_recalculateSummaryTab(); } } function jscoverage_body_load() { // check if this is a file: URL if (window.location && window.location.href && /^file:/i.test(window.location.href)) { var warningDiv = document.getElementById('warningDiv'); warningDiv.style.display = 'block'; } 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; if (window.JSON && window.JSON.parse) { json = window.JSON.parse(response); } else { json = eval('(' + response + ')'); } var file; for (file in json) { if (! json.hasOwnProperty(file)) { continue; } 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_openWindow() { var input = document.getElementById("location"); var url = input.value; window.open(url); } function jscoverage_input_keypress(e) { if (e.keyCode === 13) { if (e.shiftKey) { jscoverage_openWindow(); } else { jscoverage_updateBrowser(); } } } function jscoverage_openInFrameButton_click() { jscoverage_updateBrowser(); } function jscoverage_openInWindowButton_click() { jscoverage_openWindow(); } 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_createHandler(file, line) { return function () { jscoverage_get(file, line); return false; }; } function jscoverage_createLink(file, line) { var link = document.createElement("a"); link.href = '#'; link.onclick = jscoverage_createHandler(file, line); var text; if (line) { text = line.toString(); } else { text = file; } 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 }; var file; var files = []; for (file in cc) { if (! cc.hasOwnProperty(file)) { continue; } 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]); // group contiguous missing lines; e.g., 10, 11, 12 -> 10-12 var j, start = missing[i]; for (;;) { j = 1; while (i + j < missing.length && missing[i + j] == missing[i] + j) { j++; } var nextmissing = missing[i + j], cur = missing[i] + j; if (isNaN(nextmissing)) { break; } while (cur < nextmissing && ! fileCC[cur]) { cur++; } if (cur < nextmissing || cur >= length) { break; } i += j; } if (start != missing[i] || j > 1) { i += j - 1; link.innerHTML += "-" + missing[i]; } cell.appendChild(link); } row.appendChild(cell); } tbody.appendChild(row); totals['files'] ++; totals['statements'] += num_statements; totals['executed'] += num_executed; // 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(100 * totals['executed'] / totals['statements']); 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 += '