raus22-jspec 2.0.4

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