jspec 2.11.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/History.rdoc +522 -0
  2. data/Manifest +57 -0
  3. data/README.rdoc +825 -0
  4. data/Rakefile +75 -0
  5. data/bin/jspec +305 -0
  6. data/jspec.gemspec +44 -0
  7. data/lib/images/bg.png +0 -0
  8. data/lib/images/hr.png +0 -0
  9. data/lib/images/loading.gif +0 -0
  10. data/lib/images/sprites.bg.png +0 -0
  11. data/lib/images/sprites.png +0 -0
  12. data/lib/images/vr.png +0 -0
  13. data/lib/jspec.css +145 -0
  14. data/lib/jspec.jquery.js +71 -0
  15. data/lib/jspec.js +1771 -0
  16. data/lib/jspec.shell.js +36 -0
  17. data/lib/jspec.timers.js +90 -0
  18. data/lib/jspec.xhr.js +183 -0
  19. data/server/browsers.rb +228 -0
  20. data/server/helpers.rb +82 -0
  21. data/server/routes.rb +57 -0
  22. data/server/server.rb +88 -0
  23. data/spec/async +1 -0
  24. data/spec/env.js +695 -0
  25. data/spec/fixtures/test.html +1 -0
  26. data/spec/fixtures/test.json +1 -0
  27. data/spec/fixtures/test.xml +5 -0
  28. data/spec/helpers.js +66 -0
  29. data/spec/server.rb +2 -0
  30. data/spec/spec.dom.html +34 -0
  31. data/spec/spec.fixtures.js +18 -0
  32. data/spec/spec.grammar-less.js +34 -0
  33. data/spec/spec.grammar.js +226 -0
  34. data/spec/spec.jquery.js +176 -0
  35. data/spec/spec.jquery.xhr.js +65 -0
  36. data/spec/spec.js +166 -0
  37. data/spec/spec.matchers.js +493 -0
  38. data/spec/spec.modules.js +67 -0
  39. data/spec/spec.node.js +46 -0
  40. data/spec/spec.rhino.js +17 -0
  41. data/spec/spec.server.html +29 -0
  42. data/spec/spec.shared-behaviors.js +80 -0
  43. data/spec/spec.utils.js +279 -0
  44. data/spec/spec.xhr.js +156 -0
  45. data/templates/default/History.rdoc +4 -0
  46. data/templates/default/README.rdoc +29 -0
  47. data/templates/default/lib/yourlib.core.js +2 -0
  48. data/templates/default/spec/server.rb +4 -0
  49. data/templates/default/spec/spec.core.js +8 -0
  50. data/templates/default/spec/spec.dom.html +20 -0
  51. data/templates/default/spec/spec.rhino.js +8 -0
  52. data/templates/default/spec/spec.server.html +16 -0
  53. data/templates/rails/server.rb +4 -0
  54. data/templates/rails/spec.application.js +8 -0
  55. data/templates/rails/spec.dom.html +20 -0
  56. data/templates/rails/spec.rhino.js +8 -0
  57. data/templates/rails/spec.server.html +16 -0
  58. 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, '&amp;')
1002
+ .replace(/"/gmi, '&quot;')
1003
+ .replace(/>/gmi, '&gt;')
1004
+ .replace(/</gmi, '&lt;')
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
+ })()