jspec 2.11.2

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.
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
+ })()