visionmedia-jspec 1.1.1

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