raus22-jspec 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.rdoc +323 -0
- data/Manifest +44 -0
- data/README.rdoc +495 -0
- data/Rakefile +72 -0
- data/bin/jspec +137 -0
- data/jspec.gemspec +35 -0
- data/lib/images/bg.png +0 -0
- data/lib/images/hr.png +0 -0
- data/lib/images/loading.gif +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 +145 -0
- data/lib/jspec.jquery.js +64 -0
- data/lib/jspec.js +1468 -0
- data/server/browsers.rb +16 -0
- data/server/server.rb +103 -0
- data/spec/async +1 -0
- data/spec/jquery-1.3.1.js +4241 -0
- data/spec/server.html +23 -0
- data/spec/spec.grammar-less.js +34 -0
- data/spec/spec.grammar.js +179 -0
- data/spec/spec.html +27 -0
- data/spec/spec.jquery.js +166 -0
- data/spec/spec.js +119 -0
- data/spec/spec.matchers.js +382 -0
- data/spec/spec.rhino.js +12 -0
- data/spec/spec.shared-behaviors.js +51 -0
- data/spec/spec.utils.js +138 -0
- data/templates/default/History.rdoc +4 -0
- data/templates/default/README.rdoc +29 -0
- data/templates/default/lib/yourlib.core.js +2 -0
- data/templates/default/spec/spec.core.js +8 -0
- data/templates/default/spec/spec.html +20 -0
- data/templates/rhino/History.rdoc +4 -0
- data/templates/rhino/README.rdoc +29 -0
- data/templates/rhino/lib/yourlib.core.js +2 -0
- data/templates/rhino/spec/spec.core.js +8 -0
- data/templates/rhino/spec/spec.js +7 -0
- data/templates/server/History.rdoc +4 -0
- data/templates/server/README.rdoc +29 -0
- data/templates/server/lib/yourlib.core.js +2 -0
- data/templates/server/spec/spec.core.js +8 -0
- data/templates/server/spec/spec.html +15 -0
- metadata +120 -0
data/lib/jspec.js
ADDED
@@ -0,0 +1,1468 @@
|
|
1
|
+
|
2
|
+
// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
3
|
+
|
4
|
+
(function(){
|
5
|
+
|
6
|
+
JSpec = {
|
7
|
+
|
8
|
+
version : '2.0.3',
|
9
|
+
suites : [],
|
10
|
+
allSuites : [],
|
11
|
+
matchers : {},
|
12
|
+
stats : { specs : 0, assertions : 0, failures : 0, passes : 0, specsFinished : 0, suitesFinished : 0 },
|
13
|
+
options : { profile : false },
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Default context in which bodies are evaluated.
|
17
|
+
*
|
18
|
+
* Replace context simply by setting JSpec.context
|
19
|
+
* to your own like below:
|
20
|
+
*
|
21
|
+
* JSpec.context = { foo : 'bar' }
|
22
|
+
*
|
23
|
+
* Contexts can be changed within any body, this can be useful
|
24
|
+
* in order to provide specific helper methods to specific suites.
|
25
|
+
*
|
26
|
+
* To reset (usually in after hook) simply set to null like below:
|
27
|
+
*
|
28
|
+
* JSpec.context = null
|
29
|
+
*
|
30
|
+
*/
|
31
|
+
|
32
|
+
defaultContext : {
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Return an object used for proxy assertions.
|
36
|
+
* This object is used to indicate that an object
|
37
|
+
* should be an instance of _object_, not the constructor
|
38
|
+
* itself.
|
39
|
+
*
|
40
|
+
* @param {function} constructor
|
41
|
+
* @return {hash}
|
42
|
+
* @api public
|
43
|
+
*/
|
44
|
+
|
45
|
+
an_instance_of : function(constructor) {
|
46
|
+
return { an_instance_of : constructor }
|
47
|
+
},
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Sets the current spec's wait duration to _n_.
|
51
|
+
*
|
52
|
+
* wait(3000)
|
53
|
+
* wait(1, 'second')
|
54
|
+
* wait(3, 'seconds')
|
55
|
+
*
|
56
|
+
* @param {number} n
|
57
|
+
* @param {string} unit
|
58
|
+
* @api public
|
59
|
+
*/
|
60
|
+
|
61
|
+
wait : function(n, unit) {
|
62
|
+
JSpec.currentSpec.wait = {
|
63
|
+
'second' : n * 1000,
|
64
|
+
'seconds' : n * 1000
|
65
|
+
}[unit] || n
|
66
|
+
}
|
67
|
+
},
|
68
|
+
|
69
|
+
// --- Objects
|
70
|
+
|
71
|
+
formatters : {
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Default formatter, outputting to the DOM.
|
75
|
+
*
|
76
|
+
* Options:
|
77
|
+
* - reportToId id of element to output reports to, defaults to 'jspec'
|
78
|
+
* - failuresOnly displays only suites with failing specs
|
79
|
+
*
|
80
|
+
* @api public
|
81
|
+
*/
|
82
|
+
|
83
|
+
DOM : function(results, options) {
|
84
|
+
var id = option('reportToId') || 'jspec'
|
85
|
+
var report = document.getElementById(id)
|
86
|
+
var failuresOnly = option('failuresOnly')
|
87
|
+
var classes = results.stats.failures ? 'has-failures' : ''
|
88
|
+
if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
|
89
|
+
|
90
|
+
var markup =
|
91
|
+
'<div id="jspec-report" class="' + classes + '"><div class="heading"> \
|
92
|
+
<span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
|
93
|
+
<span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
|
94
|
+
</div><table class="suites">'
|
95
|
+
|
96
|
+
bodyContents = function(body) {
|
97
|
+
return JSpec.
|
98
|
+
escape(JSpec.contentsOf(body)).
|
99
|
+
replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
|
100
|
+
replace("\n", '<br/>')
|
101
|
+
}
|
102
|
+
|
103
|
+
renderSuite = function(suite) {
|
104
|
+
var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
105
|
+
if (displaySuite && suite.hasSpecs()) {
|
106
|
+
markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>'
|
107
|
+
each(suite.specs, function(i, spec){
|
108
|
+
markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
|
109
|
+
if (spec.requiresImplementation())
|
110
|
+
markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>'
|
111
|
+
else if (spec.passed() && !failuresOnly)
|
112
|
+
markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
|
113
|
+
else if(!spec.passed())
|
114
|
+
markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
|
115
|
+
markup += '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
|
116
|
+
})
|
117
|
+
markup += '</tr>'
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
renderSuites = function(suites) {
|
122
|
+
each(suites, function(suite){
|
123
|
+
renderSuite(suite)
|
124
|
+
if (suite.hasSuites()) renderSuites(suite.suites)
|
125
|
+
})
|
126
|
+
}
|
127
|
+
|
128
|
+
renderSuites(results.suites)
|
129
|
+
markup += '</table></div>'
|
130
|
+
report.innerHTML = markup
|
131
|
+
},
|
132
|
+
|
133
|
+
/**
|
134
|
+
* Terminal formatter.
|
135
|
+
*
|
136
|
+
* @api public
|
137
|
+
*/
|
138
|
+
|
139
|
+
Terminal : function(results, options) {
|
140
|
+
failuresOnly = option('failuresOnly')
|
141
|
+
print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
|
142
|
+
color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n")
|
143
|
+
|
144
|
+
indent = function(string) {
|
145
|
+
return string.replace(/^(.)/gm, ' $1')
|
146
|
+
}
|
147
|
+
|
148
|
+
renderSuite = function(suite) {
|
149
|
+
displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
150
|
+
if (displaySuite && suite.hasSpecs()) {
|
151
|
+
print(color(' ' + suite.description, 'bold'))
|
152
|
+
each(suite.specs, function(spec){
|
153
|
+
var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
|
154
|
+
return graph + color('.', assertion.passed ? 'green' : 'red')
|
155
|
+
})
|
156
|
+
if (spec.requiresImplementation())
|
157
|
+
print(color(' ' + spec.description, 'blue') + assertionsGraph)
|
158
|
+
else if (spec.passed() && !failuresOnly)
|
159
|
+
print(color(' ' + spec.description, 'green') + assertionsGraph)
|
160
|
+
else if (!spec.passed())
|
161
|
+
print(color(' ' + spec.description, 'red') + assertionsGraph +
|
162
|
+
"\n" + indent(spec.failure().message) + "\n")
|
163
|
+
})
|
164
|
+
print("")
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
renderSuites = function(suites) {
|
169
|
+
each(suites, function(suite){
|
170
|
+
renderSuite(suite)
|
171
|
+
if (suite.hasSuites()) renderSuites(suite.suites)
|
172
|
+
})
|
173
|
+
}
|
174
|
+
|
175
|
+
renderSuites(results.suites)
|
176
|
+
},
|
177
|
+
|
178
|
+
/**
|
179
|
+
* Console formatter, tested with Firebug and Safari 4.
|
180
|
+
*
|
181
|
+
* @api public
|
182
|
+
*/
|
183
|
+
|
184
|
+
Console : function(results, options) {
|
185
|
+
console.log('')
|
186
|
+
console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
|
187
|
+
|
188
|
+
renderSuite = function(suite) {
|
189
|
+
if (suite.ran) {
|
190
|
+
console.group(suite.description)
|
191
|
+
each(suite.specs, function(spec){
|
192
|
+
var assertionCount = spec.assertions.length + ':'
|
193
|
+
if (spec.requiresImplementation())
|
194
|
+
console.warn(spec.description)
|
195
|
+
else if (spec.passed())
|
196
|
+
console.log(assertionCount + ' ' + spec.description)
|
197
|
+
else
|
198
|
+
console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
|
199
|
+
})
|
200
|
+
console.groupEnd()
|
201
|
+
}
|
202
|
+
}
|
203
|
+
|
204
|
+
renderSuites = function(suites) {
|
205
|
+
each(suites, function(suite){
|
206
|
+
renderSuite(suite)
|
207
|
+
if (suite.hasSuites()) renderSuites(suite.suites)
|
208
|
+
})
|
209
|
+
}
|
210
|
+
|
211
|
+
renderSuites(results.suites)
|
212
|
+
}
|
213
|
+
},
|
214
|
+
|
215
|
+
Assertion : function(matcher, actual, expected, negate) {
|
216
|
+
extend(this, {
|
217
|
+
message : '',
|
218
|
+
passed : false,
|
219
|
+
actual : actual,
|
220
|
+
negate : negate,
|
221
|
+
matcher : matcher,
|
222
|
+
expected : expected,
|
223
|
+
|
224
|
+
// Report assertion results
|
225
|
+
|
226
|
+
report : function() {
|
227
|
+
this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
|
228
|
+
return this
|
229
|
+
},
|
230
|
+
|
231
|
+
// Run the assertion
|
232
|
+
|
233
|
+
run : function() {
|
234
|
+
// TODO: remove unshifting
|
235
|
+
expected.unshift(actual)
|
236
|
+
this.result = matcher.match.apply(this, expected)
|
237
|
+
this.passed = negate ? !this.result : this.result
|
238
|
+
if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
|
239
|
+
return this
|
240
|
+
}
|
241
|
+
})
|
242
|
+
},
|
243
|
+
|
244
|
+
ProxyAssertion : function(object, method, times) {
|
245
|
+
var self = this
|
246
|
+
var old = object[method]
|
247
|
+
|
248
|
+
// Proxy
|
249
|
+
|
250
|
+
object[method] = function(){
|
251
|
+
args = argumentsToArray(arguments)
|
252
|
+
result = old.apply(object, args)
|
253
|
+
self.calls.push({ args : args, result : result })
|
254
|
+
return result
|
255
|
+
}
|
256
|
+
|
257
|
+
// Times
|
258
|
+
|
259
|
+
this.times = {
|
260
|
+
'once' : 1,
|
261
|
+
'twice' : 2
|
262
|
+
}[times] || times || 1
|
263
|
+
|
264
|
+
// TODO: negation
|
265
|
+
|
266
|
+
extend(this, {
|
267
|
+
calls : [],
|
268
|
+
message : '',
|
269
|
+
defer : true,
|
270
|
+
passed : false,
|
271
|
+
object : object,
|
272
|
+
method : method,
|
273
|
+
|
274
|
+
// Proxy return value
|
275
|
+
|
276
|
+
and_return : function(result) {
|
277
|
+
this.expectedResult = result
|
278
|
+
return this
|
279
|
+
},
|
280
|
+
|
281
|
+
// Proxy arguments passed
|
282
|
+
|
283
|
+
with_args : function() {
|
284
|
+
this.expectedArgs = argumentsToArray(arguments)
|
285
|
+
return this
|
286
|
+
},
|
287
|
+
|
288
|
+
// Check if any calls have failing results
|
289
|
+
|
290
|
+
anyResultsFail : function() {
|
291
|
+
return any(this.calls, function(call){
|
292
|
+
return self.expectedResult.an_instance_of ?
|
293
|
+
call.result.constructor != self.expectedResult.an_instance_of:
|
294
|
+
hash(self.expectedResult) != hash(call.result)
|
295
|
+
})
|
296
|
+
},
|
297
|
+
|
298
|
+
// Return the failing result
|
299
|
+
|
300
|
+
failingResult : function() {
|
301
|
+
return this.anyResultsFail().result
|
302
|
+
},
|
303
|
+
|
304
|
+
// Check if any arguments fail
|
305
|
+
|
306
|
+
anyArgsFail : function() {
|
307
|
+
return any(this.calls, function(call){
|
308
|
+
return any(self.expectedArgs, function(i, arg){
|
309
|
+
return arg.an_instance_of ?
|
310
|
+
call.args[i].constructor != arg.an_instance_of:
|
311
|
+
hash(arg) != hash(call.args[i])
|
312
|
+
|
313
|
+
})
|
314
|
+
})
|
315
|
+
},
|
316
|
+
|
317
|
+
// Return the failing args
|
318
|
+
|
319
|
+
failingArgs : function() {
|
320
|
+
return this.anyArgsFail().args
|
321
|
+
},
|
322
|
+
|
323
|
+
// Report assertion results
|
324
|
+
|
325
|
+
report : function() {
|
326
|
+
this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
|
327
|
+
return this
|
328
|
+
},
|
329
|
+
|
330
|
+
// Run the assertion
|
331
|
+
|
332
|
+
run : function() {
|
333
|
+
var methodString = 'expected ' + object.toString() + '.' + method + '()'
|
334
|
+
|
335
|
+
function times(n) {
|
336
|
+
return n > 2 ? n + ' times' : { 1 : 'once', 2 : 'twice' }[n]
|
337
|
+
}
|
338
|
+
|
339
|
+
if (this.calls.length < this.times)
|
340
|
+
this.message = methodString + ' to be called ' + times(this.times) +
|
341
|
+
', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
|
342
|
+
|
343
|
+
if (this.expectedResult && this.anyResultsFail())
|
344
|
+
this.message = methodString + ' to return ' + puts(this.expectedResult) +
|
345
|
+
' but got ' + puts(this.failingResult())
|
346
|
+
|
347
|
+
if (this.expectedArgs && this.anyArgsFail())
|
348
|
+
this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
|
349
|
+
' but was called with ' + puts.apply(this, this.failingArgs())
|
350
|
+
|
351
|
+
if (!this.message.length)
|
352
|
+
this.passed = true
|
353
|
+
|
354
|
+
return this
|
355
|
+
}
|
356
|
+
})
|
357
|
+
},
|
358
|
+
|
359
|
+
/**
|
360
|
+
* Specification Suite block object.
|
361
|
+
*
|
362
|
+
* @param {string} description
|
363
|
+
* @param {function} body
|
364
|
+
* @api private
|
365
|
+
*/
|
366
|
+
|
367
|
+
Suite : function(description, body) {
|
368
|
+
var self = this
|
369
|
+
extend(this, {
|
370
|
+
body: body,
|
371
|
+
description: description,
|
372
|
+
suites: [],
|
373
|
+
specs: [],
|
374
|
+
ran: false,
|
375
|
+
hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
|
376
|
+
|
377
|
+
// Add a spec to the suite
|
378
|
+
|
379
|
+
addSpec : function(description, body) {
|
380
|
+
var spec = new JSpec.Spec(description, body)
|
381
|
+
this.specs.push(spec)
|
382
|
+
JSpec.stats.specs++ // TODO: abstract
|
383
|
+
spec.suite = this
|
384
|
+
},
|
385
|
+
|
386
|
+
// Add a hook to the suite
|
387
|
+
|
388
|
+
addHook : function(hook, body) {
|
389
|
+
this.hooks[hook].push(body)
|
390
|
+
},
|
391
|
+
|
392
|
+
// Add a nested suite
|
393
|
+
|
394
|
+
addSuite : function(description, body) {
|
395
|
+
var suite = new JSpec.Suite(description, body)
|
396
|
+
JSpec.allSuites.push(suite)
|
397
|
+
suite.name = suite.description
|
398
|
+
suite.description = this.description + ' ' + suite.description
|
399
|
+
this.suites.push(suite)
|
400
|
+
suite.suite = this
|
401
|
+
},
|
402
|
+
|
403
|
+
// Invoke a hook in context to this suite
|
404
|
+
|
405
|
+
hook : function(hook) {
|
406
|
+
if (this.suite) this.suite.hook(hook)
|
407
|
+
each(this.hooks[hook], function(body) {
|
408
|
+
JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
|
409
|
+
})
|
410
|
+
},
|
411
|
+
|
412
|
+
// Check if nested suites are present
|
413
|
+
|
414
|
+
hasSuites : function() {
|
415
|
+
return this.suites.length
|
416
|
+
},
|
417
|
+
|
418
|
+
// Check if this suite has specs
|
419
|
+
|
420
|
+
hasSpecs : function() {
|
421
|
+
return this.specs.length
|
422
|
+
},
|
423
|
+
|
424
|
+
// Check if the entire suite passed
|
425
|
+
|
426
|
+
passed : function() {
|
427
|
+
return !any(this.specs, function(spec){
|
428
|
+
return !spec.passed()
|
429
|
+
})
|
430
|
+
}
|
431
|
+
})
|
432
|
+
},
|
433
|
+
|
434
|
+
/**
|
435
|
+
* Specification block object.
|
436
|
+
*
|
437
|
+
* @param {string} description
|
438
|
+
* @param {function} body
|
439
|
+
* @api private
|
440
|
+
*/
|
441
|
+
|
442
|
+
Spec : function(description, body) {
|
443
|
+
extend(this, {
|
444
|
+
body : body,
|
445
|
+
description : description,
|
446
|
+
assertions : [],
|
447
|
+
|
448
|
+
// Run deferred assertions
|
449
|
+
|
450
|
+
runDeferredAssertions : function() {
|
451
|
+
each(this.assertions, function(assertion){
|
452
|
+
if (assertion.defer) assertion.run().report()
|
453
|
+
})
|
454
|
+
},
|
455
|
+
|
456
|
+
// Find first failing assertion
|
457
|
+
|
458
|
+
failure : function() {
|
459
|
+
return find(this.assertions, function(assertion){
|
460
|
+
return !assertion.passed
|
461
|
+
})
|
462
|
+
},
|
463
|
+
|
464
|
+
// Find all failing assertions
|
465
|
+
|
466
|
+
failures : function() {
|
467
|
+
return select(this.assertions, function(assertion){
|
468
|
+
return !assertion.passed
|
469
|
+
})
|
470
|
+
},
|
471
|
+
|
472
|
+
// Weither or not the spec passed
|
473
|
+
|
474
|
+
passed : function() {
|
475
|
+
return !this.failure()
|
476
|
+
},
|
477
|
+
|
478
|
+
// Weither or not the spec requires implementation (no assertions)
|
479
|
+
|
480
|
+
requiresImplementation : function() {
|
481
|
+
return this.assertions.length == 0
|
482
|
+
},
|
483
|
+
|
484
|
+
// Sprite based assertions graph
|
485
|
+
|
486
|
+
assertionsGraph : function() {
|
487
|
+
return map(this.assertions, function(assertion){
|
488
|
+
return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
|
489
|
+
}).join('')
|
490
|
+
}
|
491
|
+
})
|
492
|
+
},
|
493
|
+
|
494
|
+
// --- DSLs
|
495
|
+
|
496
|
+
DSLs : {
|
497
|
+
snake : {
|
498
|
+
expect : function(actual){
|
499
|
+
return JSpec.expect(actual)
|
500
|
+
},
|
501
|
+
|
502
|
+
describe : function(description, body) {
|
503
|
+
return JSpec.currentSuite.addSuite(description, body)
|
504
|
+
},
|
505
|
+
|
506
|
+
it : function(description, body) {
|
507
|
+
return JSpec.currentSuite.addSpec(description, body)
|
508
|
+
},
|
509
|
+
|
510
|
+
before : function(body) {
|
511
|
+
return JSpec.currentSuite.addHook('before', body)
|
512
|
+
},
|
513
|
+
|
514
|
+
after : function(body) {
|
515
|
+
return JSpec.currentSuite.addHook('after', body)
|
516
|
+
},
|
517
|
+
|
518
|
+
before_each : function(body) {
|
519
|
+
return JSpec.currentSuite.addHook('before_each', body)
|
520
|
+
},
|
521
|
+
|
522
|
+
after_each : function(body) {
|
523
|
+
return JSpec.currentSuite.addHook('after_each', body)
|
524
|
+
},
|
525
|
+
|
526
|
+
should_behave_like : function(description) {
|
527
|
+
return JSpec.shareBehaviorsOf(description)
|
528
|
+
}
|
529
|
+
}
|
530
|
+
},
|
531
|
+
|
532
|
+
// --- Methods
|
533
|
+
|
534
|
+
/**
|
535
|
+
* Find a suite by its description or name.
|
536
|
+
*
|
537
|
+
* @param {string} description
|
538
|
+
* @return {Suite}
|
539
|
+
* @api private
|
540
|
+
*/
|
541
|
+
|
542
|
+
findSuite : function(description) {
|
543
|
+
return find(this.allSuites, function(suite){
|
544
|
+
return suite.name == description || suite.description == description
|
545
|
+
})
|
546
|
+
},
|
547
|
+
|
548
|
+
/**
|
549
|
+
* Share behaviors (specs) of the given suite with
|
550
|
+
* the current suite.
|
551
|
+
*
|
552
|
+
* @param {string} description
|
553
|
+
* @api public
|
554
|
+
*/
|
555
|
+
|
556
|
+
shareBehaviorsOf : function(description) {
|
557
|
+
if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
|
558
|
+
else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
|
559
|
+
},
|
560
|
+
|
561
|
+
/**
|
562
|
+
* Copy specs from one suite to another.
|
563
|
+
*
|
564
|
+
* @param {Suite} fromSuite
|
565
|
+
* @param {Suite} toSuite
|
566
|
+
* @api public
|
567
|
+
*/
|
568
|
+
|
569
|
+
copySpecs : function(fromSuite, toSuite) {
|
570
|
+
each(fromSuite.specs, function(spec){
|
571
|
+
toSuite.specs.push(spec)
|
572
|
+
})
|
573
|
+
},
|
574
|
+
|
575
|
+
/**
|
576
|
+
* Convert arguments to an array.
|
577
|
+
*
|
578
|
+
* @param {object} arguments
|
579
|
+
* @param {int} offset
|
580
|
+
* @return {array}
|
581
|
+
* @api public
|
582
|
+
*/
|
583
|
+
|
584
|
+
argumentsToArray : function(arguments, offset) {
|
585
|
+
var args = []
|
586
|
+
for (i = 0; i < arguments.length; i++) args.push(arguments[i])
|
587
|
+
return args.slice(offset || 0)
|
588
|
+
},
|
589
|
+
|
590
|
+
/**
|
591
|
+
* Return ANSI-escaped colored string.
|
592
|
+
*
|
593
|
+
* @param {string} string
|
594
|
+
* @param {string} color
|
595
|
+
* @return {string}
|
596
|
+
* @api public
|
597
|
+
*/
|
598
|
+
|
599
|
+
color : function(string, color) {
|
600
|
+
return "\u001B[" + {
|
601
|
+
bold : 1,
|
602
|
+
black : 30,
|
603
|
+
red : 31,
|
604
|
+
green : 32,
|
605
|
+
yellow : 33,
|
606
|
+
blue : 34,
|
607
|
+
magenta : 35,
|
608
|
+
cyan : 36,
|
609
|
+
white : 37
|
610
|
+
}[color] + 'm' + string + "\u001B[0m"
|
611
|
+
},
|
612
|
+
|
613
|
+
/**
|
614
|
+
* Default matcher message callback.
|
615
|
+
*
|
616
|
+
* @api private
|
617
|
+
*/
|
618
|
+
|
619
|
+
defaultMatcherMessage : function(actual, expected, negate, name) {
|
620
|
+
return 'expected ' + puts(actual) + ' to ' +
|
621
|
+
(negate ? 'not ' : '') +
|
622
|
+
name.replace(/_/g, ' ') +
|
623
|
+
' ' + puts.apply(this, expected.slice(1))
|
624
|
+
},
|
625
|
+
|
626
|
+
/**
|
627
|
+
* Normalize a matcher message.
|
628
|
+
*
|
629
|
+
* When no messge callback is present the defaultMatcherMessage
|
630
|
+
* will be assigned, will suffice for most matchers.
|
631
|
+
*
|
632
|
+
* @param {hash} matcher
|
633
|
+
* @return {hash}
|
634
|
+
* @api public
|
635
|
+
*/
|
636
|
+
|
637
|
+
normalizeMatcherMessage : function(matcher) {
|
638
|
+
if (typeof matcher.message != 'function')
|
639
|
+
matcher.message = this.defaultMatcherMessage
|
640
|
+
return matcher
|
641
|
+
},
|
642
|
+
|
643
|
+
/**
|
644
|
+
* Normalize a matcher body
|
645
|
+
*
|
646
|
+
* This process allows the following conversions until
|
647
|
+
* the matcher is in its final normalized hash state.
|
648
|
+
*
|
649
|
+
* - '==' becomes 'actual == expected'
|
650
|
+
* - 'actual == expected' becomes 'return actual == expected'
|
651
|
+
* - function(actual, expected) { return actual == expected } becomes
|
652
|
+
* { match : function(actual, expected) { return actual == expected }}
|
653
|
+
*
|
654
|
+
* @param {mixed} body
|
655
|
+
* @return {hash}
|
656
|
+
* @api public
|
657
|
+
*/
|
658
|
+
|
659
|
+
normalizeMatcherBody : function(body) {
|
660
|
+
switch (body.constructor) {
|
661
|
+
case String:
|
662
|
+
if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
|
663
|
+
if (body.length < 4) body = 'actual ' + body + ' expected'
|
664
|
+
return { match : function(actual, expected) { return eval(body) }}
|
665
|
+
|
666
|
+
case Function:
|
667
|
+
return { match : body }
|
668
|
+
|
669
|
+
default:
|
670
|
+
return body
|
671
|
+
}
|
672
|
+
},
|
673
|
+
|
674
|
+
/**
|
675
|
+
* Get option value. This method first checks if
|
676
|
+
* the option key has been set via the query string,
|
677
|
+
* otherwise returning the options hash value.
|
678
|
+
*
|
679
|
+
* @param {string} key
|
680
|
+
* @return {mixed}
|
681
|
+
* @api public
|
682
|
+
*/
|
683
|
+
|
684
|
+
option : function(key) {
|
685
|
+
return (value = query(key)) !== null ? value :
|
686
|
+
JSpec.options[key] || null
|
687
|
+
},
|
688
|
+
|
689
|
+
/**
|
690
|
+
* Generates a hash of the object passed.
|
691
|
+
*
|
692
|
+
* @param {object} object
|
693
|
+
* @return {string}
|
694
|
+
* @api private
|
695
|
+
*/
|
696
|
+
|
697
|
+
hash : function(object) {
|
698
|
+
if (object == undefined) return 'undefined'
|
699
|
+
serialize = function(prefix) {
|
700
|
+
return inject(object, prefix + ':', function(buffer, key, value){
|
701
|
+
return buffer += hash(value)
|
702
|
+
})
|
703
|
+
}
|
704
|
+
switch (object.constructor) {
|
705
|
+
case Array : return serialize('a')
|
706
|
+
case Object: return serialize('o')
|
707
|
+
case RegExp: return 'r:' + object.toString()
|
708
|
+
case Number: return 'n:' + object.toString()
|
709
|
+
case String: return 's:' + object.toString()
|
710
|
+
default: return object.toString()
|
711
|
+
}
|
712
|
+
},
|
713
|
+
|
714
|
+
/**
|
715
|
+
* Return last element of an array.
|
716
|
+
*
|
717
|
+
* @param {array} array
|
718
|
+
* @return {object}
|
719
|
+
* @api public
|
720
|
+
*/
|
721
|
+
|
722
|
+
last : function(array) {
|
723
|
+
return array[array.length - 1]
|
724
|
+
},
|
725
|
+
|
726
|
+
/**
|
727
|
+
* Convert object(s) to a print-friend string.
|
728
|
+
*
|
729
|
+
* @param {object, ...} object
|
730
|
+
* @return {string}
|
731
|
+
* @api public
|
732
|
+
*/
|
733
|
+
|
734
|
+
puts : function(object) {
|
735
|
+
if (arguments.length > 1) {
|
736
|
+
return map(argumentsToArray(arguments), function(arg){
|
737
|
+
return puts(arg)
|
738
|
+
}).join(', ')
|
739
|
+
}
|
740
|
+
if (object === undefined) return ''
|
741
|
+
if (object === null) return 'null'
|
742
|
+
if (object === true) return 'true'
|
743
|
+
if (object === false) return 'false'
|
744
|
+
if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
|
745
|
+
if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + ''
|
746
|
+
if (object.jquery) return escape(object.html())
|
747
|
+
if (object.nodeName) return escape(object.outerHTML)
|
748
|
+
switch (object.constructor) {
|
749
|
+
case String: return "'" + escape(object) + "'"
|
750
|
+
case Number: return object
|
751
|
+
case Function: return object.name || object
|
752
|
+
case Array :
|
753
|
+
return inject(object, '[', function(b, v){
|
754
|
+
return b + ', ' + puts(v)
|
755
|
+
}).replace('[,', '[') + ' ]'
|
756
|
+
case Object:
|
757
|
+
return inject(object, '{', function(b, k, v) {
|
758
|
+
return b + ', ' + puts(k) + ' : ' + puts(v)
|
759
|
+
}).replace('{,', '{') + ' }'
|
760
|
+
default:
|
761
|
+
return escape(object.toString())
|
762
|
+
}
|
763
|
+
},
|
764
|
+
|
765
|
+
/**
|
766
|
+
* Escape HTML.
|
767
|
+
*
|
768
|
+
* @param {string} html
|
769
|
+
* @return {string}
|
770
|
+
* @api public
|
771
|
+
*/
|
772
|
+
|
773
|
+
escape : function(html) {
|
774
|
+
return html.toString().
|
775
|
+
replace(/&/gmi, '&').
|
776
|
+
replace(/"/gmi, '"').
|
777
|
+
replace(/>/gmi, '>').
|
778
|
+
replace(/</gmi, '<')
|
779
|
+
},
|
780
|
+
|
781
|
+
/**
|
782
|
+
* Perform an assertion without reporting.
|
783
|
+
*
|
784
|
+
* This method is primarily used for internal
|
785
|
+
* matchers in order retain DRYness. May be invoked
|
786
|
+
* like below:
|
787
|
+
*
|
788
|
+
* does('foo', 'eql', 'foo')
|
789
|
+
* does([1,2], 'include', 1, 2)
|
790
|
+
*
|
791
|
+
* @param {mixed} actual
|
792
|
+
* @param {string} matcher
|
793
|
+
* @param {...} expected
|
794
|
+
* @return {mixed}
|
795
|
+
* @api private
|
796
|
+
*/
|
797
|
+
|
798
|
+
does : function(actual, matcher, expected) {
|
799
|
+
var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, argumentsToArray(arguments, 2))
|
800
|
+
return assertion.run().result
|
801
|
+
},
|
802
|
+
|
803
|
+
/**
|
804
|
+
* Perform an assertion.
|
805
|
+
*
|
806
|
+
* expect(true).to('be', true)
|
807
|
+
* expect('foo').not_to('include', 'bar')
|
808
|
+
* expect([1, [2]]).to('include', 1, [2])
|
809
|
+
*
|
810
|
+
* @param {mixed} actual
|
811
|
+
* @return {hash}
|
812
|
+
* @api public
|
813
|
+
*/
|
814
|
+
|
815
|
+
expect : function(actual) {
|
816
|
+
assert = function(matcher, args, negate) {
|
817
|
+
var expected = []
|
818
|
+
for (i = 1; i < args.length; i++) expected.push(args[i])
|
819
|
+
assertion = new JSpec.Assertion(matcher, actual, expected, negate)
|
820
|
+
if (matcher.defer) assertion.run()
|
821
|
+
else JSpec.currentSpec.assertions.push(assertion.run().report())
|
822
|
+
return assertion.result
|
823
|
+
}
|
824
|
+
|
825
|
+
to = function(matcher) {
|
826
|
+
return assert(matcher, arguments, false)
|
827
|
+
}
|
828
|
+
|
829
|
+
not_to = function(matcher) {
|
830
|
+
return assert(matcher, arguments, true)
|
831
|
+
}
|
832
|
+
|
833
|
+
return {
|
834
|
+
to : to,
|
835
|
+
should : to,
|
836
|
+
not_to: not_to,
|
837
|
+
should_not : not_to
|
838
|
+
}
|
839
|
+
},
|
840
|
+
|
841
|
+
/**
|
842
|
+
* Strim whitespace or chars.
|
843
|
+
*
|
844
|
+
* @param {string} string
|
845
|
+
* @param {string} chars
|
846
|
+
* @return {string}
|
847
|
+
* @api public
|
848
|
+
*/
|
849
|
+
|
850
|
+
strip : function(string, chars) {
|
851
|
+
return string.
|
852
|
+
replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
|
853
|
+
replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
|
854
|
+
},
|
855
|
+
|
856
|
+
/**
|
857
|
+
* Call an iterator callback with arguments a, or b
|
858
|
+
* depending on the arity of the callback.
|
859
|
+
*
|
860
|
+
* @param {function} callback
|
861
|
+
* @param {mixed} a
|
862
|
+
* @param {mixed} b
|
863
|
+
* @return {mixed}
|
864
|
+
* @api private
|
865
|
+
*/
|
866
|
+
|
867
|
+
callIterator : function(callback, a, b) {
|
868
|
+
return callback.length == 1 ? callback(b) : callback(a, b)
|
869
|
+
},
|
870
|
+
|
871
|
+
/**
|
872
|
+
* Extend an object with another.
|
873
|
+
*
|
874
|
+
* @param {object} object
|
875
|
+
* @param {object} other
|
876
|
+
* @api public
|
877
|
+
*/
|
878
|
+
|
879
|
+
extend : function(object, other) {
|
880
|
+
each(other, function(property, value){
|
881
|
+
object[property] = value
|
882
|
+
})
|
883
|
+
},
|
884
|
+
|
885
|
+
/**
|
886
|
+
* Iterate an object, invoking the given callback.
|
887
|
+
*
|
888
|
+
* @param {hash, array, string} object
|
889
|
+
* @param {function} callback
|
890
|
+
* @return {JSpec}
|
891
|
+
* @api public
|
892
|
+
*/
|
893
|
+
|
894
|
+
each : function(object, callback) {
|
895
|
+
if (typeof object == 'string') object = object.split(' ')
|
896
|
+
for (key in object)
|
897
|
+
if (object.hasOwnProperty(key))
|
898
|
+
callIterator(callback, key, object[key])
|
899
|
+
},
|
900
|
+
|
901
|
+
/**
|
902
|
+
* Iterate with memo.
|
903
|
+
*
|
904
|
+
* @param {hash, array} object
|
905
|
+
* @param {object} memo
|
906
|
+
* @param {function} callback
|
907
|
+
* @return {object}
|
908
|
+
* @api public
|
909
|
+
*/
|
910
|
+
|
911
|
+
inject : function(object, memo, callback) {
|
912
|
+
each(object, function(key, value){
|
913
|
+
memo = (callback.length == 2 ?
|
914
|
+
callback(memo, value):
|
915
|
+
callback(memo, key, value)) ||
|
916
|
+
memo
|
917
|
+
})
|
918
|
+
return memo
|
919
|
+
},
|
920
|
+
|
921
|
+
/**
|
922
|
+
* Map callback return values.
|
923
|
+
*
|
924
|
+
* @param {hash, array} object
|
925
|
+
* @param {function} callback
|
926
|
+
* @return {array}
|
927
|
+
* @api public
|
928
|
+
*/
|
929
|
+
|
930
|
+
map : function(object, callback) {
|
931
|
+
return inject(object, [], function(memo, key, value){
|
932
|
+
memo.push(callIterator(callback, key, value))
|
933
|
+
})
|
934
|
+
},
|
935
|
+
|
936
|
+
/**
|
937
|
+
* Returns the first matching expression or null.
|
938
|
+
*
|
939
|
+
* @param {hash, array} object
|
940
|
+
* @param {function} callback
|
941
|
+
* @return {mixed}
|
942
|
+
* @api public
|
943
|
+
*/
|
944
|
+
|
945
|
+
any : function(object, callback) {
|
946
|
+
return inject(object, null, function(state, key, value){
|
947
|
+
return state ? state :
|
948
|
+
callIterator(callback, key, value) ?
|
949
|
+
value : state
|
950
|
+
})
|
951
|
+
},
|
952
|
+
|
953
|
+
/**
|
954
|
+
* Returns an array of values collected when the callback
|
955
|
+
* given evaluates to true.
|
956
|
+
*
|
957
|
+
* @param {hash, array} object
|
958
|
+
* @return {function} callback
|
959
|
+
* @return {array}
|
960
|
+
* @api public
|
961
|
+
*/
|
962
|
+
|
963
|
+
select : function(object, callback) {
|
964
|
+
return inject(object, [], function(memo, key, value){
|
965
|
+
if (callIterator(callback, key, value))
|
966
|
+
memo.push(value)
|
967
|
+
})
|
968
|
+
},
|
969
|
+
|
970
|
+
/**
|
971
|
+
* Define matchers.
|
972
|
+
*
|
973
|
+
* @param {hash} matchers
|
974
|
+
* @api public
|
975
|
+
*/
|
976
|
+
|
977
|
+
addMatchers : function(matchers) {
|
978
|
+
each(matchers, function(name, body){
|
979
|
+
JSpec.addMatcher(name, body)
|
980
|
+
})
|
981
|
+
},
|
982
|
+
|
983
|
+
/**
|
984
|
+
* Define a matcher.
|
985
|
+
*
|
986
|
+
* @param {string} name
|
987
|
+
* @param {hash, function, string} body
|
988
|
+
* @api public
|
989
|
+
*/
|
990
|
+
|
991
|
+
addMatcher : function(name, body) {
|
992
|
+
this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
|
993
|
+
this.matchers[name].name = name
|
994
|
+
},
|
995
|
+
|
996
|
+
/**
|
997
|
+
* Add a root suite to JSpec.
|
998
|
+
*
|
999
|
+
* @param {string} description
|
1000
|
+
* @param {body} function
|
1001
|
+
* @api public
|
1002
|
+
*/
|
1003
|
+
|
1004
|
+
describe : function(description, body) {
|
1005
|
+
var suite = new JSpec.Suite(description, body)
|
1006
|
+
this.allSuites.push(suite)
|
1007
|
+
this.suites.push(suite)
|
1008
|
+
},
|
1009
|
+
|
1010
|
+
/**
|
1011
|
+
* Return the contents of a function body.
|
1012
|
+
*
|
1013
|
+
* @param {function} body
|
1014
|
+
* @return {string}
|
1015
|
+
* @api public
|
1016
|
+
*/
|
1017
|
+
|
1018
|
+
contentsOf : function(body) {
|
1019
|
+
return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
|
1020
|
+
},
|
1021
|
+
|
1022
|
+
/**
|
1023
|
+
* Evaluate a JSpec capture body.
|
1024
|
+
*
|
1025
|
+
* @param {function} body
|
1026
|
+
* @param {string} errorMessage (optional)
|
1027
|
+
* @return {Type}
|
1028
|
+
* @api private
|
1029
|
+
*/
|
1030
|
+
|
1031
|
+
evalBody : function(body, errorMessage) {
|
1032
|
+
var dsl = this.DSL || this.DSLs.snake
|
1033
|
+
var matchers = this.matchers
|
1034
|
+
var context = this.context || this.defaultContext
|
1035
|
+
var contents = this.contentsOf(body)
|
1036
|
+
try { eval('with (dsl){ with (context) { with (matchers) { ' + contents + ' }}}') }
|
1037
|
+
catch(e) { error(errorMessage, e) }
|
1038
|
+
},
|
1039
|
+
|
1040
|
+
/**
|
1041
|
+
* Pre-process a string of JSpec.
|
1042
|
+
*
|
1043
|
+
* @param {string} input
|
1044
|
+
* @return {string}
|
1045
|
+
* @api private
|
1046
|
+
*/
|
1047
|
+
|
1048
|
+
preprocess : function(input) {
|
1049
|
+
return input.
|
1050
|
+
replace(/describe (.*?)$/gm, 'describe($1, function(){').
|
1051
|
+
replace(/ it (.*?)$/gm, ' it($1, function(){').
|
1052
|
+
replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
|
1053
|
+
replace(/end(?= |\n|$)/gm, '});').
|
1054
|
+
replace(/-\{/g, 'function(){').
|
1055
|
+
replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
|
1056
|
+
replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)').
|
1057
|
+
replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 expect($2).$3($4, $5)').
|
1058
|
+
replace(/, \)/gm, ')').
|
1059
|
+
replace(/should\.not/gm, 'should_not')
|
1060
|
+
},
|
1061
|
+
|
1062
|
+
/**
|
1063
|
+
* Create a range string which can be evaluated to a native array.
|
1064
|
+
*
|
1065
|
+
* @param {int} start
|
1066
|
+
* @param {int} end
|
1067
|
+
* @return {string}
|
1068
|
+
* @api public
|
1069
|
+
*/
|
1070
|
+
|
1071
|
+
range : function(start, end) {
|
1072
|
+
var current = parseInt(start), end = parseInt(end), values = [current]
|
1073
|
+
if (end > current) while (++current <= end) values.push(current)
|
1074
|
+
else while (--current >= end) values.push(current)
|
1075
|
+
return '[' + values + ']'
|
1076
|
+
},
|
1077
|
+
|
1078
|
+
/**
|
1079
|
+
* Call _callback_ when all specs have finished.
|
1080
|
+
*
|
1081
|
+
* @param {function} callback
|
1082
|
+
* @api public
|
1083
|
+
*/
|
1084
|
+
|
1085
|
+
whenFinished : function(callback) {
|
1086
|
+
if (this.stats.specsFinished >= this.stats.specs) callback()
|
1087
|
+
else setTimeout(function(){ JSpec.whenFinished(callback) }, 50)
|
1088
|
+
},
|
1089
|
+
|
1090
|
+
/**
|
1091
|
+
* Report on the results.
|
1092
|
+
*
|
1093
|
+
* @api public
|
1094
|
+
*/
|
1095
|
+
|
1096
|
+
report : function() {
|
1097
|
+
this.whenFinished(function() {
|
1098
|
+
JSpec.options.formatter ?
|
1099
|
+
new JSpec.options.formatter(JSpec, JSpec.options):
|
1100
|
+
new JSpec.formatters.DOM(JSpec, JSpec.options)
|
1101
|
+
})
|
1102
|
+
},
|
1103
|
+
|
1104
|
+
/**
|
1105
|
+
* Run the spec suites. Options are merged
|
1106
|
+
* with JSpec options when present.
|
1107
|
+
*
|
1108
|
+
* @param {hash} options
|
1109
|
+
* @return {JSpec}
|
1110
|
+
* @api public
|
1111
|
+
*/
|
1112
|
+
|
1113
|
+
run : function(options) {
|
1114
|
+
if (options) extend(this.options, options)
|
1115
|
+
if (option('profile')) console.group('Profile')
|
1116
|
+
each(this.suites, function(suite) { JSpec.runSuite(suite) })
|
1117
|
+
if (option('profile')) console.groupEnd()
|
1118
|
+
return this
|
1119
|
+
},
|
1120
|
+
|
1121
|
+
/**
|
1122
|
+
* When the current spec's wait duration has passed
|
1123
|
+
* the _callback_ will be called.
|
1124
|
+
*
|
1125
|
+
* @param {function} callback
|
1126
|
+
* @api public
|
1127
|
+
*/
|
1128
|
+
|
1129
|
+
whenCurrentSpecIsFinished : function(callback) {
|
1130
|
+
if (this.currentSpec && this.currentSpec.wait)
|
1131
|
+
setTimeout(callback, this.currentSpec.wait)
|
1132
|
+
else callback()
|
1133
|
+
},
|
1134
|
+
|
1135
|
+
/**
|
1136
|
+
* Run a suite.
|
1137
|
+
*
|
1138
|
+
* @param {Suite} suite
|
1139
|
+
* @api public
|
1140
|
+
*/
|
1141
|
+
|
1142
|
+
runSuite : function(suite) {
|
1143
|
+
this.currentSuite = suite
|
1144
|
+
this.evalBody(suite.body)
|
1145
|
+
suite.ran = true
|
1146
|
+
suite.hook('before')
|
1147
|
+
each(suite.specs, function(spec) {
|
1148
|
+
JSpec.whenCurrentSpecIsFinished(function(){
|
1149
|
+
suite.hook('before_each')
|
1150
|
+
JSpec.runSpec(spec)
|
1151
|
+
suite.hook('after_each')
|
1152
|
+
})
|
1153
|
+
})
|
1154
|
+
if (suite.hasSuites()) {
|
1155
|
+
each(suite.suites, function(suite) {
|
1156
|
+
JSpec.runSuite(suite)
|
1157
|
+
})
|
1158
|
+
}
|
1159
|
+
suite.hook('after')
|
1160
|
+
this.stats.suitesFinished++
|
1161
|
+
},
|
1162
|
+
|
1163
|
+
/**
|
1164
|
+
* Report a failure for the current spec.
|
1165
|
+
*
|
1166
|
+
* @param {string} message
|
1167
|
+
* @api public
|
1168
|
+
*/
|
1169
|
+
|
1170
|
+
fail : function(message) {
|
1171
|
+
JSpec.currentSpec.assertions.push({ passed : false, message : message })
|
1172
|
+
JSpec.stats.failures++
|
1173
|
+
},
|
1174
|
+
|
1175
|
+
/**
|
1176
|
+
* Run a spec.
|
1177
|
+
*
|
1178
|
+
* @param {Spec} spec
|
1179
|
+
* @api public
|
1180
|
+
*/
|
1181
|
+
|
1182
|
+
runSpec : function(spec) {
|
1183
|
+
this.currentSpec = spec
|
1184
|
+
if (option('profile')) console.time(spec.description)
|
1185
|
+
try { this.evalBody(spec.body) }
|
1186
|
+
catch (e) { fail(e) }
|
1187
|
+
spec.runDeferredAssertions()
|
1188
|
+
if (option('profile')) console.timeEnd(spec.description)
|
1189
|
+
this.stats.specsFinished++
|
1190
|
+
this.stats.assertions += spec.assertions.length
|
1191
|
+
},
|
1192
|
+
|
1193
|
+
/**
|
1194
|
+
* Require a dependency, with optional message.
|
1195
|
+
*
|
1196
|
+
* @param {string} dependency
|
1197
|
+
* @param {string} message (optional)
|
1198
|
+
* @api public
|
1199
|
+
*/
|
1200
|
+
|
1201
|
+
requires : function(dependency, message) {
|
1202
|
+
try { eval(dependency) }
|
1203
|
+
catch (e) { error('JSpec depends on ' + dependency + ' ' + message) }
|
1204
|
+
},
|
1205
|
+
|
1206
|
+
/**
|
1207
|
+
* Query against the current query strings keys
|
1208
|
+
* or the queryString specified.
|
1209
|
+
*
|
1210
|
+
* @param {string} key
|
1211
|
+
* @param {string} queryString
|
1212
|
+
* @return {string, null}
|
1213
|
+
* @api private
|
1214
|
+
*/
|
1215
|
+
|
1216
|
+
query : function(key, queryString) {
|
1217
|
+
var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
|
1218
|
+
return inject(queryString.split('&'), null, function(value, pair){
|
1219
|
+
parts = pair.split('=')
|
1220
|
+
return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
|
1221
|
+
})
|
1222
|
+
},
|
1223
|
+
|
1224
|
+
/**
|
1225
|
+
* Throw a JSpec related error.
|
1226
|
+
*
|
1227
|
+
* @param {string} message
|
1228
|
+
* @param {Exception} e
|
1229
|
+
* @api public
|
1230
|
+
*/
|
1231
|
+
|
1232
|
+
error : function(message, e) {
|
1233
|
+
throw (message ? message : '') + e.toString() +
|
1234
|
+
(e.line ? ' near line ' + e.line : '')
|
1235
|
+
},
|
1236
|
+
|
1237
|
+
/**
|
1238
|
+
* Ad-hoc POST request for JSpec server usage.
|
1239
|
+
*
|
1240
|
+
* @param {string} url
|
1241
|
+
* @param {string} data
|
1242
|
+
* @api private
|
1243
|
+
*/
|
1244
|
+
|
1245
|
+
post : function(url, data) {
|
1246
|
+
var request = this.xhr()
|
1247
|
+
request.open('POST', url, false)
|
1248
|
+
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
1249
|
+
request.send(data)
|
1250
|
+
},
|
1251
|
+
|
1252
|
+
/**
|
1253
|
+
* Report back to server with statistics.
|
1254
|
+
*
|
1255
|
+
* @api private
|
1256
|
+
*/
|
1257
|
+
|
1258
|
+
reportToServer : function() {
|
1259
|
+
this.whenFinished(function(){
|
1260
|
+
JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures)
|
1261
|
+
if ('close' in main) main.close()
|
1262
|
+
})
|
1263
|
+
},
|
1264
|
+
|
1265
|
+
/**
|
1266
|
+
* Instantiate an XMLHttpRequest.
|
1267
|
+
*
|
1268
|
+
* @return {ActiveXObject, XMLHttpRequest}
|
1269
|
+
* @api private
|
1270
|
+
*/
|
1271
|
+
|
1272
|
+
xhr : function() {
|
1273
|
+
return 'ActiveXObject' in main ?
|
1274
|
+
new ActiveXObject("Microsoft.XMLHTTP"):
|
1275
|
+
new XMLHttpRequest()
|
1276
|
+
},
|
1277
|
+
|
1278
|
+
/**
|
1279
|
+
* Check for HTTP request support.
|
1280
|
+
*
|
1281
|
+
* @return {bool}
|
1282
|
+
* @api private
|
1283
|
+
*/
|
1284
|
+
|
1285
|
+
hasXhr : function() {
|
1286
|
+
return 'XMLHttpRequest' in main || 'ActiveXObject' in main
|
1287
|
+
},
|
1288
|
+
|
1289
|
+
/**
|
1290
|
+
* Load a files contents.
|
1291
|
+
*
|
1292
|
+
* @param {string} file
|
1293
|
+
* @return {string}
|
1294
|
+
* @api public
|
1295
|
+
*/
|
1296
|
+
|
1297
|
+
load : function(file) {
|
1298
|
+
if (this.hasXhr()) {
|
1299
|
+
var request = this.xhr()
|
1300
|
+
request.open('GET', file, false)
|
1301
|
+
request.send(null)
|
1302
|
+
if (request.readyState == 4) return request.responseText
|
1303
|
+
}
|
1304
|
+
else if ('readFile' in main)
|
1305
|
+
return readFile(file)
|
1306
|
+
else
|
1307
|
+
error('cannot load ' + file)
|
1308
|
+
},
|
1309
|
+
|
1310
|
+
/**
|
1311
|
+
* Load, pre-process, and evaluate a file.
|
1312
|
+
*
|
1313
|
+
* @param {string} file
|
1314
|
+
* @param {JSpec}
|
1315
|
+
* @api public
|
1316
|
+
*/
|
1317
|
+
|
1318
|
+
exec : function(file) {
|
1319
|
+
eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
|
1320
|
+
return this
|
1321
|
+
}
|
1322
|
+
}
|
1323
|
+
|
1324
|
+
// --- Utility functions
|
1325
|
+
|
1326
|
+
var main = this
|
1327
|
+
var map = JSpec.map
|
1328
|
+
var any = JSpec.any
|
1329
|
+
var find = JSpec.any
|
1330
|
+
var last = JSpec.last
|
1331
|
+
var fail = JSpec.fail
|
1332
|
+
var range = JSpec.range
|
1333
|
+
var each = JSpec.each
|
1334
|
+
var option = JSpec.option
|
1335
|
+
var inject = JSpec.inject
|
1336
|
+
var select = JSpec.select
|
1337
|
+
var error = JSpec.error
|
1338
|
+
var escape = JSpec.escape
|
1339
|
+
var extend = JSpec.extend
|
1340
|
+
var puts = JSpec.puts
|
1341
|
+
var hash = JSpec.hash
|
1342
|
+
var query = JSpec.query
|
1343
|
+
var strip = JSpec.strip
|
1344
|
+
var color = JSpec.color
|
1345
|
+
var does = JSpec.does
|
1346
|
+
var addMatchers = JSpec.addMatchers
|
1347
|
+
var callIterator = JSpec.callIterator
|
1348
|
+
var argumentsToArray = JSpec.argumentsToArray
|
1349
|
+
if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
|
1350
|
+
|
1351
|
+
// --- Matchers
|
1352
|
+
|
1353
|
+
addMatchers({
|
1354
|
+
equal : "===",
|
1355
|
+
be : "alias equal",
|
1356
|
+
be_greater_than : ">",
|
1357
|
+
be_less_than : "<",
|
1358
|
+
be_at_least : ">=",
|
1359
|
+
be_at_most : "<=",
|
1360
|
+
be_a : "actual.constructor == expected",
|
1361
|
+
be_an : "alias be_a",
|
1362
|
+
be_an_instance_of : "actual instanceof expected",
|
1363
|
+
be_null : "actual == null",
|
1364
|
+
be_empty : "actual.length == 0",
|
1365
|
+
be_true : "actual == true",
|
1366
|
+
be_false : "actual == false",
|
1367
|
+
be_type : "typeof actual == expected",
|
1368
|
+
match : "typeof actual == 'string' ? actual.match(expected) : false",
|
1369
|
+
respond_to : "typeof actual[expected] == 'function'",
|
1370
|
+
have_length : "actual.length == expected",
|
1371
|
+
be_within : "actual >= expected[0] && actual <= last(expected)",
|
1372
|
+
have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
|
1373
|
+
|
1374
|
+
eql : function(actual, expected) {
|
1375
|
+
return actual.constructor == Array ||
|
1376
|
+
actual instanceof Object ?
|
1377
|
+
hash(actual) == hash(expected):
|
1378
|
+
actual == expected
|
1379
|
+
},
|
1380
|
+
|
1381
|
+
receive : { defer : true, match : function(actual, method, times) {
|
1382
|
+
proxy = new JSpec.ProxyAssertion(actual, method, times)
|
1383
|
+
JSpec.currentSpec.assertions.push(proxy)
|
1384
|
+
return proxy
|
1385
|
+
}},
|
1386
|
+
|
1387
|
+
include : function(actual) {
|
1388
|
+
for (state = true, i = 1; i < arguments.length; i++) {
|
1389
|
+
arg = arguments[i]
|
1390
|
+
switch (actual.constructor) {
|
1391
|
+
case String:
|
1392
|
+
case Number:
|
1393
|
+
case RegExp:
|
1394
|
+
case Function:
|
1395
|
+
state = actual.toString().match(arg.toString())
|
1396
|
+
break
|
1397
|
+
|
1398
|
+
case Object:
|
1399
|
+
state = arg in actual
|
1400
|
+
break
|
1401
|
+
|
1402
|
+
case Array:
|
1403
|
+
state = any(actual, function(value){ return hash(value) == hash(arg) })
|
1404
|
+
break
|
1405
|
+
}
|
1406
|
+
if (!state) return false
|
1407
|
+
}
|
1408
|
+
return true
|
1409
|
+
},
|
1410
|
+
|
1411
|
+
throw_error : { match : function(actual, expected) {
|
1412
|
+
try { actual() }
|
1413
|
+
catch (e) {
|
1414
|
+
this.e = e
|
1415
|
+
if (expected == undefined) return true
|
1416
|
+
switch (expected.constructor) {
|
1417
|
+
case RegExp : return expected.test(e)
|
1418
|
+
case Function : return e instanceof expected
|
1419
|
+
case String : return expected == e.toString()
|
1420
|
+
}
|
1421
|
+
}
|
1422
|
+
}, message : function(actual, expected, negate) {
|
1423
|
+
// TODO: fix when expected is ONLY expected values
|
1424
|
+
expected = (function(){
|
1425
|
+
if (expected[1] == undefined) return 'exception'
|
1426
|
+
switch (expected[1].constructor) {
|
1427
|
+
case RegExp : return 'exception matching ' + puts(expected[1])
|
1428
|
+
case Function : return 'instance of ' + expected[1].name
|
1429
|
+
case String : return 'exception of ' + puts(expected[1])
|
1430
|
+
}
|
1431
|
+
})()
|
1432
|
+
return 'expected ' + (negate ? 'no ' : 'an ' ) + expected +
|
1433
|
+
' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
|
1434
|
+
}},
|
1435
|
+
|
1436
|
+
have : function(actual, length, property) {
|
1437
|
+
return actual[property].length == length
|
1438
|
+
},
|
1439
|
+
|
1440
|
+
have_at_least : function(actual, length, property) {
|
1441
|
+
return actual[property].length >= length
|
1442
|
+
},
|
1443
|
+
|
1444
|
+
have_at_most :function(actual, length, property) {
|
1445
|
+
return actual[property].length <= length
|
1446
|
+
},
|
1447
|
+
|
1448
|
+
have_within : function(actual, range, property) {
|
1449
|
+
length = actual[property].length
|
1450
|
+
return length >= range.shift() && length <= range.pop()
|
1451
|
+
},
|
1452
|
+
|
1453
|
+
have_prop : function(actual, property, value) {
|
1454
|
+
return actual[property] == null ||
|
1455
|
+
actual[property] instanceof Function ? false:
|
1456
|
+
value == null ? true:
|
1457
|
+
does(actual[property], 'eql', value)
|
1458
|
+
},
|
1459
|
+
|
1460
|
+
have_property : function(actual, property, value) {
|
1461
|
+
return actual[property] == null ||
|
1462
|
+
actual[property] instanceof Function ? false:
|
1463
|
+
value == null ? true:
|
1464
|
+
value === actual[property]
|
1465
|
+
}
|
1466
|
+
})
|
1467
|
+
|
1468
|
+
})()
|