visionmedia-jspec 1.1.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/jspec.js CHANGED
@@ -5,18 +5,15 @@
5
5
 
6
6
  var JSpec = {
7
7
 
8
- version : '1.1.7',
9
- file : '',
10
- suites : [],
11
- matchers : {},
12
- stats : { specs : 0, assertions : 0, failures : 0, passes : 0 },
13
- options : { profile : false },
8
+ version : '2.0.0',
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
14
 
15
15
  /**
16
16
  * Default context in which bodies are evaluated.
17
- * This allows specs and hooks to use the 'this' keyword in
18
- * order to store variables, as well as allowing the context
19
- * to provide helper methods or properties.
20
17
  *
21
18
  * Replace context simply by setting JSpec.context
22
19
  * to your own like below:
@@ -32,12 +29,40 @@
32
29
  *
33
30
  */
34
31
 
35
- defaultContext : {
36
- sandbox : function(name) {
37
- sandbox = document.createElement('div')
38
- sandbox.setAttribute('class', 'jspec-sandbox')
39
- document.body.appendChild(sandbox)
40
- return sandbox
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
41
66
  }
42
67
  },
43
68
 
@@ -60,14 +85,21 @@
60
85
  report = document.getElementById(id)
61
86
  failuresOnly = option('failuresOnly')
62
87
  classes = results.stats.failures ? 'has-failures' : ''
63
- if (!report) error('requires the element #' + id + ' to output its reports')
88
+ if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
64
89
 
65
90
  markup =
66
91
  '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
67
92
  <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
68
93
  <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
69
94
  </div><table class="suites">'
70
-
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
+
71
103
  renderSuite = function(suite) {
72
104
  displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
73
105
  if (displaySuite && suite.hasSpecs()) {
@@ -80,7 +112,7 @@
80
112
  markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
81
113
  else if(!spec.passed())
82
114
  markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
83
- markup += '<tr class="body" style="display: none;"><td colspan="2">' + spec.body + '</td></tr>'
115
+ markup += '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
84
116
  })
85
117
  markup += '</tr>'
86
118
  }
@@ -94,9 +126,7 @@
94
126
  }
95
127
 
96
128
  renderSuites(results.suites)
97
-
98
129
  markup += '</table></div>'
99
-
100
130
  report.innerHTML = markup
101
131
  },
102
132
 
@@ -119,7 +149,7 @@
119
149
  displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
120
150
  if (displaySuite && suite.hasSpecs()) {
121
151
  puts(color(' ' + suite.description, 'bold'))
122
- results.each(suite.specs, function(spec){
152
+ each(suite.specs, function(spec){
123
153
  assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
124
154
  return graph + color('.', assertion.passed ? 'green' : 'red')
125
155
  })
@@ -127,9 +157,9 @@
127
157
  puts(color(' ' + spec.description, 'blue') + assertionsGraph)
128
158
  else if (spec.passed() && !failuresOnly)
129
159
  puts(color(' ' + spec.description, 'green') + assertionsGraph)
130
- else
160
+ else if (!spec.passed())
131
161
  puts(color(' ' + spec.description, 'red') + assertionsGraph +
132
- "\n" + indent(spec.failure().message) + "\n")
162
+ "\n" + indent(spec.failure().message) + "\n")
133
163
  })
134
164
  puts('')
135
165
  }
