jspec 2.11.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +522 -0
- data/Manifest +57 -0
- data/README.rdoc +825 -0
- data/Rakefile +75 -0
- data/bin/jspec +305 -0
- data/jspec.gemspec +44 -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 +71 -0
- data/lib/jspec.js +1771 -0
- data/lib/jspec.shell.js +36 -0
- data/lib/jspec.timers.js +90 -0
- data/lib/jspec.xhr.js +183 -0
- data/server/browsers.rb +228 -0
- data/server/helpers.rb +82 -0
- data/server/routes.rb +57 -0
- data/server/server.rb +88 -0
- data/spec/async +1 -0
- data/spec/env.js +695 -0
- data/spec/fixtures/test.html +1 -0
- data/spec/fixtures/test.json +1 -0
- data/spec/fixtures/test.xml +5 -0
- data/spec/helpers.js +66 -0
- data/spec/server.rb +2 -0
- data/spec/spec.dom.html +34 -0
- data/spec/spec.fixtures.js +18 -0
- data/spec/spec.grammar-less.js +34 -0
- data/spec/spec.grammar.js +226 -0
- data/spec/spec.jquery.js +176 -0
- data/spec/spec.jquery.xhr.js +65 -0
- data/spec/spec.js +166 -0
- data/spec/spec.matchers.js +493 -0
- data/spec/spec.modules.js +67 -0
- data/spec/spec.node.js +46 -0
- data/spec/spec.rhino.js +17 -0
- data/spec/spec.server.html +29 -0
- data/spec/spec.shared-behaviors.js +80 -0
- data/spec/spec.utils.js +279 -0
- data/spec/spec.xhr.js +156 -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/server.rb +4 -0
- data/templates/default/spec/spec.core.js +8 -0
- data/templates/default/spec/spec.dom.html +20 -0
- data/templates/default/spec/spec.rhino.js +8 -0
- data/templates/default/spec/spec.server.html +16 -0
- data/templates/rails/server.rb +4 -0
- data/templates/rails/spec.application.js +8 -0
- data/templates/rails/spec.dom.html +20 -0
- data/templates/rails/spec.rhino.js +8 -0
- data/templates/rails/spec.server.html +16 -0
- metadata +168 -0
data/lib/jspec.js
ADDED
@@ -0,0 +1,1771 @@
|
|
1
|
+
|
2
|
+
// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
3
|
+
|
4
|
+
(function(){
|
5
|
+
|
6
|
+
JSpec = {
|
7
|
+
|
8
|
+
version : '2.11.2',
|
9
|
+
cache : {},
|
10
|
+
suites : [],
|
11
|
+
modules : [],
|
12
|
+
allSuites : [],
|
13
|
+
matchers : {},
|
14
|
+
stubbed : [],
|
15
|
+
request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
|
16
|
+
stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
|
17
|
+
options : { profile: false },
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Default context in which bodies are evaluated.
|
21
|
+
*
|
22
|
+
* Replace context simply by setting JSpec.context
|
23
|
+
* to your own like below:
|
24
|
+
*
|
25
|
+
* JSpec.context = { foo : 'bar' }
|
26
|
+
*
|
27
|
+
* Contexts can be changed within any body, this can be useful
|
28
|
+
* in order to provide specific helper methods to specific suites.
|
29
|
+
*
|
30
|
+
* To reset (usually in after hook) simply set to null like below:
|
31
|
+
*
|
32
|
+
* JSpec.context = null
|
33
|
+
*
|
34
|
+
*/
|
35
|
+
|
36
|
+
defaultContext : {
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Return an object used for proxy assertions.
|
40
|
+
* This object is used to indicate that an object
|
41
|
+
* should be an instance of _object_, not the constructor
|
42
|
+
* itself.
|
43
|
+
*
|
44
|
+
* @param {function} constructor
|
45
|
+
* @return {hash}
|
46
|
+
* @api public
|
47
|
+
*/
|
48
|
+
|
49
|
+
an_instance_of : function(constructor) {
|
50
|
+
return { an_instance_of : constructor }
|
51
|
+
},
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Load fixture at _path_. This utility function
|
55
|
+
* supplies the means to resolve, and cache fixture contents
|
56
|
+
* via the DOM or Rhino.
|
57
|
+
*
|
58
|
+
* Fixtures are resolved as:
|
59
|
+
*
|
60
|
+
* - <path>
|
61
|
+
* - fixtures/<path>
|
62
|
+
* - fixtures/<path>.html
|
63
|
+
*
|
64
|
+
* @param {string} path
|
65
|
+
* @return {string}
|
66
|
+
* @api public
|
67
|
+
*/
|
68
|
+
|
69
|
+
fixture : function(path) {
|
70
|
+
if (JSpec.cache[path]) return JSpec.cache[path]
|
71
|
+
return JSpec.cache[path] =
|
72
|
+
JSpec.tryLoading(path) ||
|
73
|
+
JSpec.tryLoading('fixtures/' + path) ||
|
74
|
+
JSpec.tryLoading('fixtures/' + path + '.html') ||
|
75
|
+
JSpec.tryLoading('spec/' + path) ||
|
76
|
+
JSpec.tryLoading('spec/fixtures/' + path) ||
|
77
|
+
JSpec.tryLoading('spec/fixtures/' + path + '.html')
|
78
|
+
}
|
79
|
+
},
|
80
|
+
|
81
|
+
// --- Objects
|
82
|
+
|
83
|
+
formatters : {
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Report to server.
|
87
|
+
*
|
88
|
+
* Options:
|
89
|
+
* - uri specific uri to report to.
|
90
|
+
* - verbose weither or not to output messages
|
91
|
+
* - failuresOnly output failure messages only
|
92
|
+
*
|
93
|
+
* @api public
|
94
|
+
*/
|
95
|
+
|
96
|
+
Server : function(results, options) {
|
97
|
+
var uri = options.uri || 'http://' + window.location.host + '/results'
|
98
|
+
JSpec.post(uri, {
|
99
|
+
stats: JSpec.stats,
|
100
|
+
options: options,
|
101
|
+
results: map(results.allSuites, function(suite) {
|
102
|
+
if (suite.hasSpecs())
|
103
|
+
return {
|
104
|
+
description: suite.description,
|
105
|
+
specs: map(suite.specs, function(spec) {
|
106
|
+
return {
|
107
|
+
description: spec.description,
|
108
|
+
message: !spec.passed() ? spec.failure().message : null,
|
109
|
+
status: spec.requiresImplementation() ? 'pending' :
|
110
|
+
spec.passed() ? 'pass' :
|
111
|
+
'fail',
|
112
|
+
assertions: map(spec.assertions, function(assertion){
|
113
|
+
return {
|
114
|
+
passed: assertion.passed
|
115
|
+
}
|
116
|
+
})
|
117
|
+
}
|
118
|
+
})
|
119
|
+
}
|
120
|
+
})
|
121
|
+
})
|
122
|
+
if ('close' in main) main.close()
|
123
|
+
},
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Default formatter, outputting to the DOM.
|
127
|
+
*
|
128
|
+
* Options:
|
129
|
+
* - reportToId id of element to output reports to, defaults to 'jspec'
|
130
|
+
* - failuresOnly displays only suites with failing specs
|
131
|
+
*
|
132
|
+
* @api public
|
133
|
+
*/
|
134
|
+
|
135
|
+
DOM : function(results, options) {
|
136
|
+
var id = option('reportToId') || 'jspec'
|
137
|
+
var report = document.getElementById(id)
|
138
|
+
var failuresOnly = option('failuresOnly')
|
139
|
+
var classes = results.stats.failures ? 'has-failures' : ''
|
140
|
+
if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
|
141
|
+
|
142
|
+
function bodyContents(body) {
|
143
|
+
return JSpec.
|
144
|
+
escape(JSpec.contentsOf(body)).
|
145
|
+
replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
|
146
|
+
replace("\n", '<br/>')
|
147
|
+
}
|
148
|
+
|
149
|
+
report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
|
150
|
+
<span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
|
151
|
+
<span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
|
152
|
+
</div><table class="suites">' + map(results.allSuites, function(suite) {
|
153
|
+
var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
154
|
+
if (displaySuite && suite.hasSpecs())
|
155
|
+
return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
|
156
|
+
map(suite.specs, function(i, spec) {
|
157
|
+
return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
|
158
|
+
(spec.requiresImplementation() ?
|
159
|
+
'<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
|
160
|
+
(spec.passed() && !failuresOnly) ?
|
161
|
+
'<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
|
162
|
+
!spec.passed() ?
|
163
|
+
'<td class="fail">' + escape(spec.description) + ' <em>' + escape(spec.failure().message) + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>' :
|
164
|
+
'') +
|
165
|
+
'<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
|
166
|
+
}).join('') + '</tr>'
|
167
|
+
}).join('') + '</table></div>'
|
168
|
+
},
|
169
|
+
|
170
|
+
/**
|
171
|
+
* Terminal formatter.
|
172
|
+
*
|
173
|
+
* @api public
|
174
|
+
*/
|
175
|
+
|
176
|
+
Terminal : function(results, options) {
|
177
|
+
failuresOnly = option('failuresOnly')
|
178
|
+
print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
|
179
|
+
color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n")
|
180
|
+
|
181
|
+
function indent(string) {
|
182
|
+
return string.replace(/^(.)/gm, ' $1')
|
183
|
+
}
|
184
|
+
|
185
|
+
each(results.allSuites, function(suite) {
|
186
|
+
var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
187
|
+
if (displaySuite && suite.hasSpecs()) {
|
188
|
+
print(color(' ' + suite.description, 'bold'))
|
189
|
+
each(suite.specs, function(spec){
|
190
|
+
var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
|
191
|
+
return graph + color('.', assertion.passed ? 'green' : 'red')
|
192
|
+
})
|
193
|
+
if (spec.requiresImplementation())
|
194
|
+
print(color(' ' + spec.description, 'blue') + assertionsGraph)
|
195
|
+
else if (spec.passed() && !failuresOnly)
|
196
|
+
print(color(' ' + spec.description, 'green') + assertionsGraph)
|
197
|
+
else if (!spec.passed())
|
198
|
+
print(color(' ' + spec.description, 'red') + assertionsGraph +
|
199
|
+
"\n" + indent(spec.failure().message) + "\n")
|
200
|
+
})
|
201
|
+
print("")
|
202
|
+
}
|
203
|
+
})
|
204
|
+
},
|
205
|
+
|
206
|
+
/**
|
207
|
+
* Console formatter.
|
208
|
+
*
|
209
|
+
* @api public
|
210
|
+
*/
|
211
|
+
|
212
|
+
Console : function(results, options) {
|
213
|
+
console.log('')
|
214
|
+
console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
|
215
|
+
each(results.allSuites, function(suite) {
|
216
|
+
if (suite.ran) {
|
217
|
+
console.group(suite.description)
|
218
|
+
each(suite.specs, function(spec){
|
219
|
+
var assertionCount = spec.assertions.length + ':'
|
220
|
+
if (spec.requiresImplementation())
|
221
|
+
console.warn(spec.description)
|
222
|
+
else if (spec.passed())
|
223
|
+
console.log(assertionCount + ' ' + spec.description)
|
224
|
+
else
|
225
|
+
console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
|
226
|
+
})
|
227
|
+
console.groupEnd()
|
228
|
+
}
|
229
|
+
})
|
230
|
+
}
|
231
|
+
},
|
232
|
+
|
233
|
+
Assertion : function(matcher, actual, expected, negate) {
|
234
|
+
extend(this, {
|
235
|
+
message: '',
|
236
|
+
passed: false,
|
237
|
+
actual: actual,
|
238
|
+
negate: negate,
|
239
|
+
matcher: matcher,
|
240
|
+
expected: expected,
|
241
|
+
|
242
|
+
// Report assertion results
|
243
|
+
|
244
|
+
report : function() {
|
245
|
+
this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
|
246
|
+
return this
|
247
|
+
},
|
248
|
+
|
249
|
+
// Run the assertion
|
250
|
+
|
251
|
+
run : function() {
|
252
|
+
// TODO: remove unshifting
|
253
|
+
expected.unshift(actual)
|
254
|
+
this.result = matcher.match.apply(this, expected)
|
255
|
+
this.passed = negate ? !this.result : this.result
|
256
|
+
if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
|
257
|
+
return this
|
258
|
+
}
|
259
|
+
})
|
260
|
+
},
|
261
|
+
|
262
|
+
ProxyAssertion : function(object, method, times, negate) {
|
263
|
+
var self = this
|
264
|
+
var old = object[method]
|
265
|
+
|
266
|
+
// Proxy
|
267
|
+
|
268
|
+
object[method] = function(){
|
269
|
+
args = argumentsToArray(arguments)
|
270
|
+
result = old.apply(object, args)
|
271
|
+
self.calls.push({ args : args, result : result })
|
272
|
+
return result
|
273
|
+
}
|
274
|
+
|
275
|
+
// Times
|
276
|
+
|
277
|
+
this.times = {
|
278
|
+
once : 1,
|
279
|
+
twice : 2
|
280
|
+
}[times] || times || 1
|
281
|
+
|
282
|
+
extend(this, {
|
283
|
+
calls: [],
|
284
|
+
message: '',
|
285
|
+
defer: true,
|
286
|
+
passed: false,
|
287
|
+
negate: negate,
|
288
|
+
object: object,
|
289
|
+
method: method,
|
290
|
+
|
291
|
+
// Proxy return value
|
292
|
+
|
293
|
+
and_return : function(result) {
|
294
|
+
this.expectedResult = result
|
295
|
+
return this
|
296
|
+
},
|
297
|
+
|
298
|
+
// Proxy arguments passed
|
299
|
+
|
300
|
+
with_args : function() {
|
301
|
+
this.expectedArgs = argumentsToArray(arguments)
|
302
|
+
return this
|
303
|
+
},
|
304
|
+
|
305
|
+
// Check if any calls have failing results
|
306
|
+
|
307
|
+
anyResultsFail : function() {
|
308
|
+
return any(this.calls, function(call){
|
309
|
+
return self.expectedResult.an_instance_of ?
|
310
|
+
call.result.constructor != self.expectedResult.an_instance_of:
|
311
|
+
hash(self.expectedResult) != hash(call.result)
|
312
|
+
})
|
313
|
+
},
|
314
|
+
|
315
|
+
// Check if any calls have passing results
|
316
|
+
|
317
|
+
anyResultsPass : function() {
|
318
|
+
return any(this.calls, function(call){
|
319
|
+
return self.expectedResult.an_instance_of ?
|
320
|
+
call.result.constructor == self.expectedResult.an_instance_of:
|
321
|
+
hash(self.expectedResult) == hash(call.result)
|
322
|
+
})
|
323
|
+
},
|
324
|
+
|
325
|
+
// Return the passing result
|
326
|
+
|
327
|
+
passingResult : function() {
|
328
|
+
return this.anyResultsPass().result
|
329
|
+
},
|
330
|
+
|
331
|
+
// Return the failing result
|
332
|
+
|
333
|
+
failingResult : function() {
|
334
|
+
return this.anyResultsFail().result
|
335
|
+
},
|
336
|
+
|
337
|
+
// Check if any arguments fail
|
338
|
+
|
339
|
+
anyArgsFail : function() {
|
340
|
+
return any(this.calls, function(call){
|
341
|
+
return any(self.expectedArgs, function(i, arg){
|
342
|
+
if (arg == null) return call.args[i] == null
|
343
|
+
return arg.an_instance_of ?
|
344
|
+
call.args[i].constructor != arg.an_instance_of:
|
345
|
+
hash(arg) != hash(call.args[i])
|
346
|
+
|
347
|
+
})
|
348
|
+
})
|
349
|
+
},
|
350
|
+
|
351
|
+
// Check if any arguments pass
|
352
|
+
|
353
|
+
anyArgsPass : function() {
|
354
|
+
return any(this.calls, function(call){
|
355
|
+
return any(self.expectedArgs, function(i, arg){
|
356
|
+
return arg.an_instance_of ?
|
357
|
+
call.args[i].constructor == arg.an_instance_of:
|
358
|
+
hash(arg) == hash(call.args[i])
|
359
|
+
|
360
|
+
})
|
361
|
+
})
|
362
|
+
},
|
363
|
+
|
364
|
+
// Return the passing args
|
365
|
+
|
366
|
+
passingArgs : function() {
|
367
|
+
return this.anyArgsPass().args
|
368
|
+
},
|
369
|
+
|
370
|
+
// Return the failing args
|
371
|
+
|
372
|
+
failingArgs : function() {
|
373
|
+
return this.anyArgsFail().args
|
374
|
+
},
|
375
|
+
|
376
|
+
// Report assertion results
|
377
|
+
|
378
|
+
report : function() {
|
379
|
+
this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures
|
380
|
+
return this
|
381
|
+
},
|
382
|
+
|
383
|
+
// Run the assertion
|
384
|
+
|
385
|
+
run : function() {
|
386
|
+
var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
|
387
|
+
|
388
|
+
function times(n) {
|
389
|
+
return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n]
|
390
|
+
}
|
391
|
+
|
392
|
+
if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
|
393
|
+
this.message = methodString + ' to return ' + puts(this.expectedResult) +
|
394
|
+
' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
|
395
|
+
|
396
|
+
if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
|
397
|
+
this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
|
398
|
+
' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
|
399
|
+
|
400
|
+
if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length == this.times : this.calls.length != this.times)
|
401
|
+
this.message = methodString + ' to be called ' + times(this.times) +
|
402
|
+
', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
|
403
|
+
|
404
|
+
if (!this.message.length)
|
405
|
+
this.passed = true
|
406
|
+
|
407
|
+
return this
|
408
|
+
}
|
409
|
+
})
|
410
|
+
},
|
411
|
+
|
412
|
+
/**
|
413
|
+
* Specification Suite block object.
|
414
|
+
*
|
415
|
+
* @param {string} description
|
416
|
+
* @param {function} body
|
417
|
+
* @api private
|
418
|
+
*/
|
419
|
+
|
420
|
+
Suite : function(description, body) {
|
421
|
+
var self = this
|
422
|
+
extend(this, {
|
423
|
+
body: body,
|
424
|
+
description: description,
|
425
|
+
suites: [],
|
426
|
+
specs: [],
|
427
|
+
ran: false,
|
428
|
+
hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
|
429
|
+
|
430
|
+
// Add a spec to the suite
|
431
|
+
|
432
|
+
addSpec : function(description, body) {
|
433
|
+
var spec = new JSpec.Spec(description, body)
|
434
|
+
this.specs.push(spec)
|
435
|
+
JSpec.stats.specs++ // TODO: abstract
|
436
|
+
spec.suite = this
|
437
|
+
},
|
438
|
+
|
439
|
+
// Add a hook to the suite
|
440
|
+
|
441
|
+
addHook : function(hook, body) {
|
442
|
+
this.hooks[hook].push(body)
|
443
|
+
},
|
444
|
+
|
445
|
+
// Add a nested suite
|
446
|
+
|
447
|
+
addSuite : function(description, body) {
|
448
|
+
var suite = new JSpec.Suite(description, body)
|
449
|
+
JSpec.allSuites.push(suite)
|
450
|
+
suite.name = suite.description
|
451
|
+
suite.description = this.description + ' ' + suite.description
|
452
|
+
this.suites.push(suite)
|
453
|
+
suite.suite = this
|
454
|
+
},
|
455
|
+
|
456
|
+
// Invoke a hook in context to this suite
|
457
|
+
|
458
|
+
hook : function(hook) {
|
459
|
+
if (this.suite) this.suite.hook(hook)
|
460
|
+
each(this.hooks[hook], function(body) {
|
461
|
+
JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
|
462
|
+
})
|
463
|
+
},
|
464
|
+
|
465
|
+
// Check if nested suites are present
|
466
|
+
|
467
|
+
hasSuites : function() {
|
468
|
+
return this.suites.length
|
469
|
+
},
|
470
|
+
|
471
|
+
// Check if this suite has specs
|
472
|
+
|
473
|
+
hasSpecs : function() {
|
474
|
+
return this.specs.length
|
475
|
+
},
|
476
|
+
|
477
|
+
// Check if the entire suite passed
|
478
|
+
|
479
|
+
passed : function() {
|
480
|
+
return !any(this.specs, function(spec){
|
481
|
+
return !spec.passed()
|
482
|
+
})
|
483
|
+
}
|
484
|
+
})
|
485
|
+
},
|
486
|
+
|
487
|
+
/**
|
488
|
+
* Specification block object.
|
489
|
+
*
|
490
|
+
* @param {string} description
|
491
|
+
* @param {function} body
|
492
|
+
* @api private
|
493
|
+
*/
|
494
|
+
|
495
|
+
Spec : function(description, body) {
|
496
|
+
extend(this, {
|
497
|
+
body: body,
|
498
|
+
description: description,
|
499
|
+
assertions: [],
|
500
|
+
|
501
|
+
// Add passing assertion
|
502
|
+
|
503
|
+
pass : function(message) {
|
504
|
+
this.assertions.push({ passed: true, message: message })
|
505
|
+
++JSpec.stats.passes
|
506
|
+
},
|
507
|
+
|
508
|
+
// Add failing assertion
|
509
|
+
|
510
|
+
fail : function(message) {
|
511
|
+
this.assertions.push({ passed: false, message: message })
|
512
|
+
++JSpec.stats.failures
|
513
|
+
},
|
514
|
+
|
515
|
+
// Run deferred assertions
|
516
|
+
|
517
|
+
runDeferredAssertions : function() {
|
518
|
+
each(this.assertions, function(assertion){
|
519
|
+
if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
|
520
|
+
})
|
521
|
+
},
|
522
|
+
|
523
|
+
// Find first failing assertion
|
524
|
+
|
525
|
+
failure : function() {
|
526
|
+
return find(this.assertions, function(assertion){
|
527
|
+
return !assertion.passed
|
528
|
+
})
|
529
|
+
},
|
530
|
+
|
531
|
+
// Find all failing assertions
|
532
|
+
|
533
|
+
failures : function() {
|
534
|
+
return select(this.assertions, function(assertion){
|
535
|
+
return !assertion.passed
|
536
|
+
})
|
537
|
+
},
|
538
|
+
|
539
|
+
// Weither or not the spec passed
|
540
|
+
|
541
|
+
passed : function() {
|
542
|
+
return !this.failure()
|
543
|
+
},
|
544
|
+
|
545
|
+
// Weither or not the spec requires implementation (no assertions)
|
546
|
+
|
547
|
+
requiresImplementation : function() {
|
548
|
+
return this.assertions.length == 0
|
549
|
+
},
|
550
|
+
|
551
|
+
// Sprite based assertions graph
|
552
|
+
|
553
|
+
assertionsGraph : function() {
|
554
|
+
return map(this.assertions, function(assertion){
|
555
|
+
return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
|
556
|
+
}).join('')
|
557
|
+
}
|
558
|
+
})
|
559
|
+
},
|
560
|
+
|
561
|
+
Module : function(methods) {
|
562
|
+
extend(this, methods)
|
563
|
+
},
|
564
|
+
|
565
|
+
JSON : {
|
566
|
+
|
567
|
+
/**
|
568
|
+
* Generic sequences.
|
569
|
+
*/
|
570
|
+
|
571
|
+
meta : {
|
572
|
+
'\b' : '\\b',
|
573
|
+
'\t' : '\\t',
|
574
|
+
'\n' : '\\n',
|
575
|
+
'\f' : '\\f',
|
576
|
+
'\r' : '\\r',
|
577
|
+
'"' : '\\"',
|
578
|
+
'\\' : '\\\\'
|
579
|
+
},
|
580
|
+
|
581
|
+
/**
|
582
|
+
* Escapable sequences.
|
583
|
+
*/
|
584
|
+
|
585
|
+
escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
586
|
+
|
587
|
+
/**
|
588
|
+
* JSON encode _object_.
|
589
|
+
*
|
590
|
+
* @param {mixed} object
|
591
|
+
* @return {string}
|
592
|
+
* @api private
|
593
|
+
*/
|
594
|
+
|
595
|
+
encode : function(object) {
|
596
|
+
var self = this
|
597
|
+
if (object == undefined || object == null) return 'null'
|
598
|
+
if (object === true) return 'true'
|
599
|
+
if (object === false) return 'false'
|
600
|
+
switch (typeof object) {
|
601
|
+
case 'number': return object
|
602
|
+
case 'string': return this.escapable.test(object) ?
|
603
|
+
'"' + object.replace(this.escapable, function (a) {
|
604
|
+
return typeof self.meta[a] === 'string' ? self.meta[a] :
|
605
|
+
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
|
606
|
+
}) + '"' :
|
607
|
+
'"' + object + '"'
|
608
|
+
case 'object':
|
609
|
+
if (object.constructor == Array)
|
610
|
+
return '[' + map(object, function(val){
|
611
|
+
return self.encode(val)
|
612
|
+
}).join(', ') + ']'
|
613
|
+
else if (object)
|
614
|
+
return '{' + map(object, function(key, val){
|
615
|
+
return self.encode(key) + ':' + self.encode(val)
|
616
|
+
}).join(', ') + '}'
|
617
|
+
}
|
618
|
+
return 'null'
|
619
|
+
}
|
620
|
+
},
|
621
|
+
|
622
|
+
// --- DSLs
|
623
|
+
|
624
|
+
DSLs : {
|
625
|
+
snake : {
|
626
|
+
expect : function(actual){
|
627
|
+
return JSpec.expect(actual)
|
628
|
+
},
|
629
|
+
|
630
|
+
describe : function(description, body) {
|
631
|
+
return JSpec.currentSuite.addSuite(description, body)
|
632
|
+
},
|
633
|
+
|
634
|
+
it : function(description, body) {
|
635
|
+
return JSpec.currentSuite.addSpec(description, body)
|
636
|
+
},
|
637
|
+
|
638
|
+
before : function(body) {
|
639
|
+
return JSpec.currentSuite.addHook('before', body)
|
640
|
+
},
|
641
|
+
|
642
|
+
after : function(body) {
|
643
|
+
return JSpec.currentSuite.addHook('after', body)
|
644
|
+
},
|
645
|
+
|
646
|
+
before_each : function(body) {
|
647
|
+
return JSpec.currentSuite.addHook('before_each', body)
|
648
|
+
},
|
649
|
+
|
650
|
+
after_each : function(body) {
|
651
|
+
return JSpec.currentSuite.addHook('after_each', body)
|
652
|
+
},
|
653
|
+
|
654
|
+
should_behave_like : function(description) {
|
655
|
+
return JSpec.shareBehaviorsOf(description)
|
656
|
+
}
|
657
|
+
}
|
658
|
+
},
|
659
|
+
|
660
|
+
// --- Methods
|
661
|
+
|
662
|
+
/**
|
663
|
+
* Check if _value_ is 'stop'. For use as a
|
664
|
+
* utility callback function.
|
665
|
+
*
|
666
|
+
* @param {mixed} value
|
667
|
+
* @return {bool}
|
668
|
+
* @api public
|
669
|
+
*/
|
670
|
+
|
671
|
+
haveStopped : function(value) {
|
672
|
+
return value === 'stop'
|
673
|
+
},
|
674
|
+
|
675
|
+
/**
|
676
|
+
* Include _object_ which may be a hash or Module instance.
|
677
|
+
*
|
678
|
+
* @param {has, Module} object
|
679
|
+
* @return {JSpec}
|
680
|
+
* @api public
|
681
|
+
*/
|
682
|
+
|
683
|
+
include : function(object) {
|
684
|
+
var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
|
685
|
+
this.modules.push(module)
|
686
|
+
if ('init' in module) module.init()
|
687
|
+
if ('utilities' in module) extend(this.defaultContext, module.utilities)
|
688
|
+
if ('matchers' in module) this.addMatchers(module.matchers)
|
689
|
+
if ('formatters' in module) extend(this.formatters, module.formatters)
|
690
|
+
if ('DSLs' in module)
|
691
|
+
each(module.DSLs, function(name, methods){
|
692
|
+
JSpec.DSLs[name] = JSpec.DSLs[name] || {}
|
693
|
+
extend(JSpec.DSLs[name], methods)
|
694
|
+
})
|
695
|
+
return this
|
696
|
+
},
|
697
|
+
|
698
|
+
/**
|
699
|
+
* Add a module hook _name_, which is immediately
|
700
|
+
* called per module with the _args_ given. An array of
|
701
|
+
* hook return values is returned.
|
702
|
+
*
|
703
|
+
* @param {name} string
|
704
|
+
* @param {...} args
|
705
|
+
* @return {array}
|
706
|
+
* @api private
|
707
|
+
*/
|
708
|
+
|
709
|
+
hook : function(name, args) {
|
710
|
+
args = argumentsToArray(arguments, 1)
|
711
|
+
return inject(JSpec.modules, [], function(results, module){
|
712
|
+
if (typeof module[name] == 'function')
|
713
|
+
results.push(JSpec.evalHook(module, name, args))
|
714
|
+
})
|
715
|
+
},
|
716
|
+
|
717
|
+
/**
|
718
|
+
* Eval _module_ hook _name_ with _args_. Evaluates in context
|
719
|
+
* to the module itself, JSpec, and JSpec.context.
|
720
|
+
*
|
721
|
+
* @param {Module} module
|
722
|
+
* @param {string} name
|
723
|
+
* @param {array} args
|
724
|
+
* @return {mixed}
|
725
|
+
* @api private
|
726
|
+
*/
|
727
|
+
|
728
|
+
evalHook : function(module, name, args) {
|
729
|
+
var context = this.context || this.defaultContext
|
730
|
+
var contents = this.contentsOf(module[name])
|
731
|
+
var params = this.paramsFor(module[name])
|
732
|
+
module.utilities = module.utilities || {}
|
733
|
+
params.unshift('context'); args.unshift(context)
|
734
|
+
hook('evaluatingHookBody', module, name, context)
|
735
|
+
try { return new Function(params.join(), 'with (this.utilities) { with (context) { with (JSpec) { ' + contents + ' }}}').apply(module, args) }
|
736
|
+
catch(e) { error('Error in hook ' + module.name + "." + name + ': ', e) }
|
737
|
+
},
|
738
|
+
|
739
|
+
/**
|
740
|
+
* Same as hook() however accepts only one _arg_ which is
|
741
|
+
* considered immutable. This function passes the arg
|
742
|
+
* to the first module, then passes the return value of the last
|
743
|
+
* module called, to the following module.
|
744
|
+
*
|
745
|
+
* @param {string} name
|
746
|
+
* @param {mixed} arg
|
747
|
+
* @return {mixed}
|
748
|
+
* @api private
|
749
|
+
*/
|
750
|
+
|
751
|
+
hookImmutable : function(name, arg) {
|
752
|
+
return inject(JSpec.modules, arg, function(result, module){
|
753
|
+
if (typeof module[name] == 'function')
|
754
|
+
return JSpec.evalHook(module, name, [result])
|
755
|
+
})
|
756
|
+
},
|
757
|
+
|
758
|
+
/**
|
759
|
+
* Find a suite by its description or name.
|
760
|
+
*
|
761
|
+
* @param {string} description
|
762
|
+
* @return {Suite}
|
763
|
+
* @api private
|
764
|
+
*/
|
765
|
+
|
766
|
+
findSuite : function(description) {
|
767
|
+
return find(this.allSuites, function(suite){
|
768
|
+
return suite.name == description || suite.description == description
|
769
|
+
})
|
770
|
+
},
|
771
|
+
|
772
|
+
/**
|
773
|
+
* Share behaviors (specs) of the given suite with
|
774
|
+
* the current suite.
|
775
|
+
*
|
776
|
+
* @param {string} description
|
777
|
+
* @api public
|
778
|
+
*/
|
779
|
+
|
780
|
+
shareBehaviorsOf : function(description) {
|
781
|
+
if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
|
782
|
+
else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
|
783
|
+
},
|
784
|
+
|
785
|
+
/**
|
786
|
+
* Copy specs from one suite to another.
|
787
|
+
*
|
788
|
+
* @param {Suite} fromSuite
|
789
|
+
* @param {Suite} toSuite
|
790
|
+
* @api public
|
791
|
+
*/
|
792
|
+
|
793
|
+
copySpecs : function(fromSuite, toSuite) {
|
794
|
+
each(fromSuite.specs, function(spec){
|
795
|
+
spec.assertions = []
|
796
|
+
toSuite.specs.push(spec)
|
797
|
+
})
|
798
|
+
},
|
799
|
+
|
800
|
+
/**
|
801
|
+
* Convert arguments to an array.
|
802
|
+
*
|
803
|
+
* @param {object} arguments
|
804
|
+
* @param {int} offset
|
805
|
+
* @return {array}
|
806
|
+
* @api public
|
807
|
+
*/
|
808
|
+
|
809
|
+
argumentsToArray : function(arguments, offset) {
|
810
|
+
return Array.prototype.slice.call(arguments, offset || 0)
|
811
|
+
},
|
812
|
+
|
813
|
+
/**
|
814
|
+
* Return ANSI-escaped colored string.
|
815
|
+
*
|
816
|
+
* @param {string} string
|
817
|
+
* @param {string} color
|
818
|
+
* @return {string}
|
819
|
+
* @api public
|
820
|
+
*/
|
821
|
+
|
822
|
+
color : function(string, color) {
|
823
|
+
return "\u001B[" + {
|
824
|
+
bold : 1,
|
825
|
+
black : 30,
|
826
|
+
red : 31,
|
827
|
+
green : 32,
|
828
|
+
yellow : 33,
|
829
|
+
blue : 34,
|
830
|
+
magenta : 35,
|
831
|
+
cyan : 36,
|
832
|
+
white : 37
|
833
|
+
}[color] + 'm' + string + "\u001B[0m"
|
834
|
+
},
|
835
|
+
|
836
|
+
/**
|
837
|
+
* Default matcher message callback.
|
838
|
+
*
|
839
|
+
* @api private
|
840
|
+
*/
|
841
|
+
|
842
|
+
defaultMatcherMessage : function(actual, expected, negate, name) {
|
843
|
+
return 'expected ' + puts(actual) + ' to ' +
|
844
|
+
(negate ? 'not ' : '') +
|
845
|
+
name.replace(/_/g, ' ') +
|
846
|
+
' ' + puts.apply(this, expected.slice(1))
|
847
|
+
},
|
848
|
+
|
849
|
+
/**
|
850
|
+
* Normalize a matcher message.
|
851
|
+
*
|
852
|
+
* When no messge callback is present the defaultMatcherMessage
|
853
|
+
* will be assigned, will suffice for most matchers.
|
854
|
+
*
|
855
|
+
* @param {hash} matcher
|
856
|
+
* @return {hash}
|
857
|
+
* @api public
|
858
|
+
*/
|
859
|
+
|
860
|
+
normalizeMatcherMessage : function(matcher) {
|
861
|
+
if (typeof matcher.message != 'function')
|
862
|
+
matcher.message = this.defaultMatcherMessage
|
863
|
+
return matcher
|
864
|
+
},
|
865
|
+
|
866
|
+
/**
|
867
|
+
* Normalize a matcher body
|
868
|
+
*
|
869
|
+
* This process allows the following conversions until
|
870
|
+
* the matcher is in its final normalized hash state.
|
871
|
+
*
|
872
|
+
* - '==' becomes 'actual == expected'
|
873
|
+
* - 'actual == expected' becomes 'return actual == expected'
|
874
|
+
* - function(actual, expected) { return actual == expected } becomes
|
875
|
+
* { match : function(actual, expected) { return actual == expected }}
|
876
|
+
*
|
877
|
+
* @param {mixed} body
|
878
|
+
* @return {hash}
|
879
|
+
* @api public
|
880
|
+
*/
|
881
|
+
|
882
|
+
normalizeMatcherBody : function(body) {
|
883
|
+
switch (body.constructor) {
|
884
|
+
case String:
|
885
|
+
if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
|
886
|
+
if (body.length < 4) body = 'actual ' + body + ' expected'
|
887
|
+
return { match: function(actual, expected) { return eval(body) }}
|
888
|
+
|
889
|
+
case Function:
|
890
|
+
return { match: body }
|
891
|
+
|
892
|
+
default:
|
893
|
+
return body
|
894
|
+
}
|
895
|
+
},
|
896
|
+
|
897
|
+
/**
|
898
|
+
* Get option value. This method first checks if
|
899
|
+
* the option key has been set via the query string,
|
900
|
+
* otherwise returning the options hash value.
|
901
|
+
*
|
902
|
+
* @param {string} key
|
903
|
+
* @return {mixed}
|
904
|
+
* @api public
|
905
|
+
*/
|
906
|
+
|
907
|
+
option : function(key) {
|
908
|
+
return (value = query(key)) !== null ? value :
|
909
|
+
JSpec.options[key] || null
|
910
|
+
},
|
911
|
+
|
912
|
+
/**
|
913
|
+
* Generates a hash of the object passed.
|
914
|
+
*
|
915
|
+
* @param {object} object
|
916
|
+
* @return {string}
|
917
|
+
* @api private
|
918
|
+
*/
|
919
|
+
|
920
|
+
hash : function(object) {
|
921
|
+
if (object == null) return 'null'
|
922
|
+
if (object == undefined) return 'undefined'
|
923
|
+
function serialize(prefix) {
|
924
|
+
return inject(object, prefix + ':', function(buffer, key, value){
|
925
|
+
return buffer += hash(value)
|
926
|
+
})
|
927
|
+
}
|
928
|
+
switch (object.constructor) {
|
929
|
+
case Array : return serialize('a')
|
930
|
+
case RegExp: return 'r:' + object.toString()
|
931
|
+
case Number: return 'n:' + object.toString()
|
932
|
+
case String: return 's:' + object.toString()
|
933
|
+
case Object: return 'o:' + inject(object, [], function(array, key, value){
|
934
|
+
array.push([key, hash(value)])
|
935
|
+
}).sort()
|
936
|
+
default: return object.toString()
|
937
|
+
}
|
938
|
+
},
|
939
|
+
|
940
|
+
/**
|
941
|
+
* Return last element of an array.
|
942
|
+
*
|
943
|
+
* @param {array} array
|
944
|
+
* @return {object}
|
945
|
+
* @api public
|
946
|
+
*/
|
947
|
+
|
948
|
+
last : function(array) {
|
949
|
+
return array[array.length - 1]
|
950
|
+
},
|
951
|
+
|
952
|
+
/**
|
953
|
+
* Convert object(s) to a print-friend string.
|
954
|
+
*
|
955
|
+
* @param {...} object
|
956
|
+
* @return {string}
|
957
|
+
* @api public
|
958
|
+
*/
|
959
|
+
|
960
|
+
puts : function(object) {
|
961
|
+
if (arguments.length > 1) {
|
962
|
+
return map(argumentsToArray(arguments), function(arg){
|
963
|
+
return puts(arg)
|
964
|
+
}).join(', ')
|
965
|
+
}
|
966
|
+
if (object === undefined) return ''
|
967
|
+
if (object === null) return 'null'
|
968
|
+
if (object === true) return 'true'
|
969
|
+
if (object === false) return 'false'
|
970
|
+
if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
|
971
|
+
if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + ''
|
972
|
+
if (object.jquery) return object.html()
|
973
|
+
if (object.nodeName) return object.outerHTML
|
974
|
+
switch (object.constructor) {
|
975
|
+
case String: return "'" + object + "'"
|
976
|
+
case Number: return object
|
977
|
+
case Function: return object.name || object
|
978
|
+
case Array:
|
979
|
+
return inject(object, '[', function(b, v){
|
980
|
+
return b + ', ' + puts(v)
|
981
|
+
}).replace('[,', '[') + ' ]'
|
982
|
+
case Object:
|
983
|
+
return inject(object, '{', function(b, k, v) {
|
984
|
+
return b + ', ' + puts(k) + ' : ' + puts(v)
|
985
|
+
}).replace('{,', '{') + ' }'
|
986
|
+
default:
|
987
|
+
return object.toString()
|
988
|
+
}
|
989
|
+
},
|
990
|
+
|
991
|
+
/**
|
992
|
+
* Escape HTML.
|
993
|
+
*
|
994
|
+
* @param {string} html
|
995
|
+
* @return {string}
|
996
|
+
* @api public
|
997
|
+
*/
|
998
|
+
|
999
|
+
escape : function(html) {
|
1000
|
+
return html.toString()
|
1001
|
+
.replace(/&/gmi, '&')
|
1002
|
+
.replace(/"/gmi, '"')
|
1003
|
+
.replace(/>/gmi, '>')
|
1004
|
+
.replace(/</gmi, '<')
|
1005
|
+
},
|
1006
|
+
|
1007
|
+
/**
|
1008
|
+
* Perform an assertion without reporting.
|
1009
|
+
*
|
1010
|
+
* This method is primarily used for internal
|
1011
|
+
* matchers in order retain DRYness. May be invoked
|
1012
|
+
* like below:
|
1013
|
+
*
|
1014
|
+
* does('foo', 'eql', 'foo')
|
1015
|
+
* does([1,2], 'include', 1, 2)
|
1016
|
+
*
|
1017
|
+
* External hooks are not run for internal assertions
|
1018
|
+
* performed by does().
|
1019
|
+
*
|
1020
|
+
* @param {mixed} actual
|
1021
|
+
* @param {string} matcher
|
1022
|
+
* @param {...} expected
|
1023
|
+
* @return {mixed}
|
1024
|
+
* @api private
|
1025
|
+
*/
|
1026
|
+
|
1027
|
+
does : function(actual, matcher, expected) {
|
1028
|
+
var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, argumentsToArray(arguments, 2))
|
1029
|
+
return assertion.run().result
|
1030
|
+
},
|
1031
|
+
|
1032
|
+
/**
|
1033
|
+
* Perform an assertion.
|
1034
|
+
*
|
1035
|
+
* expect(true).to('be', true)
|
1036
|
+
* expect('foo').not_to('include', 'bar')
|
1037
|
+
* expect([1, [2]]).to('include', 1, [2])
|
1038
|
+
*
|
1039
|
+
* @param {mixed} actual
|
1040
|
+
* @return {hash}
|
1041
|
+
* @api public
|
1042
|
+
*/
|
1043
|
+
|
1044
|
+
expect : function(actual) {
|
1045
|
+
assert = function(matcher, args, negate) {
|
1046
|
+
var expected = argumentsToArray(args, 1)
|
1047
|
+
matcher.negate = negate
|
1048
|
+
assertion = new JSpec.Assertion(matcher, actual, expected, negate)
|
1049
|
+
hook('beforeAssertion', assertion)
|
1050
|
+
if (matcher.defer) assertion.run()
|
1051
|
+
else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
|
1052
|
+
return assertion.result
|
1053
|
+
}
|
1054
|
+
|
1055
|
+
to = function(matcher) {
|
1056
|
+
return assert(matcher, arguments, false)
|
1057
|
+
}
|
1058
|
+
|
1059
|
+
not_to = function(matcher) {
|
1060
|
+
return assert(matcher, arguments, true)
|
1061
|
+
}
|
1062
|
+
|
1063
|
+
return {
|
1064
|
+
to : to,
|
1065
|
+
should : to,
|
1066
|
+
not_to: not_to,
|
1067
|
+
should_not : not_to
|
1068
|
+
}
|
1069
|
+
},
|
1070
|
+
|
1071
|
+
/**
|
1072
|
+
* Strim whitespace or chars.
|
1073
|
+
*
|
1074
|
+
* @param {string} string
|
1075
|
+
* @param {string} chars
|
1076
|
+
* @return {string}
|
1077
|
+
* @api public
|
1078
|
+
*/
|
1079
|
+
|
1080
|
+
strip : function(string, chars) {
|
1081
|
+
return string.
|
1082
|
+
replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
|
1083
|
+
replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
|
1084
|
+
},
|
1085
|
+
|
1086
|
+
/**
|
1087
|
+
* Call an iterator callback with arguments a, or b
|
1088
|
+
* depending on the arity of the callback.
|
1089
|
+
*
|
1090
|
+
* @param {function} callback
|
1091
|
+
* @param {mixed} a
|
1092
|
+
* @param {mixed} b
|
1093
|
+
* @return {mixed}
|
1094
|
+
* @api private
|
1095
|
+
*/
|
1096
|
+
|
1097
|
+
callIterator : function(callback, a, b) {
|
1098
|
+
return callback.length == 1 ? callback(b) : callback(a, b)
|
1099
|
+
},
|
1100
|
+
|
1101
|
+
/**
|
1102
|
+
* Extend an object with another.
|
1103
|
+
*
|
1104
|
+
* @param {object} object
|
1105
|
+
* @param {object} other
|
1106
|
+
* @api public
|
1107
|
+
*/
|
1108
|
+
|
1109
|
+
extend : function(object, other) {
|
1110
|
+
each(other, function(property, value){
|
1111
|
+
object[property] = value
|
1112
|
+
})
|
1113
|
+
},
|
1114
|
+
|
1115
|
+
/**
|
1116
|
+
* Iterate an object, invoking the given callback.
|
1117
|
+
*
|
1118
|
+
* @param {hash, array, string} object
|
1119
|
+
* @param {function} callback
|
1120
|
+
* @return {JSpec}
|
1121
|
+
* @api public
|
1122
|
+
*/
|
1123
|
+
|
1124
|
+
each : function(object, callback) {
|
1125
|
+
if (typeof object == 'string') object = object.split(' ')
|
1126
|
+
for (key in object)
|
1127
|
+
if (object.hasOwnProperty(key))
|
1128
|
+
callIterator(callback, key, object[key])
|
1129
|
+
},
|
1130
|
+
|
1131
|
+
/**
|
1132
|
+
* Iterate with memo.
|
1133
|
+
*
|
1134
|
+
* @param {hash, array} object
|
1135
|
+
* @param {object} memo
|
1136
|
+
* @param {function} callback
|
1137
|
+
* @return {object}
|
1138
|
+
* @api public
|
1139
|
+
*/
|
1140
|
+
|
1141
|
+
inject : function(object, memo, callback) {
|
1142
|
+
each(object, function(key, value){
|
1143
|
+
memo = (callback.length == 2 ?
|
1144
|
+
callback(memo, value):
|
1145
|
+
callback(memo, key, value)) ||
|
1146
|
+
memo
|
1147
|
+
})
|
1148
|
+
return memo
|
1149
|
+
},
|
1150
|
+
|
1151
|
+
/**
|
1152
|
+
* Destub _object_'s _method_. When no _method_ is passed
|
1153
|
+
* all stubbed methods are destubbed. When no arguments
|
1154
|
+
* are passed every object found in JSpec.stubbed will be
|
1155
|
+
* destubbed.
|
1156
|
+
*
|
1157
|
+
* @param {mixed} object
|
1158
|
+
* @param {string} method
|
1159
|
+
* @api public
|
1160
|
+
*/
|
1161
|
+
|
1162
|
+
destub : function(object, method) {
|
1163
|
+
if (method) {
|
1164
|
+
if (object['__prototype__' + method])
|
1165
|
+
delete object[method]
|
1166
|
+
else
|
1167
|
+
object[method] = object['__original__' + method]
|
1168
|
+
delete object['__prototype__' + method]
|
1169
|
+
delete object['__original____' + method]
|
1170
|
+
}
|
1171
|
+
else if (object) {
|
1172
|
+
for (var key in object)
|
1173
|
+
if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
|
1174
|
+
destub(object, captures[1])
|
1175
|
+
}
|
1176
|
+
else
|
1177
|
+
while (JSpec.stubbed.length)
|
1178
|
+
destub(JSpec.stubbed.shift())
|
1179
|
+
},
|
1180
|
+
|
1181
|
+
/**
|
1182
|
+
* Stub _object_'s _method_.
|
1183
|
+
*
|
1184
|
+
* stub(foo, 'toString').and_return('bar')
|
1185
|
+
*
|
1186
|
+
* @param {mixed} object
|
1187
|
+
* @param {string} method
|
1188
|
+
* @return {hash}
|
1189
|
+
* @api public
|
1190
|
+
*/
|
1191
|
+
|
1192
|
+
stub : function(object, method) {
|
1193
|
+
hook('stubbing', object, method)
|
1194
|
+
JSpec.stubbed.push(object)
|
1195
|
+
var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
|
1196
|
+
object[type + method] = object[method]
|
1197
|
+
object[method] = function(){}
|
1198
|
+
return {
|
1199
|
+
and_return : function(value) {
|
1200
|
+
if (typeof value == 'function') object[method] = value
|
1201
|
+
else object[method] = function(){ return value }
|
1202
|
+
}
|
1203
|
+
}
|
1204
|
+
},
|
1205
|
+
|
1206
|
+
/**
|
1207
|
+
* Map callback return values.
|
1208
|
+
*
|
1209
|
+
* @param {hash, array} object
|
1210
|
+
* @param {function} callback
|
1211
|
+
* @return {array}
|
1212
|
+
* @api public
|
1213
|
+
*/
|
1214
|
+
|
1215
|
+
map : function(object, callback) {
|
1216
|
+
return inject(object, [], function(memo, key, value){
|
1217
|
+
memo.push(callIterator(callback, key, value))
|
1218
|
+
})
|
1219
|
+
},
|
1220
|
+
|
1221
|
+
/**
|
1222
|
+
* Returns the first matching expression or null.
|
1223
|
+
*
|
1224
|
+
* @param {hash, array} object
|
1225
|
+
* @param {function} callback
|
1226
|
+
* @return {mixed}
|
1227
|
+
* @api public
|
1228
|
+
*/
|
1229
|
+
|
1230
|
+
any : function(object, callback) {
|
1231
|
+
return inject(object, null, function(state, key, value){
|
1232
|
+
if (state == undefined)
|
1233
|
+
return callIterator(callback, key, value) ? value : state
|
1234
|
+
})
|
1235
|
+
},
|
1236
|
+
|
1237
|
+
/**
|
1238
|
+
* Returns an array of values collected when the callback
|
1239
|
+
* given evaluates to true.
|
1240
|
+
*
|
1241
|
+
* @param {hash, array} object
|
1242
|
+
* @return {function} callback
|
1243
|
+
* @return {array}
|
1244
|
+
* @api public
|
1245
|
+
*/
|
1246
|
+
|
1247
|
+
select : function(object, callback) {
|
1248
|
+
return inject(object, [], function(selected, key, value){
|
1249
|
+
if (callIterator(callback, key, value))
|
1250
|
+
selected.push(value)
|
1251
|
+
})
|
1252
|
+
},
|
1253
|
+
|
1254
|
+
/**
|
1255
|
+
* Define matchers.
|
1256
|
+
*
|
1257
|
+
* @param {hash} matchers
|
1258
|
+
* @api public
|
1259
|
+
*/
|
1260
|
+
|
1261
|
+
addMatchers : function(matchers) {
|
1262
|
+
each(matchers, function(name, body){
|
1263
|
+
JSpec.addMatcher(name, body)
|
1264
|
+
})
|
1265
|
+
},
|
1266
|
+
|
1267
|
+
/**
|
1268
|
+
* Define a matcher.
|
1269
|
+
*
|
1270
|
+
* @param {string} name
|
1271
|
+
* @param {hash, function, string} body
|
1272
|
+
* @api public
|
1273
|
+
*/
|
1274
|
+
|
1275
|
+
addMatcher : function(name, body) {
|
1276
|
+
hook('addingMatcher', name, body)
|
1277
|
+
if (name.indexOf(' ') != -1) {
|
1278
|
+
var matchers = name.split(/\s+/)
|
1279
|
+
var prefix = matchers.shift()
|
1280
|
+
each(matchers, function(name) {
|
1281
|
+
JSpec.addMatcher(prefix + '_' + name, body(name))
|
1282
|
+
})
|
1283
|
+
}
|
1284
|
+
this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
|
1285
|
+
this.matchers[name].name = name
|
1286
|
+
},
|
1287
|
+
|
1288
|
+
/**
|
1289
|
+
* Add a root suite to JSpec.
|
1290
|
+
*
|
1291
|
+
* @param {string} description
|
1292
|
+
* @param {body} function
|
1293
|
+
* @api public
|
1294
|
+
*/
|
1295
|
+
|
1296
|
+
describe : function(description, body) {
|
1297
|
+
var suite = new JSpec.Suite(description, body)
|
1298
|
+
hook('addingSuite', suite)
|
1299
|
+
this.allSuites.push(suite)
|
1300
|
+
this.suites.push(suite)
|
1301
|
+
},
|
1302
|
+
|
1303
|
+
/**
|
1304
|
+
* Return the contents of a function body.
|
1305
|
+
*
|
1306
|
+
* @param {function} body
|
1307
|
+
* @return {string}
|
1308
|
+
* @api public
|
1309
|
+
*/
|
1310
|
+
|
1311
|
+
contentsOf : function(body) {
|
1312
|
+
return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
|
1313
|
+
},
|
1314
|
+
|
1315
|
+
/**
|
1316
|
+
* Return param names for a function body.
|
1317
|
+
*
|
1318
|
+
* @param {function} body
|
1319
|
+
* @return {array}
|
1320
|
+
* @api public
|
1321
|
+
*/
|
1322
|
+
|
1323
|
+
paramsFor : function(body) {
|
1324
|
+
return body.toString().match(/\((.*?)\)/)[1].match(/[\w]+/g) || []
|
1325
|
+
},
|
1326
|
+
|
1327
|
+
/**
|
1328
|
+
* Evaluate a JSpec capture body.
|
1329
|
+
*
|
1330
|
+
* @param {function} body
|
1331
|
+
* @param {string} errorMessage (optional)
|
1332
|
+
* @return {Type}
|
1333
|
+
* @api private
|
1334
|
+
*/
|
1335
|
+
|
1336
|
+
evalBody : function(body, errorMessage) {
|
1337
|
+
var dsl = this.DSL || this.DSLs.snake
|
1338
|
+
var matchers = this.matchers
|
1339
|
+
var context = this.context || this.defaultContext
|
1340
|
+
var contents = this.contentsOf(body)
|
1341
|
+
hook('evaluatingBody', dsl, matchers, context, contents)
|
1342
|
+
try { eval('with (dsl){ with (context) { with (matchers) { ' + contents + ' }}}') }
|
1343
|
+
catch(e) { error(errorMessage, e) }
|
1344
|
+
},
|
1345
|
+
|
1346
|
+
/**
|
1347
|
+
* Pre-process a string of JSpec.
|
1348
|
+
*
|
1349
|
+
* @param {string} input
|
1350
|
+
* @return {string}
|
1351
|
+
* @api private
|
1352
|
+
*/
|
1353
|
+
|
1354
|
+
preprocess : function(input) {
|
1355
|
+
if (typeof input != 'string') return
|
1356
|
+
input = hookImmutable('preprocessing', input)
|
1357
|
+
return input.
|
1358
|
+
replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
|
1359
|
+
replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
|
1360
|
+
replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
|
1361
|
+
replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
|
1362
|
+
replace(/^\s*end(?=\s|$)/gm, '});').
|
1363
|
+
replace(/-\{/g, 'function(){').
|
1364
|
+
replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
|
1365
|
+
replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
|
1366
|
+
replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
|
1367
|
+
replace(/, \)/gm, ')').
|
1368
|
+
replace(/should\.not/gm, 'should_not').
|
1369
|
+
replace(/__END__.*/m, '')
|
1370
|
+
},
|
1371
|
+
|
1372
|
+
/**
|
1373
|
+
* Create a range string which can be evaluated to a native array.
|
1374
|
+
*
|
1375
|
+
* @param {int} start
|
1376
|
+
* @param {int} end
|
1377
|
+
* @return {string}
|
1378
|
+
* @api public
|
1379
|
+
*/
|
1380
|
+
|
1381
|
+
range : function(start, end) {
|
1382
|
+
var current = parseInt(start), end = parseInt(end), values = [current]
|
1383
|
+
if (end > current) while (++current <= end) values.push(current)
|
1384
|
+
else while (--current >= end) values.push(current)
|
1385
|
+
return '[' + values + ']'
|
1386
|
+
},
|
1387
|
+
|
1388
|
+
/**
|
1389
|
+
* Report on the results.
|
1390
|
+
*
|
1391
|
+
* @api public
|
1392
|
+
*/
|
1393
|
+
|
1394
|
+
report : function() {
|
1395
|
+
hook('reporting', JSpec.options)
|
1396
|
+
new (JSpec.options.formatter || JSpec.formatters.DOM)(JSpec, JSpec.options)
|
1397
|
+
},
|
1398
|
+
|
1399
|
+
/**
|
1400
|
+
* Run the spec suites. Options are merged
|
1401
|
+
* with JSpec options when present.
|
1402
|
+
*
|
1403
|
+
* @param {hash} options
|
1404
|
+
* @return {JSpec}
|
1405
|
+
* @api public
|
1406
|
+
*/
|
1407
|
+
|
1408
|
+
run : function(options) {
|
1409
|
+
if (any(hook('running'), haveStopped)) return this
|
1410
|
+
if (options) extend(this.options, options)
|
1411
|
+
if (option('profile')) console.group('Profile')
|
1412
|
+
each(this.suites, function(suite) { JSpec.runSuite(suite) })
|
1413
|
+
if (option('profile')) console.groupEnd()
|
1414
|
+
return this
|
1415
|
+
},
|
1416
|
+
|
1417
|
+
/**
|
1418
|
+
* Run a suite.
|
1419
|
+
*
|
1420
|
+
* @param {Suite} suite
|
1421
|
+
* @api public
|
1422
|
+
*/
|
1423
|
+
|
1424
|
+
runSuite : function(suite) {
|
1425
|
+
this.currentSuite = suite
|
1426
|
+
this.evalBody(suite.body)
|
1427
|
+
suite.ran = true
|
1428
|
+
hook('beforeSuite', suite), suite.hook('before')
|
1429
|
+
each(suite.specs, function(spec) {
|
1430
|
+
hook('beforeSpec', spec)
|
1431
|
+
suite.hook('before_each')
|
1432
|
+
JSpec.runSpec(spec)
|
1433
|
+
hook('afterSpec', spec)
|
1434
|
+
suite.hook('after_each')
|
1435
|
+
})
|
1436
|
+
if (suite.hasSuites()) {
|
1437
|
+
each(suite.suites, function(suite) {
|
1438
|
+
JSpec.runSuite(suite)
|
1439
|
+
})
|
1440
|
+
}
|
1441
|
+
hook('afterSuite', suite), suite.hook('after')
|
1442
|
+
this.stats.suitesFinished++
|
1443
|
+
},
|
1444
|
+
|
1445
|
+
/**
|
1446
|
+
* Report a failure for the current spec.
|
1447
|
+
*
|
1448
|
+
* @param {string} message
|
1449
|
+
* @api public
|
1450
|
+
*/
|
1451
|
+
|
1452
|
+
fail : function(message) {
|
1453
|
+
JSpec.currentSpec.fail(message)
|
1454
|
+
},
|
1455
|
+
|
1456
|
+
/**
|
1457
|
+
* Report a passing assertion for the current spec.
|
1458
|
+
*
|
1459
|
+
* @param {string} message
|
1460
|
+
* @api public
|
1461
|
+
*/
|
1462
|
+
|
1463
|
+
pass : function(message) {
|
1464
|
+
JSpec.currentSpec.pass(message)
|
1465
|
+
},
|
1466
|
+
|
1467
|
+
/**
|
1468
|
+
* Run a spec.
|
1469
|
+
*
|
1470
|
+
* @param {Spec} spec
|
1471
|
+
* @api public
|
1472
|
+
*/
|
1473
|
+
|
1474
|
+
runSpec : function(spec) {
|
1475
|
+
this.currentSpec = spec
|
1476
|
+
if (option('profile')) console.time(spec.description)
|
1477
|
+
try { this.evalBody(spec.body) }
|
1478
|
+
catch (e) { fail(e) }
|
1479
|
+
if (option('profile')) console.timeEnd(spec.description)
|
1480
|
+
spec.runDeferredAssertions()
|
1481
|
+
destub()
|
1482
|
+
this.stats.specsFinished++
|
1483
|
+
this.stats.assertions += spec.assertions.length
|
1484
|
+
},
|
1485
|
+
|
1486
|
+
/**
|
1487
|
+
* Require a dependency, with optional message.
|
1488
|
+
*
|
1489
|
+
* @param {string} dependency
|
1490
|
+
* @param {string} message (optional)
|
1491
|
+
* @return {JSpec}
|
1492
|
+
* @api public
|
1493
|
+
*/
|
1494
|
+
|
1495
|
+
requires : function(dependency, message) {
|
1496
|
+
hook('requiring', dependency, message)
|
1497
|
+
try { eval(dependency) }
|
1498
|
+
catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
|
1499
|
+
return this
|
1500
|
+
},
|
1501
|
+
|
1502
|
+
/**
|
1503
|
+
* Query against the current query strings keys
|
1504
|
+
* or the queryString specified.
|
1505
|
+
*
|
1506
|
+
* @param {string} key
|
1507
|
+
* @param {string} queryString
|
1508
|
+
* @return {string, null}
|
1509
|
+
* @api private
|
1510
|
+
*/
|
1511
|
+
|
1512
|
+
query : function(key, queryString) {
|
1513
|
+
var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
|
1514
|
+
return inject(queryString.split('&'), null, function(value, pair){
|
1515
|
+
parts = pair.split('=')
|
1516
|
+
return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
|
1517
|
+
})
|
1518
|
+
},
|
1519
|
+
|
1520
|
+
/**
|
1521
|
+
* Throw a JSpec related error.
|
1522
|
+
*
|
1523
|
+
* @param {string} message
|
1524
|
+
* @param {Exception} e
|
1525
|
+
* @api public
|
1526
|
+
*/
|
1527
|
+
|
1528
|
+
error : function(message, e) {
|
1529
|
+
throw (message ? message : '') + e.toString() +
|
1530
|
+
(e.line ? ' near line ' + e.line : '')
|
1531
|
+
},
|
1532
|
+
|
1533
|
+
/**
|
1534
|
+
* Ad-hoc POST request for JSpec server usage.
|
1535
|
+
*
|
1536
|
+
* @param {string} uri
|
1537
|
+
* @param {string} data
|
1538
|
+
* @api private
|
1539
|
+
*/
|
1540
|
+
|
1541
|
+
post : function(uri, data) {
|
1542
|
+
if (any(hook('posting', uri, data), haveStopped)) return
|
1543
|
+
var request = this.xhr()
|
1544
|
+
request.open('POST', uri, false)
|
1545
|
+
request.setRequestHeader('Content-Type', 'application/json')
|
1546
|
+
request.send(JSpec.JSON.encode(data))
|
1547
|
+
},
|
1548
|
+
|
1549
|
+
/**
|
1550
|
+
* Instantiate an XMLHttpRequest.
|
1551
|
+
*
|
1552
|
+
* @return {XMLHttpRequest, ActiveXObject}
|
1553
|
+
* @api private
|
1554
|
+
*/
|
1555
|
+
|
1556
|
+
xhr : function() {
|
1557
|
+
return new (JSpec.request || ActiveXObject("Microsoft.XMLHTTP"))
|
1558
|
+
},
|
1559
|
+
|
1560
|
+
/**
|
1561
|
+
* Check for HTTP request support.
|
1562
|
+
*
|
1563
|
+
* @return {bool}
|
1564
|
+
* @api private
|
1565
|
+
*/
|
1566
|
+
|
1567
|
+
hasXhr : function() {
|
1568
|
+
return JSpec.request || 'ActiveXObject' in main
|
1569
|
+
},
|
1570
|
+
|
1571
|
+
/**
|
1572
|
+
* Try loading _file_ returning the contents
|
1573
|
+
* string or null. Chain to locate / read a file.
|
1574
|
+
*
|
1575
|
+
* @param {string} file
|
1576
|
+
* @return {string}
|
1577
|
+
* @api public
|
1578
|
+
*/
|
1579
|
+
|
1580
|
+
tryLoading : function(file) {
|
1581
|
+
try { return JSpec.load(file) }
|
1582
|
+
catch (e) {}
|
1583
|
+
},
|
1584
|
+
|
1585
|
+
/**
|
1586
|
+
* Load a _file_'s contents.
|
1587
|
+
*
|
1588
|
+
* @param {string} file
|
1589
|
+
* @param {function} callback
|
1590
|
+
* @return {string}
|
1591
|
+
* @api public
|
1592
|
+
*/
|
1593
|
+
|
1594
|
+
load : function(file, callback) {
|
1595
|
+
if (any(hook('loading', file), haveStopped)) return
|
1596
|
+
if ('readFile' in main)
|
1597
|
+
return callback ? readFile(file, callback) : readFile(file)
|
1598
|
+
else if (this.hasXhr()) {
|
1599
|
+
var request = this.xhr()
|
1600
|
+
request.open('GET', file, false)
|
1601
|
+
request.send(null)
|
1602
|
+
if (request.readyState == 4 &&
|
1603
|
+
(request.status == 0 ||
|
1604
|
+
parseInt(request.status.toString()[0]) == 2))
|
1605
|
+
return request.responseText
|
1606
|
+
}
|
1607
|
+
else
|
1608
|
+
error("failed to load `" + file + "'")
|
1609
|
+
},
|
1610
|
+
|
1611
|
+
/**
|
1612
|
+
* Load, pre-process, and evaluate a file.
|
1613
|
+
*
|
1614
|
+
* @param {string} file
|
1615
|
+
* @param {JSpec}
|
1616
|
+
* @api public
|
1617
|
+
*/
|
1618
|
+
|
1619
|
+
exec : function(file) {
|
1620
|
+
if (any(hook('executing', file), haveStopped)) return this
|
1621
|
+
if ('node' in main)
|
1622
|
+
this.load(file, function(contents){
|
1623
|
+
eval('with (JSpec){ ' + JSpec.preprocess(contents) + ' }')
|
1624
|
+
})
|
1625
|
+
else
|
1626
|
+
eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
|
1627
|
+
return this
|
1628
|
+
}
|
1629
|
+
}
|
1630
|
+
|
1631
|
+
// --- Utility functions
|
1632
|
+
|
1633
|
+
var main = this
|
1634
|
+
var find = JSpec.any
|
1635
|
+
var utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
|
1636
|
+
error escape extend puts hash query strip color does addMatchers callIterator argumentsToArray'.split(/\s+/)
|
1637
|
+
while (utils.length) util = utils.shift(), eval('var ' + util + ' = JSpec.' + util)
|
1638
|
+
if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
|
1639
|
+
|
1640
|
+
// --- Matchers
|
1641
|
+
|
1642
|
+
addMatchers({
|
1643
|
+
equal : "===",
|
1644
|
+
be : "alias equal",
|
1645
|
+
be_greater_than : ">",
|
1646
|
+
be_less_than : "<",
|
1647
|
+
be_at_least : ">=",
|
1648
|
+
be_at_most : "<=",
|
1649
|
+
be_a : "actual.constructor == expected",
|
1650
|
+
be_an : "alias be_a",
|
1651
|
+
be_an_instance_of : "actual instanceof expected",
|
1652
|
+
be_null : "actual == null",
|
1653
|
+
be_true : "actual == true",
|
1654
|
+
be_false : "actual == false",
|
1655
|
+
be_undefined : "typeof actual == 'undefined'",
|
1656
|
+
be_type : "typeof actual == expected",
|
1657
|
+
match : "typeof actual == 'string' ? actual.match(expected) : false",
|
1658
|
+
respond_to : "typeof actual[expected] == 'function'",
|
1659
|
+
have_length : "actual.length == expected",
|
1660
|
+
be_within : "actual >= expected[0] && actual <= last(expected)",
|
1661
|
+
have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
|
1662
|
+
|
1663
|
+
eql : function(actual, expected) {
|
1664
|
+
return actual.constructor == Array ||
|
1665
|
+
actual instanceof Object ?
|
1666
|
+
hash(actual) == hash(expected):
|
1667
|
+
actual == expected
|
1668
|
+
},
|
1669
|
+
|
1670
|
+
receive : { defer : true, match : function(actual, method, times) {
|
1671
|
+
proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
|
1672
|
+
JSpec.currentSpec.assertions.push(proxy)
|
1673
|
+
return proxy
|
1674
|
+
}},
|
1675
|
+
|
1676
|
+
be_empty : function(actual) {
|
1677
|
+
if (actual.constructor == Object && actual.length == undefined)
|
1678
|
+
for (var key in actual)
|
1679
|
+
return false;
|
1680
|
+
return !actual.length
|
1681
|
+
},
|
1682
|
+
|
1683
|
+
include : function(actual) {
|
1684
|
+
for (state = true, i = 1; i < arguments.length; i++) {
|
1685
|
+
arg = arguments[i]
|
1686
|
+
switch (actual.constructor) {
|
1687
|
+
case String:
|
1688
|
+
case Number:
|
1689
|
+
case RegExp:
|
1690
|
+
case Function:
|
1691
|
+
state = actual.toString().match(arg.toString())
|
1692
|
+
break
|
1693
|
+
|
1694
|
+
case Object:
|
1695
|
+
state = arg in actual
|
1696
|
+
break
|
1697
|
+
|
1698
|
+
case Array:
|
1699
|
+
state = any(actual, function(value){ return hash(value) == hash(arg) })
|
1700
|
+
break
|
1701
|
+
}
|
1702
|
+
if (!state) return false
|
1703
|
+
}
|
1704
|
+
return true
|
1705
|
+
},
|
1706
|
+
|
1707
|
+
throw_error : { match : function(actual, expected, message) {
|
1708
|
+
try { actual() }
|
1709
|
+
catch (e) {
|
1710
|
+
this.e = e
|
1711
|
+
var assert = function(arg) {
|
1712
|
+
switch (arg.constructor) {
|
1713
|
+
case RegExp : return arg.test(e)
|
1714
|
+
case String : return arg == (e.message || e.toString())
|
1715
|
+
case Function : return (e.name || 'Error') == arg.name
|
1716
|
+
}
|
1717
|
+
}
|
1718
|
+
return message ? assert(expected) && assert(message) :
|
1719
|
+
expected ? assert(expected) :
|
1720
|
+
true
|
1721
|
+
}
|
1722
|
+
}, message : function(actual, expected, negate) {
|
1723
|
+
// TODO: refactor when actual is not in expected [0]
|
1724
|
+
var message_for = function(i) {
|
1725
|
+
if (expected[i] == undefined) return 'exception'
|
1726
|
+
switch (expected[i].constructor) {
|
1727
|
+
case RegExp : return 'exception matching ' + puts(expected[i])
|
1728
|
+
case String : return 'exception of ' + puts(expected[i])
|
1729
|
+
case Function : return expected[i].name || 'Error'
|
1730
|
+
}
|
1731
|
+
}
|
1732
|
+
exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
|
1733
|
+
return 'expected ' + exception + (negate ? ' not ' : '' ) +
|
1734
|
+
' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
|
1735
|
+
}},
|
1736
|
+
|
1737
|
+
have : function(actual, length, property) {
|
1738
|
+
return actual[property].length == length
|
1739
|
+
},
|
1740
|
+
|
1741
|
+
have_at_least : function(actual, length, property) {
|
1742
|
+
return actual[property].length >= length
|
1743
|
+
},
|
1744
|
+
|
1745
|
+
have_at_most :function(actual, length, property) {
|
1746
|
+
return actual[property].length <= length
|
1747
|
+
},
|
1748
|
+
|
1749
|
+
have_within : function(actual, range, property) {
|
1750
|
+
length = actual[property].length
|
1751
|
+
return length >= range.shift() && length <= range.pop()
|
1752
|
+
},
|
1753
|
+
|
1754
|
+
have_prop : function(actual, property, value) {
|
1755
|
+
return actual[property] == null ||
|
1756
|
+
actual[property] instanceof Function ? false:
|
1757
|
+
value == null ? true:
|
1758
|
+
does(actual[property], 'eql', value)
|
1759
|
+
},
|
1760
|
+
|
1761
|
+
have_property : function(actual, property, value) {
|
1762
|
+
return actual[property] == null ||
|
1763
|
+
actual[property] instanceof Function ? false:
|
1764
|
+
value == null ? true:
|
1765
|
+
value === actual[property]
|
1766
|
+
}
|
1767
|
+
})
|
1768
|
+
|
1769
|
+
if ('exports' in main) exports.JSpec = JSpec
|
1770
|
+
|
1771
|
+
})()
|