visionmedia-jspec 1.1.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+ })()