@@ -158,7 +188,7 @@
158
188
  renderSuite = function(suite) {
159
189
  if (suite.ran) {
160
190
  console.group(suite.description)
161
- results.each(suite.specs, function(spec){
191
+ each(suite.specs, function(spec){
162
192
  assertionCount = spec.assertions.length + ':'
163
193
  if (spec.requiresImplementation())
164
194
  console.warn(spec.description)
@@ -190,22 +220,141 @@
190
220
  negate : negate,
191
221
  matcher : matcher,
192
222
  expected : expected,
193
- record : function(result) {
194
- result ? JSpec.stats.passes++ : JSpec.stats.failures++
223
+
224
+ // Report assertion results
225
+
226
+ report : function() {
227
+ this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
228
+ return this
195
229
  },
196
230
 
197
- exec : function() {
231
+ // Run the assertion
232
+
233
+ run : function() {
198
234
  // TODO: remove unshifting of expected
199
235
  expected.unshift(actual == null ? null : actual.valueOf())
200
- result = matcher.match.apply(JSpec, expected)
201
- this.passed = negate ? !result : result
202
- this.record(this.passed)
236
+ this.result = matcher.match.apply(JSpec, expected)
237
+ this.passed = negate ? !this.result : this.result
203
238
  if (!this.passed) this.message = matcher.message(actual, expected, negate, matcher.name)
204
239
  return this
205
240
  }
206
241
  })
207
242
  },
208
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
+ methodString = 'expected ' + object.toString() + '.' + method + '()'
334
+ times = function(n) {
335
+ return n > 2 ? n + ' times' : { 1 : 'once', 2 : 'twice' }[n]
336
+ }
337
+
338
+ if (this.calls.length < this.times)
339
+ this.message = methodString + ' to be called ' + times(this.times) +
340
+ ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
341
+
342
+ if (this.expectedResult && this.anyResultsFail())
343
+ this.message = methodString + ' to return ' + print(this.expectedResult) +
344
+ ' but got ' + print(this.failingResult())
345
+
346
+ if (this.expectedArgs && this.anyArgsFail())
347
+ this.message = methodString + ' to be called with ' + print.apply(this, this.expectedArgs) +
348
+ ' but was called with ' + print.apply(this, this.failingArgs())
349
+
350
+ if (!this.message.length)
351
+ this.passed = true
352
+
353
+ return this
354
+ }
355
+ })
356
+ },
357
+
209
358
  /**
210
359
  * Specification Suite block object.
211
360
  *
@@ -215,6 +364,7 @@
215
364
  */
216
365
 
217
366
  Suite : function(description, body) {
367
+ var self = this
218
368
  extend(this, {
219
369
  body: body,
220
370
  description: description,
@@ -225,9 +375,10 @@
225
375
 
226
376
  // Add a spec to the suite
227
377
 
228
- it : function(description, body) {
378
+ addSpec : function(description, body) {
229
379
  spec = new JSpec.Spec(description, body)
230
380
  this.specs.push(spec)
381
+ JSpec.stats.specs++ // TODO: abstract
231
382
  spec.suite = this
232
383
  },
233
384
 
@@ -239,8 +390,10 @@
239
390
 
240
391
  // Add a nested suite
241
392
 
242
- describe : function(description, body) {
393
+ addSuite : function(description, body) {
243
394
  suite = new JSpec.Suite(description, body)
395
+ JSpec.allSuites.push(suite)
396
+ suite.name = suite.description
244
397
  suite.description = this.description + ' ' + suite.description
245
398
  this.suites.push(suite)
246
399
  suite.suite = this
@@ -251,7 +404,7 @@
251
404
  hook : function(hook) {
252
405
  if (this.suite) this.suite.hook(hook)
253
406
  each(this.hooks[hook], function(body) {
254
- JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
407
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
255
408
  })
256
409
  },
257
410
 
@@ -270,11 +423,9 @@
270
423
  // Check if the entire suite passed
271
424
 
272
425
  passed : function() {
273
- var passed = true
274
- each(this.specs, function(spec){
275
- if (!spec.passed()) passed = false
426
+ return !any(this.specs, function(spec){
427
+ return !spec.passed()
276
428
  })
277
- return passed
278
429
  }
279
430
  })
280
431
  },
@@ -293,20 +444,27 @@
293
444
  description : description,
294
445
  assertions : [],
295
446
 
447
+ // Run deferred assertions
448
+
449
+ runDeferredAssertions : function() {
450
+ each(this.assertions, function(assertion){
451
+ if (assertion.defer) assertion.run().report()
452
+ })
453
+ },
454
+
296
455
  // Find first failing assertion
297
456
 
298
457
  failure : function() {
299
- return inject(this.assertions, null, function(failure, assertion){
300
- return !assertion.passed && !failure ? assertion : failure
458
+ return find(this.assertions, function(assertion){
459
+ return !assertion.passed
301
460
  })
302
461
  },
303
462
 
304
463
  // Find all failing assertions
305
464
 
306
465
  failures : function() {
307
- return inject(this.assertions, [], function(failures, assertion){
308
- if (!assertion.passed) failures.push(assertion)
309
- return failures
466
+ return select(this.assertions, function(assertion){
467
+ return !assertion.passed
310
468
  })
311
469
  },
312
470
 
@@ -331,9 +489,103 @@
331
489
  }
332
490
  })
333
491
  },
492
+
493
+ // --- DSLs
494
+
495
+ DSLs : {
496
+ snake : {
497
+ expect : function(actual){
498
+ return JSpec.expect(actual)
499
+ },
500
+
501
+ describe : function(description, body) {
502
+ return JSpec.currentSuite.addSuite(description, body)
503
+ },
504
+
505
+ it : function(description, body) {
506
+ return JSpec.currentSuite.addSpec(description, body)
507
+ },
508
+
509
+ before : function(body) {
510
+ return JSpec.currentSuite.addHook('before', body)
511
+ },
512
+
513
+ after : function(body) {
514
+ return JSpec.currentSuite.addHook('after', body)
515
+ },
516
+
517
+ before_each : function(body) {
518
+ return JSpec.currentSuite.addHook('before_each', body)
519
+ },
520
+
521
+ after_each : function(body) {
522
+ return JSpec.currentSuite.addHook('after_each', body)
523
+ },
524
+
525
+ should_behave_like : function(description) {
526
+ return JSpec.shareBehaviorsOf(description)
527
+ }
528
+ }
529
+ },
334
530
 
335
531
  // --- Methods
336
532
 
533
+ /**
534
+ * Find a suite by its description or name.
535
+ *
536
+ * @param {string} description
537
+ * @return {Suite}
538
+ * @api private
539
+ */
540
+
541
+ findSuite : function(description) {
542
+ return find(this.allSuites, function(suite){
543
+ return suite.name == description || suite.description == description
544
+ })
545
+ },
546
+
547
+ /**
548
+ * Share behaviors (specs) of the given suite with
549
+ * the current suite.
550
+ *
551
+ * @param {string} description
552
+ * @api public
553
+ */
554
+
555
+ shareBehaviorsOf : function(description) {
556
+ if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
557
+ else throw 'failed to share behaviors. ' + print(description) + ' is not a valid Suite name'
558
+ },
559
+
560
+ /**
561
+ * Copy specs from one suite to another.
562
+ *
563
+ * @param {Suite} fromSuite
564
+ * @param {Suite} toSuite
565
+ * @api public
566
+ */
567
+
568
+ copySpecs : function(fromSuite, toSuite) {
569
+ each(fromSuite.specs, function(spec){
570
+ toSuite.specs.push(spec)
571
+ })
572
+ },
573
+
574
+ /**
575
+ * Convert arguments to an array.
576
+ *
577
+ * @param {object} arguments
578
+ * @param {int} offset
579
+ * @return {array}
580
+ * @api public
581
+ */
582
+
583
+ argumentsToArray : function(arguments, offset) {
584
+ args = []
585
+ for (i = 0; i < arguments.length; i++) args.push(arguments[i])
586
+ return args.slice(offset || 0)
587
+ },
588
+
337
589
  /**
338
590
  * Return ANSI-escaped colored string.
339
591
  *
@@ -442,13 +694,14 @@
442
694
  */
443
695
 
444
696
  hash : function(object) {
697
+ if (object == undefined) return 'undefined'
445
698
  serialize = function(prefix) {
446
699
  return inject(object, prefix + ':', function(buffer, key, value){
447
700
  return buffer += hash(value)
448
701
  })
449
702
  }
450
703
  switch (object.constructor) {
451
- case Array: return serialize('a')
704
+ case Array : return serialize('a')
452
705
  case Object: return serialize('o')
453
706
  case RegExp: return 'r:' + object.toString()
454
707
  case Number: return 'n:' + object.toString()
@@ -479,28 +732,29 @@
479
732
 
480
733
  print : function(object) {
481
734
  if (arguments.length > 1) {
482
- list = []
483
- for (i = 0; i < arguments.length; i++) list.push(print(arguments[i]))
484
- return list.join(', ')
735
+ return map(argumentsToArray(arguments), function(arg){
736
+ return print(arg)
737
+ }).join(', ')
485
738
  }
486
739
  if (object === undefined) return ''
487
740
  if (object === null) return 'null'
488
741
  if (object === true) return 'true'
489
742
  if (object === false) return 'false'
743
+ if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
490
744
  if (object.jquery && object.selector.length > 0) return 'selector ' + print(object.selector) + ''
491
745
  if (object.jquery) return escape(object.html())
492
746
  if (object.nodeName) return escape(object.outerHTML)
493
747
  switch (object.constructor) {
494
748
  case String: return "'" + escape(object) + "'"
495
749
  case Number: return object
496
- case Array :
497
- buff = '['
498
- each(object, function(v){ buff += ', ' + print(v) })
499
- return buff.replace('[,', '[') + ' ]'
750
+ case Array :
751
+ return inject(object, '[', function(b, v){
752
+ return b + ', ' + print(v)
753
+ }).replace('[,', '[') + ' ]'
500
754
  case Object:
501
- buff = '{'
502
- each(object, function(k, v){ buff += ', ' + print(k) + ' : ' + print(v)})
503
- return buff.replace('{,', '{') + ' }'
755
+ return inject(object, '{', function(b, k, v) {
756
+ return b + ', ' + print(k) + ' : ' + print(v)
757
+ }).replace('{,', '{') + ' }'
504
758
  default:
505
759
  return escape(object.toString())
506
760
  }
@@ -515,13 +769,34 @@
515
769
  */
516
770
 
517
771
  escape : function(html) {
518
- if (typeof html != 'string') return html
519
- return html.
772
+ return html.toString().
520
773
  replace(/&/gmi, '&amp;').
521
774
  replace(/"/gmi, '&quot;').
522
775
  replace(/>/gmi, '&gt;').
523
776
  replace(/</gmi, '&lt;')
524
777
  },
778
+
779
+ /**
780
+ * Perform an assertion without reporting.
781
+ *
782
+ * This method is primarily used for internal
783
+ * matchers in order retain DRYness. May be invoked
784
+ * like below:
785
+ *
786
+ * does('foo', 'eql', 'foo')
787
+ * does([1,2], 'include', 1, 2)
788
+ *
789
+ * @param {mixed} actual
790
+ * @param {string} matcher
791
+ * @param {...} expected
792
+ * @return {mixed}
793
+ * @api private
794
+ */
795
+
796
+ does : function(actual, matcher, expected) {
797
+ assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, argumentsToArray(arguments, 2))
798
+ return assertion.run().result
799
+ },
525
800
 
526
801
  /**
527
802
  * Perform an assertion.
@@ -536,20 +811,21 @@
536
811
  */
537
812
 
538
813
  expect : function(actual) {
539
- assert = function(name, args, negate) {
814
+ assert = function(matcher, args, negate) {
540
815
  expected = []
541
816
  for (i = 1; i < args.length; i++) expected.push(args[i])
542
- assertion = new JSpec.Assertion(JSpec.matchers[name], actual, expected, negate)
543
- JSpec.currentSpec.assertions.push(assertion.exec())
544
- return assertion.passed
817
+ assertion = new JSpec.Assertion(matcher, actual, expected, negate)
818
+ if (matcher.defer) assertion.run()
819
+ else JSpec.currentSpec.assertions.push(assertion.run().report())
820
+ return assertion.result
545
821
  }
546
822
 
547
- to = function(name) {
548
- return assert(name, arguments, false)
823
+ to = function(matcher) {
824
+ return assert(matcher, arguments, false)
549
825
  }
550
826
 
551
- not_to = function(name) {
552
- return assert(name, arguments, true)
827
+ not_to = function(matcher) {
828
+ return assert(matcher, arguments, true)
553
829
  }
554
830
 
555
831
  return {
@@ -560,45 +836,6 @@
560
836
  }
561
837
  },
562
838
 
563
- /**
564
- * Iterate an object, invoking the given callback.
565
- *
566
- * @param {hash, array, string} object
567
- * @param {function} callback
568
- * @return {JSpec}
569
- * @api public
570
- */
571
-
572
- each : function(object, callback) {
573
- if (typeof object == 'string') object = object.split(' ')
574
- for (key in object) {
575
- if (object.hasOwnProperty(key))
576
- callback.length == 1 ?
577
- callback.call(JSpec, object[key]):
578
- callback.call(JSpec, key, object[key])
579
- }
580
- return JSpec
581
- },
582
-
583
- /**
584
- * Iterate with memo.
585
- *
586
- * @param {hash, array} object
587
- * @param {object} initial
588
- * @param {function} callback
589
- * @return {object}
590
- * @api public
591
- */
592
-
593
- inject : function(object, initial, callback) {
594
- each(object, function(key, value){
595
- initial = callback.length == 2 ?
596
- callback.call(JSpec, initial, value):
597
- callback.call(JSpec, initial, key, value) || initial
598
- })
599
- return initial
600
- },
601
-
602
839
  /**
603
840
  * Strim whitespace or chars.
604
841
  *
@@ -614,6 +851,21 @@
614
851
  replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
615
852
  },
616
853
 
854
+ /**
855
+ * Call an iterator callback with arguments a, or b
856
+ * depending on the arity of the callback.
857
+ *
858
+ * @param {function} callback
859
+ * @param {mixed} a
860
+ * @param {mixed} b
861
+ * @return {mixed}
862
+ * @api private
863
+ */
864
+
865
+ callIterator : function(callback, a, b) {
866
+ return callback.length == 1 ? callback(b) : callback(a, b)
867
+ },
868
+
617
869
  /**
618
870
  * Extend an object with another.
619
871
  *
@@ -627,7 +879,43 @@
627
879
  object[property] = value
628
880
  })
629
881
  },
630
-
882
+
883
+ /**
884
+ * Iterate an object, invoking the given callback.
885
+ *
886
+ * @param {hash, array, string} object
887
+ * @param {function} callback
888
+ * @return {JSpec}
889
+ * @api public
890
+ */
891
+
892
+ each : function(object, callback) {
893
+ if (typeof object == 'string') object = object.split(' ')
894
+ for (key in object)
895
+ if (object.hasOwnProperty(key))
896
+ callIterator(callback, key, object[key])
897
+ },
898
+
899
+ /**
900
+ * Iterate with memo.
901
+ *
902
+ * @param {hash, array} object
903
+ * @param {object} memo
904
+ * @param {function} callback
905
+ * @return {object}
906
+ * @api public
907
+ */
908
+
909
+ inject : function(object, memo, callback) {
910
+ each(object, function(key, value){
911
+ memo = (callback.length == 2 ?
912
+ callback(memo, value):
913
+ callback(memo, key, value)) ||
914
+ memo
915
+ })
916
+ return memo
917
+ },
918
+
631
919
  /**
632
920
  * Map callback return values.
633
921
  *
@@ -639,27 +927,41 @@
639
927
 
640
928
  map : function(object, callback) {
641
929
  return inject(object, [], function(memo, key, value){
642
- memo.push(callback.length == 1 ?
643
- callback.call(JSpec, value):
644
- callback.call(JSpec, key, value))
930
+ memo.push(callIterator(callback, key, value))
645
931
  })
646
932
  },
647
933
 
648
934
  /**
649
- * Returns true if the callback returns true at least once.
935
+ * Returns the first matching expression or null.
650
936
  *
651
937
  * @param {hash, array} object
652
938
  * @param {function} callback
653
- * @return {bool}
939
+ * @return {mixed}
654
940
  * @api public
655
941
  */
656
942
 
657
943
  any : function(object, callback) {
658
- return inject(object, false, function(state, key, value){
659
- if (state) return true
660
- return callback.length == 1 ?
661
- callback.call(JSpec, value):
662
- callback.call(JSpec, key, value)
944
+ return inject(object, null, function(state, key, value){
945
+ return state ? state :
946
+ callIterator(callback, key, value) ?
947
+ value : state
948
+ })
949
+ },
950
+
951
+ /**
952
+ * Returns an array of values collected when the callback
953
+ * given evaluates to true.
954
+ *
955
+ * @param {hash, array} object
956
+ * @return {function} callback
957
+ * @return {array}
958
+ * @api public
959
+ */
960
+
961
+ select : function(object, callback) {
962
+ return inject(object, [], function(memo, key, value){
963
+ if (callIterator(callback, key, value))
964
+ memo.push(value)
663
965
  })
664
966
  },
665
967
 
@@ -667,15 +969,13 @@
667
969
  * Define matchers.
668
970
  *
669
971
  * @param {hash} matchers
670
- * @return {JSpec}
671
972
  * @api public
672
973
  */
673
974
 
674
975
  addMatchers : function(matchers) {
675
976
  each(matchers, function(name, body){
676
- this.addMatcher(name, body)
977
+ JSpec.addMatcher(name, body)
677
978
  })
678
- return this
679
979
  },
680
980
 
681
981
  /**
@@ -683,14 +983,12 @@
683
983
  *
684
984
  * @param {string} name
685
985
  * @param {hash, function, string} body
686
- * @return {JSpec}
687
986
  * @api public
688
987
  */
689
988
 
690
989
  addMatcher : function(name, body) {
691
990
  this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
692
991
  this.matchers[name].name = name
693
- return this
694
992
  },
695
993
 
696
994
  /**
@@ -698,13 +996,25 @@
698
996
  *
699
997
  * @param {string} description
700
998
  * @param {body} function
701
- * @return {JSpec}
702
999
  * @api public
703
1000
  */
704
1001
 
705
1002
  describe : function(description, body) {
706
- this.suites.push(new JSpec.Suite(description, body))
707
- return this
1003
+ suite = new JSpec.Suite(description, body)
1004
+ this.allSuites.push(suite)
1005
+ this.suites.push(suite)
1006
+ },
1007
+
1008
+ /**
1009
+ * Return the contents of a function body.
1010
+ *
1011
+ * @param {function} body
1012
+ * @return {string}
1013
+ * @api public
1014
+ */
1015
+
1016
+ contentsOf : function(body) {
1017
+ return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
708
1018
  },
709
1019
 
710
1020
  /**
@@ -717,7 +1027,11 @@
717
1027
  */
718
1028
 
719
1029
  evalBody : function(body, errorMessage) {
720
- try { body.call(this.context || this.defaultContext) }
1030
+ dsl = this.DSL || this.DSLs.snake
1031
+ matchers = this.matchers
1032
+ context = this.context || this.defaultContext
1033
+ contents = this.contentsOf(body)
1034
+ try { eval('with (dsl){ with (context) { with (matchers) { ' + contents + ' }}}') }
721
1035
  catch(e) { error(errorMessage, e) }
722
1036
  },
723
1037
 
@@ -731,16 +1045,14 @@
731
1045
 
732
1046
  preprocess : function(input) {
733
1047
  return input.
734
- replace(/describe (.*?)$/m, 'JSpec.describe($1, function(){').
735
- replace(/describe (.*?)$/gm, 'this.describe($1, function(){').
736
- replace(/it (.*?)$/gm, 'this.it($1, function(){').
737
- replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'this.addHook("$1", function(){').
1048
+ replace(/describe (.*?)$/gm, 'describe($1, function(){').
1049
+ replace(/ it (.*?)$/gm, ' it($1, function(){').
1050
+ replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
738
1051
  replace(/end(?= |\n|$)/gm, '});').
739
1052
  replace(/-\{/g, 'function(){').
740
1053
  replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
741
- replace(/([\s\(]+)\./gm, '$1this.').
742
1054
  replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)').
743
- replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 expect($2).$3("$4", $5)').
1055
+ replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 expect($2).$3($4, $5)').
744
1056
  replace(/, \)/gm, ')').
745
1057
  replace(/should\.not/gm, 'should_not')
746
1058
  },
@@ -760,6 +1072,18 @@
760
1072
  else while (--current >= end) values.push(current)
761
1073
  return '[' + values + ']'
762
1074
  },
1075
+
1076
+ /**
1077
+ * Call _callback_ when all specs have finished.
1078
+ *
1079
+ * @param {function} callback
1080
+ * @api public
1081
+ */
1082
+
1083
+ whenFinished : function(callback) {
1084
+ if (this.stats.specsFinished >= this.stats.specs) callback()
1085
+ else setTimeout(function(){ JSpec.whenFinished(callback) }, 50)
1086
+ },
763
1087
 
764
1088
  /**
765
1089
  * Report on the results.
@@ -768,9 +1092,11 @@
768
1092
  */
769
1093
 
770
1094
  report : function() {
771
- this.options.formatter ?
772
- new this.options.formatter(this, this.options):
773
- new this.formatters.DOM(this, this.options)
1095
+ this.whenFinished(function() {
1096
+ JSpec.options.formatter ?
1097
+ new JSpec.options.formatter(JSpec, JSpec.options):
1098
+ new JSpec.formatters.DOM(JSpec, JSpec.options)
1099
+ })
774
1100
  },
775
1101
 
776
1102
  /**
@@ -785,35 +1111,51 @@
785
1111
  run : function(options) {
786
1112
  if (options) extend(this.options, options)
787
1113
  if (option('profile')) console.group('Profile')
788
- each(this.suites, function(suite) { this.runSuite(suite) })
1114
+ each(this.suites, function(suite) { JSpec.runSuite(suite) })
789
1115
  if (option('profile')) console.groupEnd()
790
1116
  return this
791
1117
  },
1118
+
1119
+ /**
1120
+ * When the current spec's wait duration has passed
1121
+ * the _callback_ will be called.
1122
+ *
1123
+ * @param {function} callback
1124
+ * @api public
1125
+ */
1126
+
1127
+ whenCurrentSpecIsFinished : function(callback) {
1128
+ if (this.currentSpec && this.currentSpec.wait)
1129
+ setTimeout(callback, this.currentSpec.wait)
1130
+ else callback()
1131
+ },
792
1132
 
793
1133
  /**
794
1134
  * Run a suite.
795
1135
  *
796
1136
  * @param {Suite} suite
797
- * @return {JSpec}
798
1137
  * @api public
799
1138
  */
800
1139
 
801
1140
  runSuite : function(suite) {
802
- suite.body()
1141
+ this.currentSuite = suite
1142
+ this.evalBody(suite.body)
803
1143
  suite.ran = true
804
1144
  suite.hook('before')
805
1145
  each(suite.specs, function(spec) {
806
- suite.hook('before_each')
807
- this.runSpec(spec)
808
- suite.hook('after_each')
1146
+ JSpec.whenCurrentSpecIsFinished(function(){
1147
+ suite.hook('before_each')
1148
+ JSpec.runSpec(spec)
1149
+ suite.hook('after_each')
1150
+ })
809
1151
  })
810
1152
  if (suite.hasSuites()) {
811
1153
  each(suite.suites, function(suite) {
812
- this.runSuite(suite)
1154
+ JSpec.runSuite(suite)
813
1155
  })
814
1156
  }
815
1157
  suite.hook('after')
816
- return this
1158
+ this.stats.suitesFinished++
817
1159
  },
818
1160
 
819
1161
  /**
@@ -837,11 +1179,12 @@
837
1179
 
838
1180
  runSpec : function(spec) {
839
1181
  this.currentSpec = spec
840
- this.stats.specs++
841
1182
  if (option('profile')) console.time(spec.description)
842
1183
  try { this.evalBody(spec.body) }
843
1184
  catch (e) { fail(e) }
1185
+ spec.runDeferredAssertions()
844
1186
  if (option('profile')) console.timeEnd(spec.description)
1187
+ this.stats.specsFinished++
845
1188
  this.stats.assertions += spec.assertions.length
846
1189
  },
847
1190
 
@@ -886,8 +1229,7 @@
886
1229
 
887
1230
  error : function(message, e) {
888
1231
  throw (message ? message : '') + e.toString() +
889
- (e.line ? ' near line ' + e.line : '') +
890
- (JSpec.file ? ' in ' + JSpec.file : '')
1232
+ (e.line ? ' near line ' + e.line : '')
891
1233
  },
892
1234
 
893
1235
  /**
@@ -912,8 +1254,10 @@
912
1254
  */
913
1255
 
914
1256
  reportToServer : function() {
915
- JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures)
916
- if ('close' in window) window.close()
1257
+ this.whenFinished(function(){
1258
+ JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures)
1259
+ if ('close' in main) main.close()
1260
+ })
917
1261
  },
918
1262
 
919
1263
  /**
@@ -924,7 +1268,7 @@
924
1268
  */
925
1269
 
926
1270
  xhr : function() {
927
- return window.ActiveXObject ?
1271
+ return 'ActiveXObject' in main ?
928
1272
  new ActiveXObject("Microsoft.XMLHTTP"):
929
1273
  new XMLHttpRequest()
930
1274
  },
@@ -949,7 +1293,6 @@
949
1293
  */
950
1294
 
951
1295
  load : function(file) {
952
- this.file = file
953
1296
  if (this.hasXhr()) {
954
1297
  request = this.xhr()
955
1298
  request.open('GET', file, false)
@@ -971,7 +1314,7 @@
971
1314
  */
972
1315
 
973
1316
  exec : function(file) {
974
- eval(this.preprocess(this.load(file)))
1317
+ eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
975
1318
  return this
976
1319
  }
977
1320
  }
@@ -979,15 +1322,17 @@
979
1322
  // --- Utility functions
980
1323
 
981
1324
  var main = this
982
- var puts = print
1325
+ var puts = main.print
983
1326
  var map = JSpec.map
984
1327
  var any = JSpec.any
1328
+ var find = JSpec.any
985
1329
  var last = JSpec.last
986
1330
  var fail = JSpec.fail
987
1331
  var range = JSpec.range
988
1332
  var each = JSpec.each
989
1333
  var option = JSpec.option
990
1334
  var inject = JSpec.inject
1335
+ var select = JSpec.select
991
1336
  var error = JSpec.error
992
1337
  var escape = JSpec.escape
993
1338
  var extend = JSpec.extend
@@ -996,8 +1341,11 @@
996
1341
  var query = JSpec.query
997
1342
  var strip = JSpec.strip
998
1343
  var color = JSpec.color
999
- var expect = JSpec.expect
1344
+ var does = JSpec.does
1000
1345
  var addMatchers = JSpec.addMatchers
1346
+ var callIterator = JSpec.callIterator
1347
+ var argumentsToArray = JSpec.argumentsToArray
1348
+ if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
1001
1349
 
1002
1350
  // --- Matchers
1003
1351
 
@@ -1028,6 +1376,12 @@
1028
1376
  hash(actual) == hash(expected):
1029
1377
  actual == expected
1030
1378
  },
1379
+
1380
+ receive : { defer : true, match : function(actual, method, times) {
1381
+ proxy = new JSpec.ProxyAssertion(actual, method, times)
1382
+ JSpec.currentSpec.assertions.push(proxy)
1383
+ return proxy
1384
+ }},
1031
1385
 
1032
1386
  include : function(actual) {
1033
1387
  for (state = true, i = 1; i < arguments.length; i++) {
@@ -1058,9 +1412,9 @@
1058
1412
  catch (e) {
1059
1413
  if (expected == undefined) return true
1060
1414
  switch (expected.constructor) {
1061
- case RegExp: return expected.test(e)
1062
- case Function: return e instanceof expected
1063
- case String: return expected == e.toString()
1415
+ case RegExp : return expected.test(e)
1416
+ case Function : return e instanceof expected
1417
+ case String : return expected == e.toString()
1064
1418
  }
1065
1419
  }
1066
1420
  },
@@ -1086,7 +1440,7 @@
1086
1440
  return actual[property] == null ||
1087
1441
  actual[property] instanceof Function ? false:
1088
1442
  value == null ? true:
1089
- JSpec.matchers.eql.match(actual[property], value)
1443
+ does(actual[property], 'eql', value)
1090
1444
  },
1091
1445
 
1092
1446
  have_property : function(actual, property, value) {
@@ -1101,4 +1455,4 @@
1101
1455
 
1102
1456
  this.JSpec = JSpec
1103
1457
 
1104
- })();
1458
+ })()