visionmedia-jspec 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
})();
|