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 +61 -0
- data/lib/jasmine/coverage.rb +3 -0
- data/lib/jasmine/coverage/version.rb +5 -0
- data/lib/tasks/coverage_output_generator.js +127 -0
- data/lib/tasks/jasmine_coverage.rake +129 -0
- data/lib/tasks/jasmine_headless_coverage_patches.rb +65 -0
- data/lib/tasks/jscoverage.js +1176 -0
- metadata +100 -0
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,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
|
+
}
|