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.
@@ -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
@@ -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'
@@ -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
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -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
+ }
@@ -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
+
@@ -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, '&amp;').
459
+ replace(/"/gmi, '&quot;').
460
+ replace(/>/gmi, '&gt;').
461
+ replace(/</gmi, '&lt;')
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
+ })();