jasmine-coverage 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Jasmine Coverage
2
+
3
+ A transcendent blend of useful JS unit testing and colourful coverage graphs.
4
+
5
+ This gem allows [Jasmine Headless Webkit](http://johnbintz.github.com/jasmine-headless-webkit/)
6
+ to be run against a Rails application's Javascript tests, and then produces a coverage report, optionally
7
+ failing it if it falls below a configurable level.
8
+
9
+ Coverage is provided by the [jscoverage](http://siliconforks.com/jscoverage/manual.html) project.
10
+
11
+ # Installation
12
+
13
+ First, ensure you have a binary of [jscoverage](http://siliconforks.com/jscoverage/manual.html)
14
+ available on your path. The installation steps are on the webpage.
15
+
16
+ Then, add the following in your Gemfile. Note, there were a raft of small issues with older versions
17
+ of [Jasmine Headless Webkit](http://johnbintz.github.com/jasmine-headless-webkit/), so for the moment you must use
18
+ the master branch of that project.
19
+
20
+ gem 'jasmine-coverage'
21
+ gem 'jasmine-headless-webkit', :git => 'git://github.com/johnbintz/jasmine-headless-webkit.git'
22
+
23
+ # Usage
24
+
25
+ To use jasmine-coverage, run the rake task.
26
+
27
+ bundle exec rake jasmine:coverage
28
+
29
+ Optionally, add a failure level percentage.
30
+
31
+ bundle exec rake jasmine:coverage JASMINE_COVERAGE_MINIMUM=75
32
+
33
+ # Output
34
+
35
+ You will see the tests execute, then a large blob of text, and finally a summary of the test coverage results.
36
+ An HTML file will also be saved that lets you view the results graphically, but only if served up from a server,
37
+ not local disk. This is because the jscoverage generated report page needs to make a request for a local json
38
+ file, and browsers won't allow a local file to read another local file off disk.
39
+
40
+ To reiterate: if you try to open the report file locally, you will see NETWORK_ERR: XMLHttpRequest Exception,
41
+ as the browser may not access the json file locally. However if your build server allows you to browse project build
42
+ artefacts, you can view the visual report as the json is served from there too.
43
+
44
+ Files generated will be
45
+
46
+ target/jscoverage/jscoverage.html - The visual report shell
47
+ target/jscoverage/jscoverage.json - The report data
48
+ target/jscoverage/jscoverage-test-rig.html - The actual page that the tests executed in
49
+
50
+ # How it works
51
+
52
+ First Sprockets is interrogated to get a list of JS files concerned. This way, the right JS files
53
+ are required *in the same order that your app uses them*. JSCoverage then runs over them, and outputs the
54
+ instrumented files in the target folder. Next, Jasmine Headless Webkit runs as normal, but a couple of monkey
55
+ patches intercept the locations of the javascript files it expects to find, rerouting them to the instrumented versions.
56
+
57
+ The data we get from the coverage can only "leave" the JS sandbox one way: via the console. This is why you see such
58
+ a large block of Base64 encoded rubbish flying past as the build progresses. The console data is captured by Jasmine
59
+ Coverage, which decodes it and builds the results HTML page, and gives a short summary in the console.
60
+
61
+ You're done.
@@ -0,0 +1,3 @@
1
+ require_relative 'coverage/version'
2
+
3
+ load 'tasks/jasmine_coverage.rake'
@@ -0,0 +1,5 @@
1
+ module Jasmine
2
+ module Coverage
3
+ VERSION = '0.1.1'
4
+ end
5
+ end
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Note, strictly speaking this isn't a spec.
3
+ * But we must run it like one so it occurs in the same context
4
+ * as the other tests that have run. In that way, we can
5
+ * call out in javascript and get the resulting coverage reports from the instrumented
6
+ * files.
7
+ *
8
+ * Further, when we log the results to console, the file logger captures that.
9
+ */
10
+ describe("jasmine-coverage", function () {
11
+
12
+ it("is generating a coverage report", function () {
13
+ // Output the complete line by line coverage report for capture by the file logger
14
+ generateEncodedCoverage();
15
+
16
+ // Get the simple percentages for each file
17
+ coverageForAllFiles();
18
+ });
19
+
20
+ });
21
+
22
+ String.prototype.lpad = function (padString, length) {
23
+ var str = this;
24
+ while (str.length < length)
25
+ str = padString + str;
26
+ return str;
27
+ };
28
+
29
+ function generateEncodedCoverage() {
30
+ var rv = {};
31
+ for (var file_name in window._$jscoverage) {
32
+ var jscov = window._$jscoverage[ file_name ];
33
+ var file_report = rv[ file_name ] = {
34
+ coverage:new Array(jscov.length),
35
+ source:new Array(jscov.length)
36
+ };
37
+ for (var i = 0; i < jscov.length; ++i) {
38
+ var hit_count = jscov[ i ] !== undefined ? jscov[ i ] : null;
39
+
40
+ file_report.coverage[ i ] = hit_count;
41
+ file_report.source[ i ] = jscov.source[ i ];
42
+ }
43
+ }
44
+ console.log("ENCODED-COVERAGE-EXPORT-STARTS:" + Base64.encode(JSON.stringify(rv)));
45
+ console.log("\nENCODED-COVERAGE-EXPORT-ENDS\n");
46
+ }
47
+
48
+ function coverageForAllFiles() {
49
+
50
+ var totals = { files:0, statements:0, executed:0 };
51
+
52
+ var output = "Coverage was:\n";
53
+
54
+ for (var file_name in window._$jscoverage) {
55
+ var jscov = window._$jscoverage[ file_name ];
56
+ var simple_file_coverage = coverageForFile(jscov);
57
+
58
+ totals['files']++;
59
+ totals['statements'] += simple_file_coverage['statements'];
60
+ totals['executed'] += simple_file_coverage['executed'];
61
+
62
+ var fraction = (simple_file_coverage['executed']+"/"+simple_file_coverage['statements']).lpad(' ', 10);
63
+ output += fraction + (" = " + simple_file_coverage['percentage'] + "").lpad(' ', 3) + "% for " + file_name + "\n";
64
+ }
65
+
66
+ var coverage = parseInt(100 * totals['executed'] / totals['statements']);
67
+ if (isNaN(coverage)) {
68
+ coverage = 0;
69
+ }
70
+
71
+ if (totals['statements'] === 0) {
72
+ log("No Javascript was found to test coverage for.");
73
+ } else {
74
+ output += ( totals['executed'] +"/"+totals['statements']+ " = "+ coverage + "").lpad(' ', 15) + "% Total\n";
75
+ log(output);
76
+ }
77
+
78
+ return coverage;
79
+ }
80
+
81
+
82
+ function coverageForFile(fileCC) {
83
+ var lineNumber;
84
+ var num_statements = 0;
85
+ var num_executed = 0;
86
+ var missing = [];
87
+ var length = fileCC.length;
88
+ var currentConditionalEnd = 0;
89
+ var conditionals = null;
90
+ if (fileCC.conditionals) {
91
+ conditionals = fileCC.conditionals;
92
+ }
93
+ for (lineNumber = 0; lineNumber < length; lineNumber++) {
94
+ var n = fileCC[lineNumber];
95
+
96
+ if (lineNumber === currentConditionalEnd) {
97
+ currentConditionalEnd = 0;
98
+ }
99
+ else if (currentConditionalEnd === 0 && conditionals && conditionals[lineNumber]) {
100
+ currentConditionalEnd = conditionals[lineNumber];
101
+ }
102
+
103
+ if (currentConditionalEnd !== 0) {
104
+ continue;
105
+ }
106
+
107
+ if (n === undefined || n === null) {
108
+ continue;
109
+ }
110
+
111
+ if (n === 0) {
112
+ missing.push(lineNumber);
113
+ }
114
+ else {
115
+ num_executed++;
116
+ }
117
+ num_statements++;
118
+ }
119
+
120
+ var percentage = ( num_statements === 0 ? 0 : parseInt(100 * num_executed / num_statements) );
121
+
122
+ return {
123
+ statements:num_statements,
124
+ executed:num_executed,
125
+ percentage:percentage
126
+ };
127
+ }
@@ -0,0 +1,129 @@
1
+ env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
2
+ if env =~ /^(development|test)$/
3
+ require 'rake'
4
+ require 'base64'
5
+
6
+ namespace :jasmine do
7
+ desc 'Runs jasmine with a coverage report'
8
+ task :coverage do
9
+
10
+ require 'jasmine-headless-webkit'
11
+ # Instill our patches for jasmine-headless to work
12
+ require_relative 'jasmine_headless_coverage_patches'
13
+
14
+ # We use jasmine-headless-webkit, since it has excellent programmatic integration with Jasmine
15
+ # But... the 'headless' part of it doesn't work on TeamCity, so we use the headless gem
16
+ require 'headless'
17
+
18
+ headless = Headless.new
19
+ headless.start
20
+
21
+ # Preprocess the JS files to add instrumentation
22
+ output_dir = File.expand_path('target/jscoverage/')
23
+ instrumented_dir = output_dir+'/instrumented/'
24
+ FileUtils.rm_rf output_dir
25
+ FileUtils.mkdir_p instrumented_dir
26
+
27
+ # The reprocessing folder map
28
+ files_map = {
29
+ File.expand_path('app/assets/javascripts') => instrumented_dir+'app',
30
+ File.expand_path('lib/assets/javascripts') => instrumented_dir+'lib',
31
+ File.expand_path('public/javascripts') => instrumented_dir+'public'
32
+ }
33
+
34
+ # Instrument the source files into the instrumented folders
35
+ files_map.keys.each do |folder|
36
+ instrument(folder, files_map[folder])
37
+ # Also hoist up the eventual viewing files
38
+ FileUtils.mv(Dir.glob(files_map[folder]+'/jscoverage*'), output_dir)
39
+ end
40
+
41
+ Jasmine::Coverage.resources = files_map
42
+ Jasmine::Coverage.output_dir = output_dir
43
+
44
+ puts "\nCoverage will now be run. Expect a large block of compiled coverage data. This will be processed for you into target/jscoverage.\n\n"
45
+
46
+ # Run Jasmine using the original config.
47
+ status_code = Jasmine::Headless::Runner.run(
48
+ # Any options from the options.rb file in jasmine-headless-webkit can be used here.
49
+
50
+ :reporters => [['File', "#{output_dir}/rawreport.txt"]]
51
+ )
52
+ errStr = "JSCoverage exited with error code: #{status_code}.\nThis implies one of four things:\n"
53
+ errStr = errStr +"0) Your JS files had exactly zero instructions. Are they all blank or just comments?\n"
54
+ errStr = errStr +"1) A test failed (run bundle exec jasmine:headless to see a better error)\n"
55
+ errStr = errStr +"2) The sourcecode has a syntax error (which JSLint should find)\n"
56
+ errStr = errStr +"3) The source files are being loaded out of sequence (so global variables are not being declared in order)\n"
57
+ errStr = errStr +" To check this, run bundle exec jasmine-headless-webkit -l to see the ordering\n"
58
+ errStr = errStr +"\nIn any case, try running the standard jasmine command to get better errors:\n\nbundle exec jasmine:headless\n\n"
59
+ fail errStr if status_code == 1
60
+
61
+ # Obtain the console log, which includes the coverage report encoded within it
62
+ contents = File.open("#{output_dir}/rawreport.txt") { |f| f.read }
63
+ # Get our Base64.
64
+ json_report_enc = contents.split(/ENCODED-COVERAGE-EXPORT-STARTS:/m)[1]
65
+ # Remove the junk at the end
66
+ json_report_enc_stripped = json_report_enc[0, json_report_enc.index("\"")]
67
+
68
+ # Unpack it from Base64
69
+ json_report = Base64.decode64(json_report_enc_stripped)
70
+
71
+ # Save the coverage report where the GUI html expects it to be
72
+ File.open("#{output_dir}/jscoverage.json", 'w') { |f| f.write(json_report) }
73
+
74
+ # Modify the jscoverage.html so it knows it is showing a report, not running a test
75
+ File.open("#{output_dir}/jscoverage.js", 'a') { |f| f.write("\njscoverage_isReport = true;") }
76
+
77
+ if json_report_enc.index("No Javascript was found to test coverage for").nil?
78
+ # Check for coverage failure
79
+ total_location = json_report_enc.index("% Total")
80
+ coverage_pc = json_report_enc[total_location-3, 3].to_i
81
+
82
+ conf = (ENV['JSCOVERAGE_MINIMUM'] || ENV['JASMINE_COVERAGE_MINIMUM'])
83
+ fail "Coverage Fail: Javascript coverage was less than #{min}%. It was #{coverage_pc}%." if conf && coverage_pc < conf.to_i
84
+ end
85
+
86
+ end
87
+
88
+ def instrument folder, instrumented_sub_dir
89
+ return if !File.directory? folder
90
+ FileUtils.mkdir_p instrumented_sub_dir
91
+ puts "Locating jscoverage..."
92
+ system "which jscoverage"
93
+ puts "Instrumenting JS files..."
94
+ jsc_status = system "jscoverage -v #{folder} #{instrumented_sub_dir}"
95
+ if jsc_status != true
96
+ puts "jscoverage failed with status '#{jsc_status}'. Is jscoverage on your path? Path follows:"
97
+ system "echo $PATH"
98
+ puts "Result of calling jscoverage with no arguments follows:"
99
+ system "jscoverage"
100
+ fail "Unable to use jscoverage"
101
+ end
102
+ end
103
+ end
104
+
105
+ module Jasmine
106
+ module Coverage
107
+ @resources
108
+
109
+ def self.resources= resources
110
+ @resources = resources
111
+ end
112
+
113
+ def self.resources
114
+ @resources
115
+ end
116
+
117
+ @output_dir
118
+
119
+ def self.output_dir= output_dir
120
+ @output_dir = output_dir
121
+ end
122
+
123
+ def self.output_dir
124
+ @output_dir
125
+ end
126
+ end
127
+ end
128
+
129
+ end
@@ -0,0 +1,65 @@
1
+ # This file holds the monkeypatches to open up jasmine headless for jasmine coverage.
2
+
3
+
4
+ # This patch writes out a copy of the file that was loaded into the JSCoverage context for testing.
5
+ # You can look at it to see if it included all the files and tests you expect.
6
+ require 'jasmine/headless/template_writer'
7
+ module Jasmine::Headless
8
+ class TemplateWriter
9
+ alias old_write :write
10
+
11
+ def write
12
+ ret = old_write
13
+ file = Jasmine::Coverage.output_dir+"/jscoverage-test-rig.html"
14
+ FileUtils.cp(all_tests_filename, file)
15
+ puts "A copy of the complete page that was used as the test environment can be found here:"
16
+ puts "#{file}"
17
+ ret
18
+ end
19
+ end
20
+ end
21
+
22
+ # Here we patch the resource handler to output the location of our instrumented files
23
+ module Jasmine::Headless
24
+ class FilesList
25
+
26
+ alias old_to_html :to_html
27
+
28
+ def to_html(files)
29
+ # Declare our test runner files
30
+ cov_files = ['/jscoverage.js', '/coverage_output_generator.js']
31
+
32
+ # Add the original files, remapping to instrumented where necessary
33
+ tags = []
34
+ (old_to_html files).each do |path|
35
+ files_map = Jasmine::Coverage.resources
36
+ files_map.keys.each do |folder|
37
+ path = path.sub(folder, files_map[folder])
38
+
39
+ # Here we must check the supplied config hasn't pulled in our jscoverage runner file.
40
+ # If it has, the tests will fire too early, capturing only minimal coverage
41
+ if cov_files.select { |f| path.include?(f) }.length > 0
42
+ fail "Assets defined by jasmine.yml must not include any of #{cov_files}: #{path}"
43
+ end
44
+
45
+ end
46
+ tags << path
47
+ end
48
+
49
+ # Attach the "in context" test runners
50
+ tags = tags + old_to_html(cov_files.map { |f| File.dirname(__FILE__)+f })
51
+
52
+ tags
53
+ end
54
+
55
+ alias old_sprockets_environment :sprockets_environment
56
+
57
+ def sprockets_environment
58
+ return @sprockets_environment if @sprockets_environment
59
+ old_sprockets_environment
60
+ # Add the location of our jscoverage.js
61
+ @sprockets_environment.append_path(File.dirname(__FILE__))
62
+ @sprockets_environment
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,1176 @@
1
+ /*
2
+ jscoverage.js - code coverage for JavaScript
3
+ Copyright (C) 2007, 2008, 2009, 2010 siliconforks.com
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ */
19
+
20
+ function jscoverage_openWarningDialog() {
21
+ var id;
22
+ if (jscoverage_isReport) {
23
+ id = 'reportWarningDialog';
24
+ }
25
+ else {
26
+ id = 'warningDialog';
27
+ }
28
+ var dialog = document.getElementById(id);
29
+ dialog.style.display = 'block';
30
+ }
31
+
32
+ function jscoverage_closeWarningDialog() {
33
+ var id;
34
+ if (jscoverage_isReport) {
35
+ id = 'reportWarningDialog';
36
+ }
37
+ else {
38
+ id = 'warningDialog';
39
+ }
40
+ var dialog = document.getElementById(id);
41
+ dialog.style.display = 'none';
42
+ }
43
+
44
+ /**
45
+ Initializes the _$jscoverage object in a window. This should be the first
46
+ function called in the page.
47
+ @param w this should always be the global window object
48
+ */
49
+ function jscoverage_init(w) {
50
+ try {
51
+ // in Safari, "import" is a syntax error
52
+ Components.utils['import']('resource://app/modules/jscoverage.jsm');
53
+ jscoverage_isInvertedMode = true;
54
+ return;
55
+ }
56
+ catch (e) {}
57
+
58
+ // check if we are in inverted mode
59
+ if (w.opener) {
60
+ try {
61
+ if (w.opener.top._$jscoverage) {
62
+ jscoverage_isInvertedMode = true;
63
+ if (! w._$jscoverage) {
64
+ w._$jscoverage = w.opener.top._$jscoverage;
65
+ }
66
+ }
67
+ else {
68
+ jscoverage_isInvertedMode = false;
69
+ }
70
+ }
71
+ catch (e) {
72
+ try {
73
+ if (w.opener._$jscoverage) {
74
+ jscoverage_isInvertedMode = true;
75
+ if (! w._$jscoverage) {
76
+ w._$jscoverage = w.opener._$jscoverage;
77
+ }
78
+ }
79
+ else {
80
+ jscoverage_isInvertedMode = false;
81
+ }
82
+ }
83
+ catch (e2) {
84
+ jscoverage_isInvertedMode = false;
85
+ }
86
+ }
87
+ }
88
+ else {
89
+ jscoverage_isInvertedMode = false;
90
+ }
91
+
92
+ if (! jscoverage_isInvertedMode) {
93
+ if (! w._$jscoverage) {
94
+ w._$jscoverage = {};
95
+ }
96
+ }
97
+ }
98
+
99
+ var jscoverage_currentFile = null;
100
+ var jscoverage_currentLine = null;
101
+
102
+ var jscoverage_inLengthyOperation = false;
103
+
104
+ /*
105
+ Possible states:
106
+ isInvertedMode isServer isReport tabs
107
+ normal false false false Browser
108
+ inverted true false false
109
+ server, normal false true false Browser, Store
110
+ server, inverted true true false Store
111
+ report false false true
112
+ */
113
+ var jscoverage_isInvertedMode = false;
114
+ var jscoverage_isServer = false;
115
+ var jscoverage_isReport = false;
116
+
117
+ jscoverage_init(window);
118
+
119
+ function jscoverage_createRequest() {
120
+ // Note that the IE7 XMLHttpRequest does not support file URL's.
121
+ // http://xhab.blogspot.com/2006/11/ie7-support-for-xmlhttprequest.html
122
+ // http://blogs.msdn.com/ie/archive/2006/12/06/file-uris-in-windows.aspx
123
+ //#JSCOVERAGE_IF
124
+ if (window.ActiveXObject) {
125
+ return new ActiveXObject("Microsoft.XMLHTTP");
126
+ }
127
+ else {
128
+ return new XMLHttpRequest();
129
+ }
130
+ }
131
+
132
+ // http://www.quirksmode.org/js/findpos.html
133
+ function jscoverage_findPos(obj) {
134
+ var result = 0;
135
+ do {
136
+ result += obj.offsetTop;
137
+ obj = obj.offsetParent;
138
+ }
139
+ while (obj);
140
+ return result;
141
+ }
142
+
143
+ // http://www.quirksmode.org/viewport/compatibility.html
144
+ function jscoverage_getViewportHeight() {
145
+ //#JSCOVERAGE_IF /MSIE/.test(navigator.userAgent)
146
+ if (self.innerHeight) {
147
+ // all except Explorer
148
+ return self.innerHeight;
149
+ }
150
+ else if (document.documentElement && document.documentElement.clientHeight) {
151
+ // Explorer 6 Strict Mode
152
+ return document.documentElement.clientHeight;
153
+ }
154
+ else if (document.body) {
155
+ // other Explorers
156
+ return document.body.clientHeight;
157
+ }
158
+ else {
159
+ throw "Couldn't calculate viewport height";
160
+ }
161
+ //#JSCOVERAGE_ENDIF
162
+ }
163
+
164
+ /**
165
+ Indicates visually that a lengthy operation has begun. The progress bar is
166
+ displayed, and the cursor is changed to busy (on browsers which support this).
167
+ */
168
+ function jscoverage_beginLengthyOperation() {
169
+ jscoverage_inLengthyOperation = true;
170
+
171
+ var progressBar = document.getElementById('progressBar');
172
+ progressBar.style.visibility = 'visible';
173
+ ProgressBar.setPercentage(progressBar, 0);
174
+ var progressLabel = document.getElementById('progressLabel');
175
+ progressLabel.style.visibility = 'visible';
176
+
177
+ /* blacklist buggy browsers */
178
+ //#JSCOVERAGE_IF
179
+ if (! /Opera|WebKit/.test(navigator.userAgent)) {
180
+ /*
181
+ Change the cursor style of each element. Note that changing the class of the
182
+ element (to one with a busy cursor) is buggy in IE.
183
+ */
184
+ var tabs = document.getElementById('tabs').getElementsByTagName('div');
185
+ var i;
186
+ for (i = 0; i < tabs.length; i++) {
187
+ tabs.item(i).style.cursor = 'wait';
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ Removes the progress bar and busy cursor.
194
+ */
195
+ function jscoverage_endLengthyOperation() {
196
+ var progressBar = document.getElementById('progressBar');
197
+ ProgressBar.setPercentage(progressBar, 100);
198
+ setTimeout(function() {
199
+ jscoverage_inLengthyOperation = false;
200
+ progressBar.style.visibility = 'hidden';
201
+ var progressLabel = document.getElementById('progressLabel');
202
+ progressLabel.style.visibility = 'hidden';
203
+ progressLabel.innerHTML = '';
204
+
205
+ var tabs = document.getElementById('tabs').getElementsByTagName('div');
206
+ var i;
207
+ for (i = 0; i < tabs.length; i++) {
208
+ tabs.item(i).style.cursor = '';
209
+ }
210
+ }, 50);
211
+ }
212
+
213
+ function jscoverage_setSize() {
214
+ //#JSCOVERAGE_IF /MSIE/.test(navigator.userAgent)
215
+ var viewportHeight = jscoverage_getViewportHeight();
216
+
217
+ /*
218
+ border-top-width: 1px
219
+ padding-top: 10px
220
+ padding-bottom: 10px
221
+ border-bottom-width: 1px
222
+ margin-bottom: 10px
223
+ ----
224
+ 32px
225
+ */
226
+ var tabPages = document.getElementById('tabPages');
227
+ var tabPageHeight = (viewportHeight - jscoverage_findPos(tabPages) - 32) + 'px';
228
+ var nodeList = tabPages.childNodes;
229
+ var length = nodeList.length;
230
+ for (var i = 0; i < length; i++) {
231
+ var node = nodeList.item(i);
232
+ if (node.nodeType !== 1) {
233
+ continue;
234
+ }
235
+ node.style.height = tabPageHeight;
236
+ }
237
+
238
+ var iframeDiv = document.getElementById('iframeDiv');
239
+ // may not exist if we have removed the first tab
240
+ if (iframeDiv) {
241
+ iframeDiv.style.height = (viewportHeight - jscoverage_findPos(iframeDiv) - 21) + 'px';
242
+ }
243
+
244
+ var summaryDiv = document.getElementById('summaryDiv');
245
+ summaryDiv.style.height = (viewportHeight - jscoverage_findPos(summaryDiv) - 21) + 'px';
246
+
247
+ var sourceDiv = document.getElementById('sourceDiv');
248
+ sourceDiv.style.height = (viewportHeight - jscoverage_findPos(sourceDiv) - 21) + 'px';
249
+
250
+ var storeDiv = document.getElementById('storeDiv');
251
+ if (storeDiv) {
252
+ storeDiv.style.height = (viewportHeight - jscoverage_findPos(storeDiv) - 21) + 'px';
253
+ }
254
+ //#JSCOVERAGE_ENDIF
255
+ }
256
+
257
+ /**
258
+ Returns the boolean value of a string. Values 'false', 'f', 'no', 'n', 'off',
259
+ and '0' (upper or lower case) are false.
260
+ @param s the string
261
+ @return a boolean value
262
+ */
263
+ function jscoverage_getBooleanValue(s) {
264
+ s = s.toLowerCase();
265
+ if (s === 'false' || s === 'f' || s === 'no' || s === 'n' || s === 'off' || s === '0') {
266
+ return false;
267
+ }
268
+ return true;
269
+ }
270
+
271
+ function jscoverage_removeTab(id) {
272
+ var tab = document.getElementById(id + 'Tab');
273
+ tab.parentNode.removeChild(tab);
274
+ var tabPage = document.getElementById(id + 'TabPage');
275
+ tabPage.parentNode.removeChild(tabPage);
276
+ }
277
+
278
+ function jscoverage_isValidURL(url) {
279
+ // RFC 3986
280
+ var matches = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/.exec(url);
281
+ if (matches === null) {
282
+ return false;
283
+ }
284
+ var scheme = matches[1];
285
+ if (typeof scheme === 'string') {
286
+ scheme = scheme.toLowerCase();
287
+ return scheme === '' || scheme === 'file:' || scheme === 'http:' || scheme === 'https:';
288
+ }
289
+ return true;
290
+ }
291
+
292
+ /**
293
+ Initializes the contents of the tabs. This sets the initial values of the
294
+ input field and iframe in the "Browser" tab and the checkbox in the "Summary"
295
+ tab.
296
+ @param queryString this should always be location.search
297
+ */
298
+ function jscoverage_initTabContents(queryString) {
299
+ var showMissingColumn = false;
300
+ var url = null;
301
+ var windowURL = null;
302
+ var parameters, parameter, i, index, name, value;
303
+ if (queryString.length > 0) {
304
+ // chop off the question mark
305
+ queryString = queryString.substring(1);
306
+ parameters = queryString.split(/&|;/);
307
+ for (i = 0; i < parameters.length; i++) {
308
+ parameter = parameters[i];
309
+ index = parameter.indexOf('=');
310
+ if (index === -1) {
311
+ // still works with old syntax
312
+ url = decodeURIComponent(parameter);
313
+ }
314
+ else {
315
+ name = parameter.substr(0, index);
316
+ value = decodeURIComponent(parameter.substr(index + 1));
317
+ if (name === 'missing' || name === 'm') {
318
+ showMissingColumn = jscoverage_getBooleanValue(value);
319
+ }
320
+ else if (name === 'url' || name === 'u' || name === 'frame' || name === 'f') {
321
+ url = value;
322
+ }
323
+ else if (name === 'window' || name === 'w') {
324
+ windowURL = value;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ var checkbox = document.getElementById('checkbox');
331
+ checkbox.checked = showMissingColumn;
332
+ if (showMissingColumn) {
333
+ jscoverage_appendMissingColumn();
334
+ }
335
+
336
+ var isValidURL = function (url) {
337
+ var result = jscoverage_isValidURL(url);
338
+ if (! result) {
339
+ alert('Invalid URL: ' + url);
340
+ }
341
+ return result;
342
+ };
343
+
344
+ if (url !== null && isValidURL(url)) {
345
+ // this will automatically propagate to the input field
346
+ frames[0].location = url;
347
+ }
348
+ else if (windowURL !== null && isValidURL(windowURL)) {
349
+ window.open(windowURL);
350
+ }
351
+
352
+ // if the browser tab is absent, we have to initialize the summary tab
353
+ if (! document.getElementById('browserTab')) {
354
+ jscoverage_recalculateSummaryTab();
355
+ }
356
+ }
357
+
358
+ function jscoverage_body_load() {
359
+ // check if this is a file: URL
360
+ if (window.location && window.location.href && /^file:/i.test(window.location.href)) {
361
+ var warningDiv = document.getElementById('warningDiv');
362
+ warningDiv.style.display = 'block';
363
+ }
364
+
365
+ var progressBar = document.getElementById('progressBar');
366
+ ProgressBar.init(progressBar);
367
+
368
+ function reportError(e) {
369
+ jscoverage_endLengthyOperation();
370
+ var summaryThrobber = document.getElementById('summaryThrobber');
371
+ summaryThrobber.style.visibility = 'hidden';
372
+ var div = document.getElementById('summaryErrorDiv');
373
+ div.innerHTML = 'Error: ' + e;
374
+ }
375
+
376
+ if (jscoverage_isReport) {
377
+ jscoverage_beginLengthyOperation();
378
+ var summaryThrobber = document.getElementById('summaryThrobber');
379
+ summaryThrobber.style.visibility = 'visible';
380
+ var request = jscoverage_createRequest();
381
+ try {
382
+ request.open('GET', 'jscoverage.json', true);
383
+ request.onreadystatechange = function (event) {
384
+ if (request.readyState === 4) {
385
+ try {
386
+ if (request.status !== 0 && request.status !== 200) {
387
+ throw request.status;
388
+ }
389
+ var response = request.responseText;
390
+ if (response === '') {
391
+ throw 404;
392
+ }
393
+
394
+ var json;
395
+ if (window.JSON && window.JSON.parse) {
396
+ json = window.JSON.parse(response);
397
+ }
398
+ else {
399
+ json = eval('(' + response + ')');
400
+ }
401
+
402
+ var file;
403
+ for (file in json) {
404
+ if (! json.hasOwnProperty(file)) {
405
+ continue;
406
+ }
407
+
408
+ var fileCoverage = json[file];
409
+ _$jscoverage[file] = fileCoverage.coverage;
410
+ _$jscoverage[file].source = fileCoverage.source;
411
+ }
412
+ jscoverage_recalculateSummaryTab();
413
+ summaryThrobber.style.visibility = 'hidden';
414
+ }
415
+ catch (e) {
416
+ reportError(e);
417
+ }
418
+ }
419
+ };
420
+ request.send(null);
421
+ }
422
+ catch (e) {
423
+ reportError(e);
424
+ }
425
+
426
+ jscoverage_removeTab('browser');
427
+ jscoverage_removeTab('store');
428
+ }
429
+ else {
430
+ if (jscoverage_isInvertedMode) {
431
+ jscoverage_removeTab('browser');
432
+ }
433
+
434
+ if (! jscoverage_isServer) {
435
+ jscoverage_removeTab('store');
436
+ }
437
+ }
438
+
439
+ jscoverage_initTabControl();
440
+
441
+ jscoverage_initTabContents(location.search);
442
+ }
443
+
444
+ function jscoverage_body_resize() {
445
+ if (/MSIE/.test(navigator.userAgent)) {
446
+ jscoverage_setSize();
447
+ }
448
+ }
449
+
450
+ // -----------------------------------------------------------------------------
451
+ // tab 1
452
+
453
+ function jscoverage_updateBrowser() {
454
+ var input = document.getElementById("location");
455
+ frames[0].location = input.value;
456
+ }
457
+
458
+ function jscoverage_openWindow() {
459
+ var input = document.getElementById("location");
460
+ var url = input.value;
461
+ window.open(url);
462
+ }
463
+
464
+ function jscoverage_input_keypress(e) {
465
+ if (e.keyCode === 13) {
466
+ if (e.shiftKey) {
467
+ jscoverage_openWindow();
468
+ }
469
+ else {
470
+ jscoverage_updateBrowser();
471
+ }
472
+ }
473
+ }
474
+
475
+ function jscoverage_openInFrameButton_click() {
476
+ jscoverage_updateBrowser();
477
+ }
478
+
479
+ function jscoverage_openInWindowButton_click() {
480
+ jscoverage_openWindow();
481
+ }
482
+
483
+ function jscoverage_browser_load() {
484
+ /* update the input box */
485
+ var input = document.getElementById("location");
486
+
487
+ /* sometimes IE seems to fire this after the tab has been removed */
488
+ if (input) {
489
+ input.value = frames[0].location;
490
+ }
491
+ }
492
+
493
+ // -----------------------------------------------------------------------------
494
+ // tab 2
495
+
496
+ function jscoverage_createHandler(file, line) {
497
+ return function () {
498
+ jscoverage_get(file, line);
499
+ return false;
500
+ };
501
+ }
502
+
503
+ function jscoverage_createLink(file, line) {
504
+ var link = document.createElement("a");
505
+ link.href = '#';
506
+ link.onclick = jscoverage_createHandler(file, line);
507
+
508
+ var text;
509
+ if (line) {
510
+ text = line.toString();
511
+ }
512
+ else {
513
+ text = file;
514
+ }
515
+
516
+ link.appendChild(document.createTextNode(text));
517
+
518
+ return link;
519
+ }
520
+
521
+ function jscoverage_recalculateSummaryTab(cc) {
522
+ var checkbox = document.getElementById('checkbox');
523
+ var showMissingColumn = checkbox.checked;
524
+
525
+ if (! cc) {
526
+ cc = window._$jscoverage;
527
+ }
528
+ if (! cc) {
529
+ //#JSCOVERAGE_IF 0
530
+ throw "No coverage information found.";
531
+ //#JSCOVERAGE_ENDIF
532
+ }
533
+
534
+ var tbody = document.getElementById("summaryTbody");
535
+ while (tbody.hasChildNodes()) {
536
+ tbody.removeChild(tbody.firstChild);
537
+ }
538
+
539
+ var totals = { files:0, statements:0, executed:0 };
540
+
541
+ var file;
542
+ var files = [];
543
+ for (file in cc) {
544
+ if (! cc.hasOwnProperty(file)) {
545
+ continue;
546
+ }
547
+
548
+ files.push(file);
549
+ }
550
+ files.sort();
551
+
552
+ var rowCounter = 0;
553
+ for (var f = 0; f < files.length; f++) {
554
+ file = files[f];
555
+ var lineNumber;
556
+ var num_statements = 0;
557
+ var num_executed = 0;
558
+ var missing = [];
559
+ var fileCC = cc[file];
560
+ var length = fileCC.length;
561
+ var currentConditionalEnd = 0;
562
+ var conditionals = null;
563
+ if (fileCC.conditionals) {
564
+ conditionals = fileCC.conditionals;
565
+ }
566
+ for (lineNumber = 0; lineNumber < length; lineNumber++) {
567
+ var n = fileCC[lineNumber];
568
+
569
+ if (lineNumber === currentConditionalEnd) {
570
+ currentConditionalEnd = 0;
571
+ }
572
+ else if (currentConditionalEnd === 0 && conditionals && conditionals[lineNumber]) {
573
+ currentConditionalEnd = conditionals[lineNumber];
574
+ }
575
+
576
+ if (currentConditionalEnd !== 0) {
577
+ continue;
578
+ }
579
+
580
+ if (n === undefined || n === null) {
581
+ continue;
582
+ }
583
+
584
+ if (n === 0) {
585
+ missing.push(lineNumber);
586
+ }
587
+ else {
588
+ num_executed++;
589
+ }
590
+ num_statements++;
591
+ }
592
+
593
+ var percentage = ( num_statements === 0 ? 0 : parseInt(100 * num_executed / num_statements) );
594
+
595
+ var row = document.createElement("tr");
596
+ row.className = ( rowCounter++ % 2 == 0 ? "odd" : "even" );
597
+
598
+ var cell = document.createElement("td");
599
+ cell.className = 'leftColumn';
600
+ var link = jscoverage_createLink(file);
601
+ cell.appendChild(link);
602
+
603
+ row.appendChild(cell);
604
+
605
+ cell = document.createElement("td");
606
+ cell.className = 'numeric';
607
+ cell.appendChild(document.createTextNode(num_statements));
608
+ row.appendChild(cell);
609
+
610
+ cell = document.createElement("td");
611
+ cell.className = 'numeric';
612
+ cell.appendChild(document.createTextNode(num_executed));
613
+ row.appendChild(cell);
614
+
615
+ // new coverage td containing a bar graph
616
+ cell = document.createElement("td");
617
+ cell.className = 'coverage';
618
+ var pctGraph = document.createElement("div"),
619
+ covered = document.createElement("div"),
620
+ pct = document.createElement("span");
621
+ pctGraph.className = "pctGraph";
622
+ if( num_statements === 0 ) {
623
+ covered.className = "skipped";
624
+ pct.appendChild(document.createTextNode("N/A"));
625
+ } else {
626
+ covered.className = "covered";
627
+ covered.style.width = percentage + "px";
628
+ pct.appendChild(document.createTextNode(percentage + '%'));
629
+ }
630
+ pct.className = "pct";
631
+ pctGraph.appendChild(covered);
632
+ cell.appendChild(pctGraph);
633
+ cell.appendChild(pct);
634
+ row.appendChild(cell);
635
+
636
+ if (showMissingColumn) {
637
+ cell = document.createElement("td");
638
+ for (var i = 0; i < missing.length; i++) {
639
+ if (i !== 0) {
640
+ cell.appendChild(document.createTextNode(", "));
641
+ }
642
+ link = jscoverage_createLink(file, missing[i]);
643
+
644
+ // group contiguous missing lines; e.g., 10, 11, 12 -> 10-12
645
+ var j, start = missing[i];
646
+ for (;;) {
647
+ j = 1;
648
+ while (i + j < missing.length && missing[i + j] == missing[i] + j) {
649
+ j++;
650
+ }
651
+ var nextmissing = missing[i + j], cur = missing[i] + j;
652
+ if (isNaN(nextmissing)) {
653
+ break;
654
+ }
655
+ while (cur < nextmissing && ! fileCC[cur]) {
656
+ cur++;
657
+ }
658
+ if (cur < nextmissing || cur >= length) {
659
+ break;
660
+ }
661
+ i += j;
662
+ }
663
+ if (start != missing[i] || j > 1) {
664
+ i += j - 1;
665
+ link.innerHTML += "-" + missing[i];
666
+ }
667
+
668
+ cell.appendChild(link);
669
+ }
670
+ row.appendChild(cell);
671
+ }
672
+
673
+ tbody.appendChild(row);
674
+
675
+ totals['files'] ++;
676
+ totals['statements'] += num_statements;
677
+ totals['executed'] += num_executed;
678
+
679
+ // write totals data into summaryTotals row
680
+ var tr = document.getElementById("summaryTotals");
681
+ if (tr) {
682
+ var tds = tr.getElementsByTagName("td");
683
+ tds[0].getElementsByTagName("span")[1].firstChild.nodeValue = totals['files'];
684
+ tds[1].firstChild.nodeValue = totals['statements'];
685
+ tds[2].firstChild.nodeValue = totals['executed'];
686
+
687
+ var coverage = parseInt(100 * totals['executed'] / totals['statements']);
688
+ if( isNaN( coverage ) ) {
689
+ coverage = 0;
690
+ }
691
+ tds[3].getElementsByTagName("span")[0].firstChild.nodeValue = coverage + '%';
692
+ tds[3].getElementsByTagName("div")[1].style.width = coverage + 'px';
693
+ }
694
+
695
+ }
696
+ jscoverage_endLengthyOperation();
697
+ }
698
+
699
+ function jscoverage_appendMissingColumn() {
700
+ var headerRow = document.getElementById('headerRow');
701
+ var missingHeader = document.createElement('th');
702
+ missingHeader.id = 'missingHeader';
703
+ missingHeader.innerHTML = '<abbr title="List of statements missed during execution">Missing</abbr>';
704
+ headerRow.appendChild(missingHeader);
705
+ var summaryTotals = document.getElementById('summaryTotals');
706
+ var empty = document.createElement('td');
707
+ empty.id = 'missingCell';
708
+ summaryTotals.appendChild(empty);
709
+ }
710
+
711
+ function jscoverage_removeMissingColumn() {
712
+ var missingNode;
713
+ missingNode = document.getElementById('missingHeader');
714
+ missingNode.parentNode.removeChild(missingNode);
715
+ missingNode = document.getElementById('missingCell');
716
+ missingNode.parentNode.removeChild(missingNode);
717
+ }
718
+
719
+ function jscoverage_checkbox_click() {
720
+ if (jscoverage_inLengthyOperation) {
721
+ return false;
722
+ }
723
+ jscoverage_beginLengthyOperation();
724
+ var checkbox = document.getElementById('checkbox');
725
+ var showMissingColumn = checkbox.checked;
726
+ setTimeout(function() {
727
+ if (showMissingColumn) {
728
+ jscoverage_appendMissingColumn();
729
+ }
730
+ else {
731
+ jscoverage_removeMissingColumn();
732
+ }
733
+ jscoverage_recalculateSummaryTab();
734
+ }, 50);
735
+ return true;
736
+ }
737
+
738
+ // -----------------------------------------------------------------------------
739
+ // tab 3
740
+
741
+ function jscoverage_makeTable() {
742
+ var coverage = _$jscoverage[jscoverage_currentFile];
743
+ var lines = coverage.source;
744
+
745
+ // this can happen if there is an error in the original JavaScript file
746
+ if (! lines) {
747
+ lines = [];
748
+ }
749
+
750
+ var rows = ['<table id="sourceTable">'];
751
+ var i = 0;
752
+ var progressBar = document.getElementById('progressBar');
753
+ var tableHTML;
754
+ var currentConditionalEnd = 0;
755
+
756
+ function joinTableRows() {
757
+ tableHTML = rows.join('');
758
+ ProgressBar.setPercentage(progressBar, 60);
759
+ /*
760
+ This may be a long delay, so set a timeout of 100 ms to make sure the
761
+ display is updated.
762
+ */
763
+ setTimeout(appendTable, 100);
764
+ }
765
+
766
+ function appendTable() {
767
+ var sourceDiv = document.getElementById('sourceDiv');
768
+ sourceDiv.innerHTML = tableHTML;
769
+ ProgressBar.setPercentage(progressBar, 80);
770
+ setTimeout(jscoverage_scrollToLine, 0);
771
+ }
772
+
773
+ while (i < lines.length) {
774
+ var lineNumber = i + 1;
775
+
776
+ if (lineNumber === currentConditionalEnd) {
777
+ currentConditionalEnd = 0;
778
+ }
779
+ else if (currentConditionalEnd === 0 && coverage.conditionals && coverage.conditionals[lineNumber]) {
780
+ currentConditionalEnd = coverage.conditionals[lineNumber];
781
+ }
782
+
783
+ var row = '<tr>';
784
+ row += '<td class="numeric">' + lineNumber + '</td>';
785
+ var timesExecuted = coverage[lineNumber];
786
+ if (timesExecuted !== undefined && timesExecuted !== null) {
787
+ if (currentConditionalEnd !== 0) {
788
+ row += '<td class="y numeric">';
789
+ }
790
+ else if (timesExecuted === 0) {
791
+ row += '<td class="r numeric" id="line-' + lineNumber + '">';
792
+ }
793
+ else {
794
+ row += '<td class="g numeric">';
795
+ }
796
+ row += timesExecuted;
797
+ row += '</td>';
798
+ }
799
+ else {
800
+ row += '<td></td>';
801
+ }
802
+ row += '<td><pre>' + lines[i] + '</pre></td>';
803
+ row += '</tr>';
804
+ row += '\n';
805
+ rows[lineNumber] = row;
806
+ i++;
807
+ }
808
+ rows[i + 1] = '</table>';
809
+ ProgressBar.setPercentage(progressBar, 40);
810
+ setTimeout(joinTableRows, 0);
811
+ }
812
+
813
+ function jscoverage_scrollToLine() {
814
+ jscoverage_selectTab('sourceTab');
815
+ if (! window.jscoverage_currentLine) {
816
+ jscoverage_endLengthyOperation();
817
+ return;
818
+ }
819
+ var div = document.getElementById('sourceDiv');
820
+ if (jscoverage_currentLine === 1) {
821
+ div.scrollTop = 0;
822
+ }
823
+ else {
824
+ var cell = document.getElementById('line-' + jscoverage_currentLine);
825
+
826
+ // this might not be there if there is an error in the original JavaScript
827
+ if (cell) {
828
+ var divOffset = jscoverage_findPos(div);
829
+ var cellOffset = jscoverage_findPos(cell);
830
+ div.scrollTop = cellOffset - divOffset;
831
+ }
832
+ }
833
+ jscoverage_currentLine = 0;
834
+ jscoverage_endLengthyOperation();
835
+ }
836
+
837
+ /**
838
+ Loads the given file (and optional line) in the source tab.
839
+ */
840
+ function jscoverage_get(file, line) {
841
+ if (jscoverage_inLengthyOperation) {
842
+ return;
843
+ }
844
+ jscoverage_beginLengthyOperation();
845
+ setTimeout(function() {
846
+ var sourceDiv = document.getElementById('sourceDiv');
847
+ sourceDiv.innerHTML = '';
848
+ jscoverage_selectTab('sourceTab');
849
+ if (file === jscoverage_currentFile) {
850
+ jscoverage_currentLine = line;
851
+ jscoverage_recalculateSourceTab();
852
+ }
853
+ else {
854
+ if (jscoverage_currentFile === null) {
855
+ var tab = document.getElementById('sourceTab');
856
+ tab.className = '';
857
+ tab.onclick = jscoverage_tab_click;
858
+ }
859
+ jscoverage_currentFile = file;
860
+ jscoverage_currentLine = line || 1; // when changing the source, always scroll to top
861
+ var fileDiv = document.getElementById('fileDiv');
862
+ fileDiv.innerHTML = jscoverage_currentFile;
863
+ jscoverage_recalculateSourceTab();
864
+ return;
865
+ }
866
+ }, 50);
867
+ }
868
+
869
+ /**
870
+ Calculates coverage statistics for the current source file.
871
+ */
872
+ function jscoverage_recalculateSourceTab() {
873
+ if (! jscoverage_currentFile) {
874
+ jscoverage_endLengthyOperation();
875
+ return;
876
+ }
877
+ var progressLabel = document.getElementById('progressLabel');
878
+ progressLabel.innerHTML = 'Calculating coverage ...';
879
+ var progressBar = document.getElementById('progressBar');
880
+ ProgressBar.setPercentage(progressBar, 20);
881
+ setTimeout(jscoverage_makeTable, 0);
882
+ }
883
+
884
+ // -----------------------------------------------------------------------------
885
+ // tabs
886
+
887
+ /**
888
+ Initializes the tab control. This function must be called when the document is
889
+ loaded.
890
+ */
891
+ function jscoverage_initTabControl() {
892
+ var tabs = document.getElementById('tabs');
893
+ var i;
894
+ var child;
895
+ var tabNum = 0;
896
+ for (i = 0; i < tabs.childNodes.length; i++) {
897
+ child = tabs.childNodes.item(i);
898
+ if (child.nodeType === 1) {
899
+ if (child.className !== 'disabled') {
900
+ child.onclick = jscoverage_tab_click;
901
+ }
902
+ tabNum++;
903
+ }
904
+ }
905
+ jscoverage_selectTab(0);
906
+ }
907
+
908
+ /**
909
+ Selects a tab.
910
+ @param tab the integer index of the tab (0, 1, 2, or 3)
911
+ OR
912
+ the ID of the tab element
913
+ OR
914
+ the tab element itself
915
+ */
916
+ function jscoverage_selectTab(tab) {
917
+ if (typeof tab !== 'number') {
918
+ tab = jscoverage_tabIndexOf(tab);
919
+ }
920
+ var tabs = document.getElementById('tabs');
921
+ var tabPages = document.getElementById('tabPages');
922
+ var nodeList;
923
+ var tabNum;
924
+ var i;
925
+ var node;
926
+
927
+ nodeList = tabs.childNodes;
928
+ tabNum = 0;
929
+ for (i = 0; i < nodeList.length; i++) {
930
+ node = nodeList.item(i);
931
+ if (node.nodeType !== 1) {
932
+ continue;
933
+ }
934
+
935
+ if (node.className !== 'disabled') {
936
+ if (tabNum === tab) {
937
+ node.className = 'selected';
938
+ }
939
+ else {
940
+ node.className = '';
941
+ }
942
+ }
943
+ tabNum++;
944
+ }
945
+
946
+ nodeList = tabPages.childNodes;
947
+ tabNum = 0;
948
+ for (i = 0; i < nodeList.length; i++) {
949
+ node = nodeList.item(i);
950
+ if (node.nodeType !== 1) {
951
+ continue;
952
+ }
953
+
954
+ if (tabNum === tab) {
955
+ node.className = 'selected TabPage';
956
+ }
957
+ else {
958
+ node.className = 'TabPage';
959
+ }
960
+ tabNum++;
961
+ }
962
+ }
963
+
964
+ /**
965
+ Returns an integer (0, 1, 2, or 3) representing the index of a given tab.
966
+ @param tab the ID of the tab element
967
+ OR
968
+ the tab element itself
969
+ */
970
+ function jscoverage_tabIndexOf(tab) {
971
+ if (typeof tab === 'string') {
972
+ tab = document.getElementById(tab);
973
+ }
974
+ var tabs = document.getElementById('tabs');
975
+ var i;
976
+ var child;
977
+ var tabNum = 0;
978
+ for (i = 0; i < tabs.childNodes.length; i++) {
979
+ child = tabs.childNodes.item(i);
980
+ if (child.nodeType === 1) {
981
+ if (child === tab) {
982
+ return tabNum;
983
+ }
984
+ tabNum++;
985
+ }
986
+ }
987
+ //#JSCOVERAGE_IF 0
988
+ throw "Tab not found";
989
+ //#JSCOVERAGE_ENDIF
990
+ }
991
+
992
+ function jscoverage_tab_click(e) {
993
+ if (jscoverage_inLengthyOperation) {
994
+ return;
995
+ }
996
+ var target;
997
+ //#JSCOVERAGE_IF
998
+ if (e) {
999
+ target = e.target;
1000
+ }
1001
+ else if (window.event) {
1002
+ // IE
1003
+ target = window.event.srcElement;
1004
+ }
1005
+ if (target.className === 'selected') {
1006
+ return;
1007
+ }
1008
+ jscoverage_beginLengthyOperation();
1009
+ setTimeout(function() {
1010
+ if (target.id === 'summaryTab') {
1011
+ var tbody = document.getElementById("summaryTbody");
1012
+ while (tbody.hasChildNodes()) {
1013
+ tbody.removeChild(tbody.firstChild);
1014
+ }
1015
+ }
1016
+ else if (target.id === 'sourceTab') {
1017
+ var sourceDiv = document.getElementById('sourceDiv');
1018
+ sourceDiv.innerHTML = '';
1019
+ }
1020
+ jscoverage_selectTab(target);
1021
+ if (target.id === 'summaryTab') {
1022
+ jscoverage_recalculateSummaryTab();
1023
+ }
1024
+ else if (target.id === 'sourceTab') {
1025
+ jscoverage_recalculateSourceTab();
1026
+ }
1027
+ else {
1028
+ jscoverage_endLengthyOperation();
1029
+ }
1030
+ }, 50);
1031
+ }
1032
+
1033
+ // -----------------------------------------------------------------------------
1034
+ // progress bar
1035
+
1036
+ var ProgressBar = {
1037
+ init: function(element) {
1038
+ element._percentage = 0;
1039
+
1040
+ /* doing this via JavaScript crashes Safari */
1041
+ /*
1042
+ var pctGraph = document.createElement('div');
1043
+ pctGraph.className = 'pctGraph';
1044
+ element.appendChild(pctGraph);
1045
+ var covered = document.createElement('div');
1046
+ covered.className = 'covered';
1047
+ pctGraph.appendChild(covered);
1048
+ var pct = document.createElement('span');
1049
+ pct.className = 'pct';
1050
+ element.appendChild(pct);
1051
+ */
1052
+
1053
+ ProgressBar._update(element);
1054
+ },
1055
+ setPercentage: function(element, percentage) {
1056
+ element._percentage = percentage;
1057
+ ProgressBar._update(element);
1058
+ },
1059
+ _update: function(element) {
1060
+ var pctGraph = element.getElementsByTagName('div').item(0);
1061
+ var covered = pctGraph.getElementsByTagName('div').item(0);
1062
+ var pct = element.getElementsByTagName('span').item(0);
1063
+ pct.innerHTML = element._percentage.toString() + '%';
1064
+ covered.style.width = element._percentage + 'px';
1065
+ }
1066
+ };
1067
+
1068
+ // -----------------------------------------------------------------------------
1069
+ // reports
1070
+
1071
+ function jscoverage_pad(s) {
1072
+ return '0000'.substr(s.length) + s;
1073
+ }
1074
+
1075
+ function jscoverage_quote(s) {
1076
+ return '"' + s.replace(/[\u0000-\u001f"\\\u007f-\uffff]/g, function (c) {
1077
+ switch (c) {
1078
+ case '\b':
1079
+ return '\\b';
1080
+ case '\f':
1081
+ return '\\f';
1082
+ case '\n':
1083
+ return '\\n';
1084
+ case '\r':
1085
+ return '\\r';
1086
+ case '\t':
1087
+ return '\\t';
1088
+ // IE doesn't support this
1089
+ /*
1090
+ case '\v':
1091
+ return '\\v';
1092
+ */
1093
+ case '"':
1094
+ return '\\"';
1095
+ case '\\':
1096
+ return '\\\\';
1097
+ default:
1098
+ return '\\u' + jscoverage_pad(c.charCodeAt(0).toString(16));
1099
+ }
1100
+ }) + '"';
1101
+ }
1102
+
1103
+ function jscoverage_serializeCoverageToJSON() {
1104
+ var json = [];
1105
+ for (var file in _$jscoverage) {
1106
+ if (! _$jscoverage.hasOwnProperty(file)) {
1107
+ continue;
1108
+ }
1109
+
1110
+ var coverage = _$jscoverage[file];
1111
+
1112
+ var array = [];
1113
+ var length = coverage.length;
1114
+ for (var line = 0; line < length; line++) {
1115
+ var value = coverage[line];
1116
+ if (value === undefined || value === null) {
1117
+ value = 'null';
1118
+ }
1119
+ array.push(value);
1120
+ }
1121
+
1122
+ var source = coverage.source;
1123
+ var lines = [];
1124
+ length = source.length;
1125
+ for (var line = 0; line < length; line++) {
1126
+ lines.push(jscoverage_quote(source[line]));
1127
+ }
1128
+
1129
+ json.push(jscoverage_quote(file) + ':{"coverage":[' + array.join(',') + '],"source":[' + lines.join(',') + ']}');
1130
+ }
1131
+ return '{' + json.join(',') + '}';
1132
+ }
1133
+
1134
+ function jscoverage_storeButton_click() {
1135
+ if (jscoverage_inLengthyOperation) {
1136
+ return;
1137
+ }
1138
+
1139
+ jscoverage_beginLengthyOperation();
1140
+ var img = document.getElementById('storeImg');
1141
+ img.style.visibility = 'visible';
1142
+
1143
+ var request = jscoverage_createRequest();
1144
+ request.open('POST', '/jscoverage-store', true);
1145
+ request.onreadystatechange = function (event) {
1146
+ if (request.readyState === 4) {
1147
+ var message;
1148
+ try {
1149
+ if (request.status !== 200 && request.status !== 201 && request.status !== 204) {
1150
+ throw request.status;
1151
+ }
1152
+ message = request.responseText;
1153
+ }
1154
+ catch (e) {
1155
+ if (e.toString().search(/^\d{3}$/) === 0) {
1156
+ message = e + ': ' + request.responseText;
1157
+ }
1158
+ else {
1159
+ message = 'Could not connect to server: ' + e;
1160
+ }
1161
+ }
1162
+
1163
+ jscoverage_endLengthyOperation();
1164
+ var img = document.getElementById('storeImg');
1165
+ img.style.visibility = 'hidden';
1166
+
1167
+ var div = document.getElementById('storeDiv');
1168
+ div.appendChild(document.createTextNode(new Date() + ': ' + message));
1169
+ div.appendChild(document.createElement('br'));
1170
+ }
1171
+ };
1172
+ request.setRequestHeader('Content-Type', 'application/json');
1173
+ var json = jscoverage_serializeCoverageToJSON();
1174
+ request.setRequestHeader('Content-Length', json.length.toString());
1175
+ request.send(json);
1176
+ }