visionmedia-jspec 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.rdoc +232 -0
- data/Manifest +26 -0
- data/README.rdoc +406 -0
- data/Rakefile +70 -0
- data/bin/jspec +64 -0
- data/jspec.gemspec +36 -0
- data/lib/images/bg.png +0 -0
- data/lib/images/hr.png +0 -0
- data/lib/images/sprites.bg.png +0 -0
- data/lib/images/sprites.png +0 -0
- data/lib/images/vr.png +0 -0
- data/lib/jspec.css +129 -0
- data/lib/jspec.jquery.js +68 -0
- data/lib/jspec.js +912 -0
- data/spec/async +1 -0
- data/spec/jquery-1.3.1.js +4241 -0
- data/spec/spec.core.dom.js +12 -0
- data/spec/spec.core.js +323 -0
- data/spec/spec.grammar.js +127 -0
- data/spec/spec.html +24 -0
- data/spec/spec.jquery.js +162 -0
- data/templates/default/History.rdoc +4 -0
- data/templates/default/README.rdoc +29 -0
- data/templates/default/lib/yourlib.core.js +35 -0
- data/templates/default/spec/spec.core.js +8 -0
- data/templates/default/spec/spec.html +20 -0
- metadata +101 -0
data/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake'
|
4
|
+
require 'echoe'
|
5
|
+
|
6
|
+
def version
|
7
|
+
$1 if File.read('lib/jspec.js').match /version *: *'(.*?)'/
|
8
|
+
end
|
9
|
+
|
10
|
+
Echoe.new "jspec", version do |p|
|
11
|
+
p.author = "TJ Holowaychuk"
|
12
|
+
p.email = "tj@vision-media.ca"
|
13
|
+
p.summary = "JavaScript BDD Testing Framework"
|
14
|
+
p.url = "http://visionmedia.github.com/jspec"
|
15
|
+
p.runtime_dependencies << "visionmedia-commander >=3.2.9"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Package'
|
19
|
+
task :package => [:clear] do
|
20
|
+
begin
|
21
|
+
sh 'mkdir pkg'
|
22
|
+
sh 'cp -fr lib/* pkg'
|
23
|
+
minify 'lib/jspec.js', 'pkg/jspec.min.js'
|
24
|
+
minify 'lib/jspec.jquery.js', 'pkg/jspec.jquery.min.js'
|
25
|
+
compress 'lib/jspec.css', 'pkg/jspec.min.css'
|
26
|
+
sh 'git add pkg/.'
|
27
|
+
rescue Exception => e
|
28
|
+
puts "Failed to package: #{e}."
|
29
|
+
else
|
30
|
+
puts "Packaging of JSpec-#{version} completed."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Clear packaging'
|
35
|
+
task :clear do
|
36
|
+
if File.directory? 'pkg'
|
37
|
+
sh 'rm -fr pkg/*'
|
38
|
+
sh 'rmdir pkg'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Display compression savings of last release'
|
43
|
+
task :savings do
|
44
|
+
totals = Hash.new { |h, k| h[k] = 0 }
|
45
|
+
format = '%-20s : %0.3f kb'
|
46
|
+
totals = %w( pkg/jspec.min.js pkg/jspec.jquery.min.js pkg/jspec.min.css ).inject totals do |total, file|
|
47
|
+
uncompressed = File.size(file.sub('.min', '')).to_f / 1024
|
48
|
+
compressed = File.size(file).to_f / 1024
|
49
|
+
saved = uncompressed - compressed
|
50
|
+
puts format % [file.sub('pkg/', ''), saved]
|
51
|
+
totals[:saved] += saved
|
52
|
+
totals[:uncompressed] += uncompressed
|
53
|
+
totals[:compressed] += compressed
|
54
|
+
totals
|
55
|
+
end
|
56
|
+
puts
|
57
|
+
puts format % ['total uncompressed', totals[:uncompressed]]
|
58
|
+
puts format % ['total compressed', totals[:compressed]]
|
59
|
+
puts format % ['total saved', totals[:saved]]
|
60
|
+
end
|
61
|
+
|
62
|
+
def minify from, to
|
63
|
+
sh "jsmin < #{from} > #{to}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def compress from, to
|
67
|
+
File.open(to, 'w+') do |file|
|
68
|
+
file.write File.read(from).gsub(/(^[\t ]*)|\n/, '')
|
69
|
+
end
|
70
|
+
end
|
data/bin/jspec
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'commander'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
JSPEC_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
8
|
+
|
9
|
+
program :name, 'JSpec'
|
10
|
+
program :version, '1.1.1'
|
11
|
+
program :description, 'JavaScript BDD Testing Framework'
|
12
|
+
default_command :bind
|
13
|
+
|
14
|
+
command :init do |c|
|
15
|
+
c.syntax = 'jspec init [dest] [options]'
|
16
|
+
c.summary = 'Initialize a JSpec project template'
|
17
|
+
c.description = 'Initialize a JSpec project template. Defaults to the current directory when <dest> is not specified.'
|
18
|
+
c.example 'Create a directory foo, initialized with a jspec template', 'jspec init foo'
|
19
|
+
c.when_called do |args, options|
|
20
|
+
dest = args.shift || '.'
|
21
|
+
unless Dir[dest + '/*'].empty?
|
22
|
+
abort unless agree "'#{dest}' is not empty; continue? "
|
23
|
+
end
|
24
|
+
FileUtils.mkdir_p dest
|
25
|
+
FileUtils.cp_r File.join(JSPEC_ROOT, 'templates', 'default', '.'), dest
|
26
|
+
spec = File.join dest, 'spec', 'spec.html'
|
27
|
+
contents = File.read spec
|
28
|
+
File.open(spec, 'w') { |file| file.write contents.gsub('JSPEC_ROOT', JSPEC_ROOT) }
|
29
|
+
say "Template initialized at '#{dest}'."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
command :run do |c|
|
34
|
+
c.syntax = 'jspec run [path] [options]'
|
35
|
+
c.summary = 'Run specifications'
|
36
|
+
c.description = 'Run specifications, defaulting <path> to spec/spec.html. You will need supply <path>
|
37
|
+
if your specs do not reside in this location. `run --bind` is the default sub-command of jspec so you
|
38
|
+
may simply execute `jspec` in order to bind execution of your specs when a file is altered.'
|
39
|
+
c.example 'Run once in Safari', 'jspec run'
|
40
|
+
c.example 'Run once in Safari and Firefox', 'jspec run --browsers Safari,Firefox'
|
41
|
+
c.example 'Run custom spec file', 'jspec run foo.html'
|
42
|
+
c.example 'Auto-refresh browsers when a file is altered', 'jspec run --bind --browsers Safari,Firefox'
|
43
|
+
c.example 'Shortcut for the previous example', 'jspec --browsers Safari,Firefox'
|
44
|
+
c.option '-b', '--browsers BROWSERS', Array, 'Specify browsers to test, defaults to Safari'
|
45
|
+
c.option '-p', '--paths PATHS', Array, 'Specify paths when binding, defaults to javascript within ./lib and ./spec'
|
46
|
+
c.option '-B', '--bind', 'Auto-run specs when source files or specs are altered'
|
47
|
+
c.when_called do |args, options|
|
48
|
+
begin
|
49
|
+
require 'bind'
|
50
|
+
spec = args.shift || 'spec/spec.html'
|
51
|
+
options.default :browsers => %w( Safari ), :paths => ['lib/**/*.js', 'spec/**/*.js']
|
52
|
+
action = Bind::Actions::RefreshBrowsers.new spec, *options.browsers
|
53
|
+
if options.bind
|
54
|
+
listener = Bind::Listener.new :paths => options.paths, :interval => 1, :actions => [action], :debug => $stdout
|
55
|
+
listener.run!
|
56
|
+
else
|
57
|
+
action.call spec
|
58
|
+
end
|
59
|
+
rescue LoadError
|
60
|
+
abort "jspec run requires the visionmedia-bind gem; http://visionmedia.github.com/bind/"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias_command :bind, :run, '--bind'
|
data/jspec.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{jspec}
|
5
|
+
s.version = "1.1.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["TJ Holowaychuk"]
|
9
|
+
s.date = %q{2009-04-12}
|
10
|
+
s.default_executable = %q{jspec}
|
11
|
+
s.description = %q{JavaScript BDD Testing Framework}
|
12
|
+
s.email = %q{tj@vision-media.ca}
|
13
|
+
s.executables = ["jspec"]
|
14
|
+
s.extra_rdoc_files = ["bin/jspec", "lib/images/bg.png", "lib/images/hr.png", "lib/images/sprites.bg.png", "lib/images/sprites.png", "lib/images/vr.png", "lib/jspec.css", "lib/jspec.jquery.js", "lib/jspec.js", "README.rdoc"]
|
15
|
+
s.files = ["bin/jspec", "History.rdoc", "jspec.gemspec", "lib/images/bg.png", "lib/images/hr.png", "lib/images/sprites.bg.png", "lib/images/sprites.png", "lib/images/vr.png", "lib/jspec.css", "lib/jspec.jquery.js", "lib/jspec.js", "Manifest", "Rakefile", "README.rdoc", "spec/async", "spec/jquery-1.3.1.js", "spec/spec.core.dom.js", "spec/spec.core.js", "spec/spec.grammar.js", "spec/spec.html", "spec/spec.jquery.js", "templates/default/History.rdoc", "templates/default/lib/yourlib.core.js", "templates/default/README.rdoc", "templates/default/spec/spec.core.js", "templates/default/spec/spec.html"]
|
16
|
+
s.has_rdoc = true
|
17
|
+
s.homepage = %q{http://visionmedia.github.com/jspec}
|
18
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Jspec", "--main", "README.rdoc"]
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
s.rubyforge_project = %q{jspec}
|
21
|
+
s.rubygems_version = %q{1.3.1}
|
22
|
+
s.summary = %q{JavaScript BDD Testing Framework}
|
23
|
+
|
24
|
+
if s.respond_to? :specification_version then
|
25
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
26
|
+
s.specification_version = 2
|
27
|
+
|
28
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
29
|
+
s.add_runtime_dependency(%q<visionmedia-commander>, [">= 3.2.9"])
|
30
|
+
else
|
31
|
+
s.add_dependency(%q<visionmedia-commander>, [">= 3.2.9"])
|
32
|
+
end
|
33
|
+
else
|
34
|
+
s.add_dependency(%q<visionmedia-commander>, [">= 3.2.9"])
|
35
|
+
end
|
36
|
+
end
|
data/lib/images/bg.png
ADDED
Binary file
|
data/lib/images/hr.png
ADDED
Binary file
|
Binary file
|
Binary file
|
data/lib/images/vr.png
ADDED
Binary file
|
data/lib/jspec.css
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
body.jspec {
|
2
|
+
margin: 45px 0;
|
3
|
+
text-align: center;
|
4
|
+
font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
|
5
|
+
background: #efefef url(images/bg.png) top left repeat-x;
|
6
|
+
}
|
7
|
+
#jspec {
|
8
|
+
margin: 0 auto;
|
9
|
+
padding-top: 25px;
|
10
|
+
width: 1008px;
|
11
|
+
background: url(images/vr.png) top left repeat-y;
|
12
|
+
text-align: left;
|
13
|
+
}
|
14
|
+
#jspec-top {
|
15
|
+
position: relative;
|
16
|
+
margin: 0 auto;
|
17
|
+
width: 1008px;
|
18
|
+
height: 40px;
|
19
|
+
background: url(images/sprites.bg.png) top left no-repeat;
|
20
|
+
}
|
21
|
+
#jspec-bottom {
|
22
|
+
margin: 0 auto;
|
23
|
+
width: 1008px;
|
24
|
+
height: 15px;
|
25
|
+
background: url(images/sprites.bg.png) bottom left no-repeat;
|
26
|
+
}
|
27
|
+
#jspec-title {
|
28
|
+
position: relative;
|
29
|
+
top: 35px;
|
30
|
+
left: 20px;
|
31
|
+
width: 160px;
|
32
|
+
font-size: 22px;
|
33
|
+
font-weight: normal;
|
34
|
+
background: url(images/sprites.png) 0 -126px no-repeat;
|
35
|
+
}
|
36
|
+
#jspec-title em {
|
37
|
+
font-size: 10px;
|
38
|
+
font-style: normal;
|
39
|
+
color: #BCC8D1;
|
40
|
+
}
|
41
|
+
#jspec-report * {
|
42
|
+
margin: 0;
|
43
|
+
padding: 0;
|
44
|
+
background: none;
|
45
|
+
border: none;
|
46
|
+
}
|
47
|
+
#jspec-report {
|
48
|
+
padding: 15px 40px;
|
49
|
+
font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
|
50
|
+
color: #7B8D9B;
|
51
|
+
}
|
52
|
+
#jspec-report.has-failures {
|
53
|
+
padding-bottom: 30px;
|
54
|
+
}
|
55
|
+
#jspec-report .hidden {
|
56
|
+
display: none;
|
57
|
+
}
|
58
|
+
#jspec-report .heading {
|
59
|
+
margin-bottom: 15px;
|
60
|
+
}
|
61
|
+
#jspec-report .heading span {
|
62
|
+
padding-right: 10px;
|
63
|
+
}
|
64
|
+
#jspec-report .heading .passes em {
|
65
|
+
color: #0ea0eb;
|
66
|
+
}
|
67
|
+
#jspec-report .heading .failures em {
|
68
|
+
color: #FA1616;
|
69
|
+
}
|
70
|
+
#jspec-report table {
|
71
|
+
width: 100%;
|
72
|
+
font-size: 11px;
|
73
|
+
border-collapse: collapse;
|
74
|
+
}
|
75
|
+
#jspec-report td {
|
76
|
+
padding: 8px;
|
77
|
+
text-indent: 30px;
|
78
|
+
color: #7B8D9B;
|
79
|
+
}
|
80
|
+
#jspec-report tr td:first-child em {
|
81
|
+
font-style: normal;
|
82
|
+
font-weight: normal;
|
83
|
+
color: #7B8D9B;
|
84
|
+
}
|
85
|
+
#jspec-report tr:not(.description):hover {
|
86
|
+
text-shadow: 1px 1px 1px #fff;
|
87
|
+
background: #F2F5F7;
|
88
|
+
}
|
89
|
+
#jspec-report td + td {
|
90
|
+
padding-right: 0;
|
91
|
+
width: 15px;
|
92
|
+
}
|
93
|
+
#jspec-report td.pass {
|
94
|
+
background: url(images/sprites.png) 3px -7px no-repeat;
|
95
|
+
}
|
96
|
+
#jspec-report td.fail {
|
97
|
+
background: url(images/sprites.png) 3px -47px no-repeat;
|
98
|
+
font-weight: bold;
|
99
|
+
color: #FC0D0D;
|
100
|
+
}
|
101
|
+
#jspec-report td.requires-implementation {
|
102
|
+
background: url(images/sprites.png) 3px -87px no-repeat;
|
103
|
+
}
|
104
|
+
#jspec-report tr.description td {
|
105
|
+
margin-top: 25px;
|
106
|
+
padding-top: 25px;
|
107
|
+
font-size: 12px;
|
108
|
+
font-weight: bold;
|
109
|
+
text-indent: 0;
|
110
|
+
color: #1a1a1a;
|
111
|
+
}
|
112
|
+
#jspec-report tr.description:first-child td {
|
113
|
+
border-top: none;
|
114
|
+
}
|
115
|
+
#jspec-report .assertion {
|
116
|
+
display: block;
|
117
|
+
float: left;
|
118
|
+
margin: 0 0 0 1px;
|
119
|
+
padding: 0;
|
120
|
+
width: 1px;
|
121
|
+
height: 5px;
|
122
|
+
background: #7B8D9B;
|
123
|
+
}
|
124
|
+
#jspec-report .assertion.failed {
|
125
|
+
background: red;
|
126
|
+
}
|
127
|
+
.jspec-sandbox {
|
128
|
+
display: none;
|
129
|
+
}
|
data/lib/jspec.jquery.js
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
// JSpec - jQuery - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
3
|
+
|
4
|
+
(function($, $$){
|
5
|
+
|
6
|
+
// --- Dependencies
|
7
|
+
|
8
|
+
$$.requires('jQuery', 'when using jspec.jquery.js')
|
9
|
+
|
10
|
+
// --- Async Support
|
11
|
+
|
12
|
+
$.ajaxSetup({ async : false })
|
13
|
+
|
14
|
+
// --- Helpers
|
15
|
+
|
16
|
+
$$.defaultContext.element = $
|
17
|
+
$$.defaultContext.elements = $
|
18
|
+
$$.defaultContext.defaultSandbox = $$.defaultContext.sandbox
|
19
|
+
$$.defaultContext.sandbox = function() { return $($$.defaultContext.defaultSandbox()) }
|
20
|
+
|
21
|
+
// --- Matchers
|
22
|
+
|
23
|
+
$$.addMatchers({
|
24
|
+
have_tag : "jQuery(expected, actual).length == 1",
|
25
|
+
have_one : "alias have_tag",
|
26
|
+
have_tags : "jQuery(expected, actual).length > 1",
|
27
|
+
have_many : "alias have_tags",
|
28
|
+
have_child : "jQuery(actual).children(expected).length == 1",
|
29
|
+
have_children : "jQuery(actual).children(expected).length > 1",
|
30
|
+
have_class : "jQuery(actual).hasClass(expected)",
|
31
|
+
have_text : "jQuery(actual).text() == expected",
|
32
|
+
have_value : "jQuery(actual).val() == expected",
|
33
|
+
be_visible : "!jQuery(actual).is(':hidden')",
|
34
|
+
be_hidden : "jQuery(actual).is(':hidden')",
|
35
|
+
be_enabled : "!jQuery(actual).attr('disabled')",
|
36
|
+
|
37
|
+
have_attr : { match : function(actual, attr, value) {
|
38
|
+
if (value) return $(actual).attr(attr) == value
|
39
|
+
else return $(actual).attr(attr)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
})
|
43
|
+
|
44
|
+
// --- be_BOOLATTR
|
45
|
+
|
46
|
+
$$.each('disabled selected checked', function(attr){
|
47
|
+
$$.matchers['be_' + attr] = "jQuery(actual).attr('" + attr + "')"
|
48
|
+
})
|
49
|
+
|
50
|
+
// --- have_ATTR
|
51
|
+
|
52
|
+
$$.each('type id title alt href src rel rev name target', function(attr){
|
53
|
+
$$.matchers['have_' + attr] = { match : function(actual, value) {
|
54
|
+
return $$.matchers.have_attr.match(actual, attr, value)
|
55
|
+
}
|
56
|
+
}
|
57
|
+
})
|
58
|
+
|
59
|
+
// --- be_a_TYPE_input (deprecated)
|
60
|
+
|
61
|
+
$$.each('checkbox radio file password submit image text reset button', function(type){
|
62
|
+
console.warn("be_a_" + type + "_input is deprected; use have_type('" + type + "')");
|
63
|
+
JSpec.matchers['be_a_' + type + '_input'] = "jQuery(actual).get(0).type == '" + type + "'"
|
64
|
+
})
|
65
|
+
|
66
|
+
})(jQuery, JSpec)
|
67
|
+
|
68
|
+
|
data/lib/jspec.js
ADDED
@@ -0,0 +1,912 @@
|
|
1
|
+
|
2
|
+
// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
3
|
+
|
4
|
+
(function(){
|
5
|
+
|
6
|
+
JSpec = {
|
7
|
+
|
8
|
+
version : '1.1.1',
|
9
|
+
main : this,
|
10
|
+
suites : [],
|
11
|
+
matchers : {},
|
12
|
+
stats : { specs : 0, assertions : 0, failures : 0, passes : 0 },
|
13
|
+
options : { profile : false },
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Default context in which bodies are evaluated.
|
17
|
+
* This allows specs and hooks to use the 'this' keyword in
|
18
|
+
* order to store variables, as well as allowing the context
|
19
|
+
* to provide helper methods or properties.
|
20
|
+
*
|
21
|
+
* Replace context simply by setting JSpec.context
|
22
|
+
* to your own like below:
|
23
|
+
*
|
24
|
+
* JSpec.context = { foo : 'bar' }
|
25
|
+
*
|
26
|
+
* Contexts can be changed within any body, this can be useful
|
27
|
+
* in order to provide specific helper methods to specific suites.
|
28
|
+
*
|
29
|
+
* To reset (usually in after hook) simply set to null like below:
|
30
|
+
*
|
31
|
+
* JSpec.context = null
|
32
|
+
*/
|
33
|
+
|
34
|
+
defaultContext : {
|
35
|
+
sandbox : function(name) {
|
36
|
+
sandbox = document.createElement('div')
|
37
|
+
sandbox.setAttribute('class', 'jspec-sandbox')
|
38
|
+
document.body.appendChild(sandbox)
|
39
|
+
return sandbox
|
40
|
+
}
|
41
|
+
},
|
42
|
+
|
43
|
+
// --- Objects
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Matcher.
|
47
|
+
*
|
48
|
+
* There are many ways to define a matcher within JSpec. The first being
|
49
|
+
* a string that is less than 4 characters long, which is considered a simple
|
50
|
+
* binary operation between two expressions. For example the matcher '==' simply
|
51
|
+
* evaluates to 'actual == expected'.
|
52
|
+
*
|
53
|
+
* The second way to create a matcher is with a larger string, which is evaluated,
|
54
|
+
* and then returned such as 'actual.match(expected)'.
|
55
|
+
*
|
56
|
+
* You may alias simply by starting a string with 'alias', such as 'be' : 'alias eql'.
|
57
|
+
*
|
58
|
+
* Finally an object may be used, and must contain a 'match' method, which is passed
|
59
|
+
* both the expected, and actual values. Optionally a 'message' method may be used to
|
60
|
+
* specify a custom message. Example:
|
61
|
+
*
|
62
|
+
* match : function(actual, expected) {
|
63
|
+
* return typeof actual == expected
|
64
|
+
* }
|
65
|
+
*
|
66
|
+
* @param {string} name
|
67
|
+
* @param {hash, string} matcher
|
68
|
+
* @param {object} actual
|
69
|
+
* @param {array} expected
|
70
|
+
* @param {bool} negate
|
71
|
+
* @return {Matcher}
|
72
|
+
* @api private
|
73
|
+
*/
|
74
|
+
|
75
|
+
Matcher : function (name, matcher, actual, expected, negate) {
|
76
|
+
self = this
|
77
|
+
this.name = name
|
78
|
+
this.message = ''
|
79
|
+
this.passed = false
|
80
|
+
|
81
|
+
// Define matchers from strings
|
82
|
+
|
83
|
+
if (typeof matcher == 'string') {
|
84
|
+
if (matcher.match(/^alias (\w+)/)) matcher = JSpec.matchers[matcher.match(/^alias (\w+)/)[1]]
|
85
|
+
if (matcher.length < 4) body = 'actual ' + matcher + ' expected'
|
86
|
+
else body = matcher
|
87
|
+
matcher = { match : function(actual, expected) { return eval(body) } }
|
88
|
+
}
|
89
|
+
|
90
|
+
// Generate matcher message
|
91
|
+
|
92
|
+
function generateMessage() {
|
93
|
+
// TODO: clone expected instead of unshifting in this.match()
|
94
|
+
expectedMessage = print.apply(this, expected.slice(1))
|
95
|
+
return 'expected ' + print(actual) + ' to ' + (negate ? ' not ' : '') + name.replace(/_/g, ' ') + ' ' + expectedMessage
|
96
|
+
}
|
97
|
+
|
98
|
+
// Set message to matcher callback invocation or auto-generated message
|
99
|
+
|
100
|
+
function setMessage() {
|
101
|
+
self.message = typeof matcher.message == 'function' ?
|
102
|
+
matcher.message(actual, expected, negate):
|
103
|
+
generateMessage()
|
104
|
+
}
|
105
|
+
|
106
|
+
// Pass the matcher
|
107
|
+
|
108
|
+
function pass() {
|
109
|
+
setMessage()
|
110
|
+
JSpec.stats.passes += 1
|
111
|
+
self.passed = true
|
112
|
+
}
|
113
|
+
|
114
|
+
// Fail the matcher
|
115
|
+
|
116
|
+
function fail() {
|
117
|
+
setMessage()
|
118
|
+
JSpec.stats.failures += 1
|
119
|
+
}
|
120
|
+
|
121
|
+
// Return result of match
|
122
|
+
|
123
|
+
this.match = function() {
|
124
|
+
expected.unshift(actual == null ? null : actual.valueOf())
|
125
|
+
return matcher.match.apply(JSpec, expected)
|
126
|
+
}
|
127
|
+
|
128
|
+
// Boolean match result
|
129
|
+
|
130
|
+
this.passes = function() {
|
131
|
+
this.result = this.match()
|
132
|
+
return negate? !this.result : this.result
|
133
|
+
}
|
134
|
+
|
135
|
+
// Performs match, and passes / fails the matcher
|
136
|
+
|
137
|
+
this.exec = function() {
|
138
|
+
this.passes() ? pass() : fail()
|
139
|
+
return this
|
140
|
+
}
|
141
|
+
},
|
142
|
+
|
143
|
+
|
144
|
+
formatters : {
|
145
|
+
|
146
|
+
/**
|
147
|
+
* Default formatter, outputting to the DOM.
|
148
|
+
*
|
149
|
+
* Options:
|
150
|
+
* - reportToId id of element to output reports to, defaults to 'jspec'
|
151
|
+
* - failuresOnly displays only suites with failing specs
|
152
|
+
*
|
153
|
+
* @api public
|
154
|
+
*/
|
155
|
+
|
156
|
+
DOM : function(results, options) {
|
157
|
+
id = option('reportToId') || 'jspec'
|
158
|
+
report = document.getElementById(id)
|
159
|
+
classes = results.stats.failures ? 'has-failures' : ''
|
160
|
+
if (!report) error('requires the element #' + id + ' to output its reports')
|
161
|
+
|
162
|
+
markup =
|
163
|
+
'<div id="jspec-report" class="' + classes + '"><div class="heading"> \
|
164
|
+
<span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
|
165
|
+
<span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
|
166
|
+
</div><table class="suites">'
|
167
|
+
|
168
|
+
function renderSuite(suite) {
|
169
|
+
failuresOnly = option('failuresOnly')
|
170
|
+
displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
171
|
+
if (displaySuite && suite.hasSpecs()) {
|
172
|
+
markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>'
|
173
|
+
each(suite.specs, function(i, spec){
|
174
|
+
markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
|
175
|
+
if (spec.requiresImplementation() && !failuresOnly) {
|
176
|
+
markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>'
|
177
|
+
}
|
178
|
+
else if (spec.passed() && !failuresOnly) {
|
179
|
+
markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
|
180
|
+
}
|
181
|
+
else if(!spec.passed()) {
|
182
|
+
markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
|
183
|
+
}
|
184
|
+
markup += '<tr class="body" style="display: none;"><td colspan="2">' + spec.body + '</td></tr>'
|
185
|
+
})
|
186
|
+
markup += '</tr>'
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
function renderSuites(suites) {
|
191
|
+
each(suites, function(suite){
|
192
|
+
renderSuite(suite)
|
193
|
+
if (suite.hasSuites()) renderSuites(suite.suites)
|
194
|
+
})
|
195
|
+
}
|
196
|
+
|
197
|
+
renderSuites(results.suites)
|
198
|
+
|
199
|
+
markup += '</table></div>'
|
200
|
+
|
201
|
+
report.innerHTML = markup
|
202
|
+
},
|
203
|
+
|
204
|
+
/**
|
205
|
+
* Console formatter, tested with Firebug and Safari 4.
|
206
|
+
*
|
207
|
+
* @api public
|
208
|
+
*/
|
209
|
+
|
210
|
+
Console : function(results, options) {
|
211
|
+
console.log('')
|
212
|
+
console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
|
213
|
+
|
214
|
+
function renderSuite(suite) {
|
215
|
+
if (suite.ran) {
|
216
|
+
console.group(suite.description)
|
217
|
+
results.each(suite.specs, function(spec){
|
218
|
+
assertionCount = spec.assertions.length + ':'
|
219
|
+
if (spec.requiresImplementation())
|
220
|
+
console.warn(spec.description)
|
221
|
+
else if (spec.passed())
|
222
|
+
console.log(assertionCount + ' ' + spec.description)
|
223
|
+
else
|
224
|
+
console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
|
225
|
+
})
|
226
|
+
console.groupEnd()
|
227
|
+
}
|
228
|
+
}
|
229
|
+
|
230
|
+
function renderSuites(suites) {
|
231
|
+
each(suites, function(suite){
|
232
|
+
renderSuite(suite)
|
233
|
+
if (suite.hasSuites()) renderSuites(suite.suites)
|
234
|
+
})
|
235
|
+
}
|
236
|
+
|
237
|
+
renderSuites(results.suites)
|
238
|
+
}
|
239
|
+
},
|
240
|
+
|
241
|
+
/**
|
242
|
+
* Specification Suite block object.
|
243
|
+
*
|
244
|
+
* @param {string} description
|
245
|
+
* @param {function} body
|
246
|
+
* @api private
|
247
|
+
*/
|
248
|
+
|
249
|
+
Suite : function(description, body) {
|
250
|
+
this.body = body, this.suites = [], this.specs = []
|
251
|
+
this.description = description, this.ran = false
|
252
|
+
this.hooks = { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }
|
253
|
+
|
254
|
+
// Add a spec to the suite
|
255
|
+
|
256
|
+
this.addSpec = function(description, body) {
|
257
|
+
spec = new JSpec.Spec(description, body)
|
258
|
+
this.specs.push(spec)
|
259
|
+
spec.suite = this
|
260
|
+
}
|
261
|
+
|
262
|
+
// Add a hook to the suite
|
263
|
+
|
264
|
+
this.addHook = function(hook, body) {
|
265
|
+
this.hooks[hook].push(body)
|
266
|
+
}
|
267
|
+
|
268
|
+
// Add a nested suite
|
269
|
+
|
270
|
+
this.addSuite = function(description, body) {
|
271
|
+
suite = new JSpec.Suite(description, body)
|
272
|
+
suite.description = this.description + ' ' + suite.description
|
273
|
+
this.suites.push(suite)
|
274
|
+
suite.suite = this
|
275
|
+
}
|
276
|
+
|
277
|
+
// Invoke a hook in context to this suite
|
278
|
+
|
279
|
+
this.hook = function(hook) {
|
280
|
+
each(this.hooks[hook], function(body) {
|
281
|
+
JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
|
282
|
+
})
|
283
|
+
}
|
284
|
+
|
285
|
+
// Check if nested suites are present
|
286
|
+
|
287
|
+
this.hasSuites = function() {
|
288
|
+
return this.suites.length
|
289
|
+
}
|
290
|
+
|
291
|
+
// Check if this suite has specs
|
292
|
+
|
293
|
+
this.hasSpecs = function() {
|
294
|
+
return this.specs.length
|
295
|
+
}
|
296
|
+
|
297
|
+
// Check if the entire suite passed
|
298
|
+
|
299
|
+
this.passed = function() {
|
300
|
+
var passed = true
|
301
|
+
each(this.specs, function(spec){
|
302
|
+
if (!spec.passed()) passed = false
|
303
|
+
})
|
304
|
+
return passed
|
305
|
+
}
|
306
|
+
},
|
307
|
+
|
308
|
+
/**
|
309
|
+
* Specification block object.
|
310
|
+
*
|
311
|
+
* @param {string} description
|
312
|
+
* @param {function} body
|
313
|
+
* @api private
|
314
|
+
*/
|
315
|
+
|
316
|
+
Spec : function(description, body) {
|
317
|
+
this.body = body, this.description = description, this.assertions = []
|
318
|
+
|
319
|
+
// Find first failing assertion
|
320
|
+
|
321
|
+
this.failure = function() {
|
322
|
+
return inject(this.assertions, null, function(failure, assertion){
|
323
|
+
return !assertion.passed && !failure ? assertion : failure
|
324
|
+
})
|
325
|
+
}
|
326
|
+
|
327
|
+
// Find all failing assertions
|
328
|
+
|
329
|
+
this.failures = function() {
|
330
|
+
return inject(this.assertions, [], function(failures, assertion){
|
331
|
+
if (!assertion.passed) failures.push(assertion)
|
332
|
+
return failures
|
333
|
+
})
|
334
|
+
}
|
335
|
+
|
336
|
+
// Weither or not the spec passed
|
337
|
+
|
338
|
+
this.passed = function() {
|
339
|
+
return !this.failure()
|
340
|
+
}
|
341
|
+
|
342
|
+
// Weither or not the spec requires implementation (no assertions)
|
343
|
+
|
344
|
+
this.requiresImplementation = function() {
|
345
|
+
return this.assertions.length == 0
|
346
|
+
}
|
347
|
+
|
348
|
+
// Sprite based assertions graph
|
349
|
+
|
350
|
+
this.assertionsGraph = function() {
|
351
|
+
return map(this.assertions, function(assertion){
|
352
|
+
return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
|
353
|
+
}).join('')
|
354
|
+
}
|
355
|
+
},
|
356
|
+
|
357
|
+
// --- Methods
|
358
|
+
|
359
|
+
/**
|
360
|
+
* Get option value. This method first checks if
|
361
|
+
* the option key has been set via the query string,
|
362
|
+
* otherwise returning the options hash value.
|
363
|
+
*
|
364
|
+
* @param {string} key
|
365
|
+
* @return {mixed}
|
366
|
+
* @api public
|
367
|
+
*/
|
368
|
+
|
369
|
+
option : function(key) {
|
370
|
+
if ((value = query(key)) !== null) return value
|
371
|
+
else return JSpec.options[key] || null
|
372
|
+
},
|
373
|
+
|
374
|
+
/**
|
375
|
+
* Generates a hash of the object passed.
|
376
|
+
*
|
377
|
+
* @param {object} object
|
378
|
+
* @return {string}
|
379
|
+
* @api private
|
380
|
+
*/
|
381
|
+
|
382
|
+
hash : function(object) {
|
383
|
+
serialize = function(prefix) {
|
384
|
+
return inject(object, prefix + ':', function(buffer, key, value){
|
385
|
+
return buffer += hash(value)
|
386
|
+
})
|
387
|
+
}
|
388
|
+
switch (object.constructor) {
|
389
|
+
case Array: return serialize('a')
|
390
|
+
case Object: return serialize('o')
|
391
|
+
case RegExp: return 'r:' + object.toString()
|
392
|
+
case Number: return 'n:' + object.toString()
|
393
|
+
case String: return 's:' + object.toString()
|
394
|
+
default: return object.toString()
|
395
|
+
}
|
396
|
+
},
|
397
|
+
|
398
|
+
/**
|
399
|
+
* Return last element of an array.
|
400
|
+
*
|
401
|
+
* @param {array} array
|
402
|
+
* @return {object}
|
403
|
+
* @api public
|
404
|
+
*/
|
405
|
+
|
406
|
+
last : function(array) {
|
407
|
+
return array[array.length - 1]
|
408
|
+
},
|
409
|
+
|
410
|
+
/**
|
411
|
+
* Convert object(s) to a print-friend string.
|
412
|
+
*
|
413
|
+
* @param {object, ...} object
|
414
|
+
* @return {string}
|
415
|
+
* @api public
|
416
|
+
*/
|
417
|
+
|
418
|
+
print : function(object) {
|
419
|
+
if (arguments.length > 1) {
|
420
|
+
list = []
|
421
|
+
for (i = 0; i < arguments.length; i++) list.push(print(arguments[i]))
|
422
|
+
return list.join(', ')
|
423
|
+
}
|
424
|
+
if (object === undefined) return ''
|
425
|
+
if (object === null) return 'null'
|
426
|
+
if (object === true) return 'true'
|
427
|
+
if (object === false) return 'false'
|
428
|
+
if (object.jquery && object.selector.length > 0) return 'selector ' + print(object.selector) + ''
|
429
|
+
if (object.jquery) return escape(object.html())
|
430
|
+
if (object.nodeName) return escape(object.outerHTML)
|
431
|
+
switch (object.constructor) {
|
432
|
+
case String: return "'" + escape(object) + "'"
|
433
|
+
case Number: return object
|
434
|
+
case Array :
|
435
|
+
buff = '['
|
436
|
+
each(object, function(v){ buff += ', ' + print(v) })
|
437
|
+
return buff.replace('[,', '[') + ' ]'
|
438
|
+
case Object:
|
439
|
+
buff = '{'
|
440
|
+
each(object, function(k, v){ buff += ', ' + print(k) + ' : ' + print(v)})
|
441
|
+
return buff.replace('{,', '{') + ' }'
|
442
|
+
default:
|
443
|
+
return escape(object.toString())
|
444
|
+
}
|
445
|
+
},
|
446
|
+
|
447
|
+
/**
|
448
|
+
* Escape HTML.
|
449
|
+
*
|
450
|
+
* @param {string} html
|
451
|
+
* @return {string}
|
452
|
+
* @api public
|
453
|
+
*/
|
454
|
+
|
455
|
+
escape : function(html) {
|
456
|
+
if (typeof html != 'string') return html
|
457
|
+
return html.
|
458
|
+
replace(/&/gmi, '&').
|
459
|
+
replace(/"/gmi, '"').
|
460
|
+
replace(/>/gmi, '>').
|
461
|
+
replace(/</gmi, '<')
|
462
|
+
},
|
463
|
+
|
464
|
+
/**
|
465
|
+
* Invoke a matcher.
|
466
|
+
*
|
467
|
+
* this.match('test', 'should', 'be_a', [String])
|
468
|
+
*
|
469
|
+
* @param {object} actual
|
470
|
+
* @param {bool, string} negate
|
471
|
+
* @param {string} name
|
472
|
+
* @param {array} expected
|
473
|
+
* @return {bool}
|
474
|
+
* @api private
|
475
|
+
*/
|
476
|
+
|
477
|
+
match : function(actual, negate, name, expected) {
|
478
|
+
if (typeof negate == 'string') negate = negate == 'should' ? false : true
|
479
|
+
matcher = new this.Matcher(name, this.matchers[name], actual, expected, negate)
|
480
|
+
this.currentSpec.assertions.push(matcher.exec())
|
481
|
+
return matcher.result
|
482
|
+
},
|
483
|
+
|
484
|
+
/**
|
485
|
+
* Iterate an object, invoking the given callback.
|
486
|
+
*
|
487
|
+
* @param {hash, array, string} object
|
488
|
+
* @param {function} callback
|
489
|
+
* @return {JSpec}
|
490
|
+
* @api public
|
491
|
+
*/
|
492
|
+
|
493
|
+
each : function(object, callback) {
|
494
|
+
if (typeof object == 'string') object = object.split(' ')
|
495
|
+
for (key in object) {
|
496
|
+
if (object.hasOwnProperty(key))
|
497
|
+
callback.length == 1 ?
|
498
|
+
callback.call(JSpec, object[key]):
|
499
|
+
callback.call(JSpec, key, object[key])
|
500
|
+
}
|
501
|
+
return JSpec
|
502
|
+
},
|
503
|
+
|
504
|
+
/**
|
505
|
+
* Iterate with memo.
|
506
|
+
*
|
507
|
+
* @param {hash, array} object
|
508
|
+
* @param {object} initial
|
509
|
+
* @param {function} callback
|
510
|
+
* @return {object}
|
511
|
+
* @api public
|
512
|
+
*/
|
513
|
+
|
514
|
+
inject : function(object, initial, callback) {
|
515
|
+
each(object, function(key, value){
|
516
|
+
initial = callback.length == 2 ?
|
517
|
+
callback.call(JSpec, initial, value):
|
518
|
+
callback.call(JSpec, initial, key, value) || initial
|
519
|
+
})
|
520
|
+
return initial
|
521
|
+
},
|
522
|
+
|
523
|
+
/**
|
524
|
+
* Strim whitespace or chars.
|
525
|
+
*
|
526
|
+
* @param {string} string
|
527
|
+
* @param {string} chars
|
528
|
+
* @return {string}
|
529
|
+
* @api public
|
530
|
+
*/
|
531
|
+
|
532
|
+
strip : function(string, chars) {
|
533
|
+
return string.
|
534
|
+
replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
|
535
|
+
replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
|
536
|
+
},
|
537
|
+
|
538
|
+
/**
|
539
|
+
* Map callback return values.
|
540
|
+
*
|
541
|
+
* @param {hash, array} object
|
542
|
+
* @param {function} callback
|
543
|
+
* @return {array}
|
544
|
+
* @api public
|
545
|
+
*/
|
546
|
+
|
547
|
+
map : function(object, callback) {
|
548
|
+
return inject(object, [], function(memo, key, value){
|
549
|
+
memo.push(callback.length == 1 ?
|
550
|
+
callback.call(JSpec, value):
|
551
|
+
callback.call(JSpec, key, value))
|
552
|
+
})
|
553
|
+
},
|
554
|
+
|
555
|
+
/**
|
556
|
+
* Returns true if the callback returns true at least once.
|
557
|
+
*
|
558
|
+
* @param {hash, array} object
|
559
|
+
* @param {function} callback
|
560
|
+
* @return {bool}
|
561
|
+
* @api public
|
562
|
+
*/
|
563
|
+
|
564
|
+
any : function(object, callback) {
|
565
|
+
return inject(object, false, function(state, key, value){
|
566
|
+
if (state) return true
|
567
|
+
return callback.length == 1 ?
|
568
|
+
callback.call(JSpec, value):
|
569
|
+
callback.call(JSpec, key, value)
|
570
|
+
})
|
571
|
+
},
|
572
|
+
|
573
|
+
/**
|
574
|
+
* Define matchers.
|
575
|
+
*
|
576
|
+
* @param {hash} matchers
|
577
|
+
* @return {JSpec}
|
578
|
+
* @api public
|
579
|
+
*/
|
580
|
+
|
581
|
+
addMatchers : function(matchers) {
|
582
|
+
each(matchers, function(name, body){ this.matchers[name] = body })
|
583
|
+
return this
|
584
|
+
},
|
585
|
+
|
586
|
+
/**
|
587
|
+
* Add a root suite to JSpec.
|
588
|
+
*
|
589
|
+
* @param {string} description
|
590
|
+
* @param {body} function
|
591
|
+
* @return {JSpec}
|
592
|
+
* @api public
|
593
|
+
*/
|
594
|
+
|
595
|
+
addSuite : function(description, body) {
|
596
|
+
this.suites.push(new JSpec.Suite(description, body))
|
597
|
+
return this
|
598
|
+
},
|
599
|
+
|
600
|
+
/**
|
601
|
+
* Evaluate a JSpec capture body.
|
602
|
+
*
|
603
|
+
* @param {function} body
|
604
|
+
* @param {string} errorMessage (optional)
|
605
|
+
* @return {Type}
|
606
|
+
* @api private
|
607
|
+
*/
|
608
|
+
|
609
|
+
evalBody : function(body, errorMessage) {
|
610
|
+
try { body.call(this.context || this.defaultContext) }
|
611
|
+
catch(e) { error(errorMessage, e) }
|
612
|
+
},
|
613
|
+
|
614
|
+
/**
|
615
|
+
* Pre-process a string of JSpec.
|
616
|
+
*
|
617
|
+
* @param {string} input
|
618
|
+
* @return {string}
|
619
|
+
* @api private
|
620
|
+
*/
|
621
|
+
|
622
|
+
preprocess : function(input) {
|
623
|
+
return input.
|
624
|
+
replace(/describe (.*?)$/m, 'JSpec.addSuite($1, function(){').
|
625
|
+
replace(/describe (.*?)$/gm, 'this.addSuite($1, function(){').
|
626
|
+
replace(/it (.*?)$/gm, 'this.addSpec($1, function(){').
|
627
|
+
replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'this.addHook("$1", function(){').
|
628
|
+
replace(/end(?= |\n|$)/gm, '});').
|
629
|
+
replace(/-{/g, 'function(){').
|
630
|
+
replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
|
631
|
+
replace(/([\s\(]+)\./gm, '$1this.').
|
632
|
+
replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)').
|
633
|
+
replace(/(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, 'JSpec.match($1, "$2", "$3", [$4]);')
|
634
|
+
},
|
635
|
+
|
636
|
+
/**
|
637
|
+
* Create a range string which can be evaluated to a native array.
|
638
|
+
*
|
639
|
+
* @param {int} start
|
640
|
+
* @param {int} end
|
641
|
+
* @return {string}
|
642
|
+
* @api public
|
643
|
+
*/
|
644
|
+
|
645
|
+
range : function(start, end) {
|
646
|
+
current = parseInt(start), end = parseInt(end), values = [current]
|
647
|
+
if (end > current) while (++current <= end) values.push(current)
|
648
|
+
else while (--current >= end) values.push(current)
|
649
|
+
return '[' + values + ']'
|
650
|
+
},
|
651
|
+
|
652
|
+
/**
|
653
|
+
* Report on the results.
|
654
|
+
*
|
655
|
+
* @return {JSpec}
|
656
|
+
* @api public
|
657
|
+
*/
|
658
|
+
|
659
|
+
report : function() {
|
660
|
+
this.options.formatter ?
|
661
|
+
new this.options.formatter(this, this.options):
|
662
|
+
new this.formatters.DOM(this, this.options)
|
663
|
+
return this
|
664
|
+
},
|
665
|
+
|
666
|
+
/**
|
667
|
+
* Run the spec suites.
|
668
|
+
*
|
669
|
+
* @return {JSpec}
|
670
|
+
* @api public
|
671
|
+
*/
|
672
|
+
|
673
|
+
run : function() {
|
674
|
+
if (option('profile')) console.group('Profile')
|
675
|
+
each(this.suites, function(suite) { this.runSuite(suite) })
|
676
|
+
if (option('profile')) console.groupEnd()
|
677
|
+
return this
|
678
|
+
},
|
679
|
+
|
680
|
+
/**
|
681
|
+
* Run a suite.
|
682
|
+
*
|
683
|
+
* @param {Suite} suite
|
684
|
+
* @return {JSpec}
|
685
|
+
* @api public
|
686
|
+
*/
|
687
|
+
|
688
|
+
runSuite : function(suite) {
|
689
|
+
suite.body()
|
690
|
+
suite.ran = true
|
691
|
+
suite.hook('before')
|
692
|
+
each(suite.specs, function(spec) {
|
693
|
+
suite.hook('before_each')
|
694
|
+
this.runSpec(spec)
|
695
|
+
suite.hook('after_each')
|
696
|
+
})
|
697
|
+
suite.hook('after')
|
698
|
+
if (suite.hasSuites()) {
|
699
|
+
each(suite.suites, function(suite) {
|
700
|
+
this.runSuite(suite)
|
701
|
+
})
|
702
|
+
}
|
703
|
+
return this
|
704
|
+
},
|
705
|
+
|
706
|
+
/**
|
707
|
+
* Run a spec.
|
708
|
+
*
|
709
|
+
* @param {Spec} spec
|
710
|
+
* @api public
|
711
|
+
*/
|
712
|
+
|
713
|
+
runSpec : function(spec) {
|
714
|
+
this.currentSpec = spec
|
715
|
+
this.stats.specs++
|
716
|
+
if (option('profile')) console.time(spec.description)
|
717
|
+
this.evalBody(spec.body, "Error in spec '" + spec.description + "': ")
|
718
|
+
if (option('profile')) console.timeEnd(spec.description)
|
719
|
+
this.stats.assertions += spec.assertions.length
|
720
|
+
},
|
721
|
+
|
722
|
+
/**
|
723
|
+
* Require a dependency, with optional message.
|
724
|
+
*
|
725
|
+
* @param {string} dependency
|
726
|
+
* @param {string} message (optional)
|
727
|
+
* @api public
|
728
|
+
*/
|
729
|
+
|
730
|
+
requires : function(dependency, message) {
|
731
|
+
try { eval(dependency) }
|
732
|
+
catch (e) { error('depends on ' + dependency + ' ' + (message || '')) }
|
733
|
+
},
|
734
|
+
|
735
|
+
/**
|
736
|
+
* Query against the current query strings keys
|
737
|
+
* or the queryString specified.
|
738
|
+
*
|
739
|
+
* @param {string} key
|
740
|
+
* @param {string} queryString
|
741
|
+
* @return {string, null}
|
742
|
+
* @api public
|
743
|
+
*/
|
744
|
+
|
745
|
+
query : function(key, queryString) {
|
746
|
+
queryString = (queryString || window.location.search || '').substring(1)
|
747
|
+
return inject(queryString.split('&'), null, function(value, pair){
|
748
|
+
parts = pair.split('=')
|
749
|
+
return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
|
750
|
+
})
|
751
|
+
},
|
752
|
+
|
753
|
+
/**
|
754
|
+
* Throw a JSpec related error.
|
755
|
+
*
|
756
|
+
* @param {string} message
|
757
|
+
* @param {Exception} e
|
758
|
+
* @api public
|
759
|
+
*/
|
760
|
+
|
761
|
+
error : function(message, e) {
|
762
|
+
throw 'jspec: ' + message + (e ? e.message : '') + ' near line ' + e.line
|
763
|
+
},
|
764
|
+
|
765
|
+
/**
|
766
|
+
* Load a files contents.
|
767
|
+
*
|
768
|
+
* @param {string} file
|
769
|
+
* @return {string}
|
770
|
+
* @api public
|
771
|
+
*/
|
772
|
+
|
773
|
+
load : function(file) {
|
774
|
+
if ('XMLHttpRequest' in this.main) {
|
775
|
+
request = new XMLHttpRequest
|
776
|
+
request.open('GET', file, false)
|
777
|
+
request.send(null)
|
778
|
+
if (request.readyState == 4) return request.responseText
|
779
|
+
}
|
780
|
+
else if ('load' in this.main) {
|
781
|
+
// TODO: workaround for IO issue / preprocessing
|
782
|
+
load(file)
|
783
|
+
}
|
784
|
+
else {
|
785
|
+
error('cannot load ' + file)
|
786
|
+
}
|
787
|
+
},
|
788
|
+
|
789
|
+
/**
|
790
|
+
* Load, pre-process, and evaluate a file.
|
791
|
+
*
|
792
|
+
* @param {string} file
|
793
|
+
* @param {JSpec}
|
794
|
+
* @api public
|
795
|
+
*/
|
796
|
+
|
797
|
+
exec : function(file) {
|
798
|
+
eval(this.preprocess(this.load(file)))
|
799
|
+
return this
|
800
|
+
}
|
801
|
+
}
|
802
|
+
|
803
|
+
// --- Utility functions
|
804
|
+
|
805
|
+
map = JSpec.map
|
806
|
+
any = JSpec.any
|
807
|
+
last = JSpec.last
|
808
|
+
range = JSpec.range
|
809
|
+
each = JSpec.each
|
810
|
+
option = JSpec.option
|
811
|
+
inject = JSpec.inject
|
812
|
+
error = JSpec.error
|
813
|
+
escape = JSpec.escape
|
814
|
+
print = JSpec.print
|
815
|
+
hash = JSpec.hash
|
816
|
+
query = JSpec.query
|
817
|
+
strip = JSpec.strip
|
818
|
+
addMatchers = JSpec.addMatchers
|
819
|
+
|
820
|
+
// --- Matchers
|
821
|
+
|
822
|
+
addMatchers({
|
823
|
+
be : "alias eql",
|
824
|
+
equal : "===",
|
825
|
+
be_greater_than : ">",
|
826
|
+
be_less_than : "<",
|
827
|
+
be_at_least : ">=",
|
828
|
+
be_at_most : "<=",
|
829
|
+
be_a : "actual.constructor == expected",
|
830
|
+
be_an : "alias be_a",
|
831
|
+
be_null : "actual == null",
|
832
|
+
be_empty : "actual.length == 0",
|
833
|
+
be_true : "actual == true",
|
834
|
+
be_false : "actual == false",
|
835
|
+
be_type : "typeof actual == expected",
|
836
|
+
match : "typeof actual == 'string' ? actual.match(expected) : false",
|
837
|
+
respond_to : "typeof actual[expected] == 'function'",
|
838
|
+
have_length : "actual.length == expected",
|
839
|
+
be_within : "actual >= expected[0] && actual <= last(expected)",
|
840
|
+
have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
|
841
|
+
|
842
|
+
eql : { match : function(actual, expected) {
|
843
|
+
if (actual.constructor == Array || actual.constructor == Object) return hash(actual) == hash(expected)
|
844
|
+
else return actual == expected
|
845
|
+
}},
|
846
|
+
|
847
|
+
include : { match : function(actual) {
|
848
|
+
for (state = true, i = 1; i < arguments.length; i++) {
|
849
|
+
arg = arguments[i]
|
850
|
+
switch (actual.constructor) {
|
851
|
+
case String:
|
852
|
+
case Number:
|
853
|
+
case RegExp:
|
854
|
+
case Function:
|
855
|
+
state = actual.toString().match(arg.toString())
|
856
|
+
break
|
857
|
+
|
858
|
+
case Object:
|
859
|
+
state = arg in actual
|
860
|
+
break
|
861
|
+
|
862
|
+
case Array:
|
863
|
+
state = any(actual, function(value){ return hash(value) == hash(arg) })
|
864
|
+
break
|
865
|
+
}
|
866
|
+
if (!state) return false
|
867
|
+
}
|
868
|
+
return true
|
869
|
+
}},
|
870
|
+
|
871
|
+
throw_error : { match : function(actual, expected) {
|
872
|
+
try { actual() }
|
873
|
+
catch (e) {
|
874
|
+
if (expected == undefined) return true
|
875
|
+
else return expected.constructor == RegExp ?
|
876
|
+
expected.test(e) : e.toString() == expected
|
877
|
+
}
|
878
|
+
}},
|
879
|
+
|
880
|
+
have : { match : function(actual, length, property) {
|
881
|
+
return actual[property].length == length
|
882
|
+
}},
|
883
|
+
|
884
|
+
have_at_least : { match : function(actual, length, property) {
|
885
|
+
return actual[property].length >= length
|
886
|
+
}},
|
887
|
+
|
888
|
+
have_at_most : { match : function(actual, length, property) {
|
889
|
+
return actual[property].length <= length
|
890
|
+
}},
|
891
|
+
|
892
|
+
have_within : { match : function(actual, range, property) {
|
893
|
+
length = actual[property].length
|
894
|
+
return length >= range.shift() && length <= range.pop()
|
895
|
+
}},
|
896
|
+
|
897
|
+
have_prop : { match : function(actual, property, value) {
|
898
|
+
if (actual[property] == null || typeof actual[property] == 'function') return false
|
899
|
+
return value == null ? true : JSpec.matchers['eql'].match(actual[property], value)
|
900
|
+
}},
|
901
|
+
|
902
|
+
have_property : { match : function(actual, property, value) {
|
903
|
+
if (actual[property] == null || typeof actual[property] == 'function') return false
|
904
|
+
return value == null ? true : value === actual[property]
|
905
|
+
}}
|
906
|
+
})
|
907
|
+
|
908
|
+
// --- Expose
|
909
|
+
|
910
|
+
this.JSpec = JSpec
|
911
|
+
|
912
|
+
})();
|