client_side_validations 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1889 @@
1
+
2
+ // JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
3
+
4
+ ;(function(){
5
+
6
+ JSpec = {
7
+ version : '4.3.1',
8
+ assert : true,
9
+ cache : {},
10
+ suites : [],
11
+ modules : [],
12
+ allSuites : [],
13
+ sharedBehaviors: [],
14
+ matchers : {},
15
+ stubbed : [],
16
+ options : {},
17
+ request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
18
+ stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
19
+
20
+ /**
21
+ * Default context in which bodies are evaluated.
22
+ *
23
+ * Replace context simply by setting JSpec.context
24
+ * to your own like below:
25
+ *
26
+ * JSpec.context = { foo : 'bar' }
27
+ *
28
+ * Contexts can be changed within any body, this can be useful
29
+ * in order to provide specific helper methods to specific suites.
30
+ *
31
+ * To reset (usually in after hook) simply set to null like below:
32
+ *
33
+ * JSpec.context = null
34
+ *
35
+ */
36
+
37
+ defaultContext : {
38
+
39
+ /**
40
+ * Return an object used for proxy assertions.
41
+ * This object is used to indicate that an object
42
+ * should be an instance of _object_, not the constructor
43
+ * itself.
44
+ *
45
+ * @param {function} constructor
46
+ * @return {hash}
47
+ * @api public
48
+ */
49
+
50
+ an_instance_of : function(constructor) {
51
+ return { an_instance_of : constructor }
52
+ },
53
+
54
+ /**
55
+ * Load fixture at _path_.
56
+ *
57
+ * Fixtures are resolved as:
58
+ *
59
+ * - <path>
60
+ * - <path>.html
61
+ *
62
+ * @param {string} path
63
+ * @return {string}
64
+ * @api public
65
+ */
66
+
67
+ fixture : function(path) {
68
+ if (JSpec.cache[path]) return JSpec.cache[path]
69
+ return JSpec.cache[path] =
70
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) ||
71
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html')
72
+ },
73
+
74
+ /**
75
+ * Load json fixture at _path_.
76
+ *
77
+ * JSON fixtures are resolved as:
78
+ *
79
+ * - <path>
80
+ * - <path>.json
81
+ *
82
+ * @param {string} path
83
+ * @return {object}
84
+ * @api public
85
+ */
86
+
87
+ json_fixture: function(path) {
88
+ if (!JSpec.cache['json:' + path])
89
+ JSpec.cache['json:' + path] =
90
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) ||
91
+ JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.json')
92
+ try {
93
+ return eval('(' + JSpec.cache['json:' + path] + ')')
94
+ } catch (e) {
95
+ throw 'json_fixture("' + path + '"): ' + e
96
+ }
97
+ }
98
+ },
99
+
100
+ // --- Objects
101
+
102
+ reporters : {
103
+
104
+ /**
105
+ * Report to server.
106
+ *
107
+ * Options:
108
+ * - uri specific uri to report to.
109
+ * - verbose weither or not to output messages
110
+ * - failuresOnly output failure messages only
111
+ *
112
+ * @api public
113
+ */
114
+
115
+ Server : function(results, options) {
116
+ var uri = options.uri || 'http://' + window.location.host + '/results'
117
+ JSpec.post(uri, {
118
+ stats: JSpec.stats,
119
+ options: options,
120
+ results: map(results.allSuites, function(suite) {
121
+ if (suite.isExecutable())
122
+ return {
123
+ description: suite.description,
124
+ specs: map(suite.specs, function(spec) {
125
+ return {
126
+ description: spec.description,
127
+ message: !spec.passed() ? spec.failure().message : null,
128
+ status: spec.requiresImplementation() ? 'pending' :
129
+ spec.passed() ? 'pass' :
130
+ 'fail',
131
+ assertions: map(spec.assertions, function(assertion){
132
+ return {
133
+ passed: assertion.passed
134
+ }
135
+ })
136
+ }
137
+ })
138
+ }
139
+ })
140
+ })
141
+ if ('close' in main) main.close()
142
+ },
143
+
144
+ /**
145
+ * Default reporter, outputting to the DOM.
146
+ *
147
+ * Options:
148
+ * - reportToId id of element to output reports to, defaults to 'jspec'
149
+ * - failuresOnly displays only suites with failing specs
150
+ *
151
+ * @api public
152
+ */
153
+
154
+ DOM : function(results, options) {
155
+ var id = option('reportToId') || 'jspec',
156
+ report = document.getElementById(id),
157
+ failuresOnly = option('failuresOnly'),
158
+ classes = results.stats.failures ? 'has-failures' : ''
159
+ if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
160
+
161
+ function bodyContents(body) {
162
+ return JSpec.
163
+ escape(JSpec.contentsOf(body)).
164
+ replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
165
+ replace(/\r\n|\r|\n/gm, '<br/>')
166
+ }
167
+
168
+ report.innerHTML = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
169
+ <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
170
+ <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
171
+ <span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \
172
+ </div><table class="suites">' + map(results.allSuites, function(suite) {
173
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
174
+ if (displaySuite && suite.isExecutable())
175
+ return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
176
+ map(suite.specs, function(i, spec) {
177
+ return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
178
+ (spec.requiresImplementation() ?
179
+ '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
180
+ (spec.passed() && !failuresOnly) ?
181
+ '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
182
+ !spec.passed() ?
183
+ '<td class="fail">' + escape(spec.description) +
184
+ map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') +
185
+ '</td><td>' + spec.assertionsGraph() + '</td>' :
186
+ '') +
187
+ '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
188
+ }).join('') + '</tr>'
189
+ }).join('') + '</table></div>'
190
+ },
191
+
192
+ /**
193
+ * Terminal reporter.
194
+ *
195
+ * @api public
196
+ */
197
+
198
+ Terminal : function(results, options) {
199
+ var failuresOnly = option('failuresOnly')
200
+ print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
201
+ color(" Failures: ", 'bold') + color(results.stats.failures, 'red') +
202
+ color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n")
203
+
204
+ function indent(string) {
205
+ return string.replace(/^(.)/gm, ' $1')
206
+ }
207
+
208
+ each(results.allSuites, function(suite) {
209
+ var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
210
+ if (displaySuite && suite.isExecutable()) {
211
+ print(color(' ' + suite.description, 'bold'))
212
+ each(suite.specs, function(spec){
213
+ var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
214
+ return graph + color('.', assertion.passed ? 'green' : 'red')
215
+ })
216
+ if (spec.requiresImplementation())
217
+ print(color(' ' + spec.description, 'blue') + assertionsGraph)
218
+ else if (spec.passed() && !failuresOnly)
219
+ print(color(' ' + spec.description, 'green') + assertionsGraph)
220
+ else if (!spec.passed())
221
+ print(color(' ' + spec.description, 'red') + assertionsGraph +
222
+ "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n")
223
+ })
224
+ print("")
225
+ }
226
+ })
227
+
228
+ quit(results.stats.failures)
229
+ }
230
+ },
231
+
232
+ Assertion : function(matcher, actual, expected, negate) {
233
+ extend(this, {
234
+ message: '',
235
+ passed: false,
236
+ actual: actual,
237
+ negate: negate,
238
+ matcher: matcher,
239
+ expected: expected,
240
+
241
+ // Report assertion results
242
+
243
+ report : function() {
244
+ if (JSpec.assert)
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
+ old = object[method]
265
+
266
+ // Proxy
267
+
268
+ object[method] = function(){
269
+ var args = toArray(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 = toArray(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
+ !equal(self.expectedResult, 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
+ equal(self.expectedResult, 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
+ !equal(arg, 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
+ equal(arg, 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
+ if (JSpec.assert)
380
+ this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures
381
+ return this
382
+ },
383
+
384
+ // Run the assertion
385
+
386
+ run : function() {
387
+ var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
388
+
389
+ function times(n) {
390
+ return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n]
391
+ }
392
+
393
+ if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
394
+ this.message = methodString + ' to return ' + puts(this.expectedResult) +
395
+ ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
396
+
397
+ if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
398
+ this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
399
+ ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
400
+
401
+ if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times)
402
+ this.message = methodString + ' to be called ' + times(this.times) +
403
+ ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
404
+
405
+ if (!this.message.length)
406
+ this.passed = true
407
+
408
+ return this
409
+ }
410
+ })
411
+ },
412
+
413
+ /**
414
+ * Specification Suite block object.
415
+ *
416
+ * @param {string} description
417
+ * @param {function} body
418
+ * @api private
419
+ */
420
+
421
+ Suite : function(description, body, isShared) {
422
+ var self = this
423
+ extend(this, {
424
+ body: body,
425
+ description: description,
426
+ suites: [],
427
+ sharedBehaviors: [],
428
+ specs: [],
429
+ ran: false,
430
+ shared: isShared,
431
+ hooks: { 'before' : [], 'after' : [],
432
+ 'before_each' : [], 'after_each' : [],
433
+ 'before_nested' : [], 'after_nested' : []},
434
+
435
+ // Add a spec to the suite
436
+
437
+ addSpec : function(description, body) {
438
+ var spec = new JSpec.Spec(description, body)
439
+ this.specs.push(spec)
440
+ JSpec.stats.specs++ // TODO: abstract
441
+ spec.suite = this
442
+ },
443
+
444
+ // Add a before hook to the suite
445
+
446
+ addBefore : function(options, body) {
447
+ body.options = options || {}
448
+ this.befores.push(body)
449
+ },
450
+
451
+ // Add an after hook to the suite
452
+
453
+ addAfter : function(options, body) {
454
+ body.options = options || {}
455
+ this.afters.unshift(body)
456
+ },
457
+
458
+ // Add a hook to the suite
459
+
460
+ addHook : function(hook, body) {
461
+ this.hooks[hook].push(body)
462
+ },
463
+
464
+ // Add a nested suite
465
+
466
+ addSuite : function(description, body, isShared) {
467
+ var suite = new JSpec.Suite(description, body, isShared)
468
+ JSpec.allSuites.push(suite)
469
+ suite.name = suite.description
470
+ suite.description = this.description + ' ' + suite.description
471
+ this.suites.push(suite)
472
+ suite.suite = this
473
+ },
474
+
475
+ // Invoke a hook in context to this suite
476
+
477
+ hook : function(hook) {
478
+ if (hook != 'before' && hook != 'after')
479
+ if (this.suite) this.suite.hook(hook)
480
+
481
+ each(this.hooks[hook], function(body) {
482
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
483
+ })
484
+ },
485
+
486
+ // Check if nested suites are present
487
+
488
+ hasSuites : function() {
489
+ return this.suites.length
490
+ },
491
+
492
+ // Check if this suite has specs
493
+
494
+ hasSpecs : function() {
495
+ return this.specs.length
496
+ },
497
+
498
+ // Check if the entire suite passed
499
+
500
+ passed : function() {
501
+ return !any(this.specs, function(spec){
502
+ return !spec.passed()
503
+ })
504
+ },
505
+
506
+ isShared : function(){
507
+ return this.shared
508
+ },
509
+
510
+ isExecutable : function() {
511
+ return !this.isShared() && this.hasSpecs()
512
+ }
513
+ })
514
+ },
515
+
516
+ /**
517
+ * Specification block object.
518
+ *
519
+ * @param {string} description
520
+ * @param {function} body
521
+ * @api private
522
+ */
523
+
524
+ Spec : function(description, body) {
525
+ extend(this, {
526
+ body: body,
527
+ description: description,
528
+ assertions: [],
529
+
530
+ // Add passing assertion
531
+
532
+ pass : function(message) {
533
+ this.assertions.push({ passed: true, message: message })
534
+ if (JSpec.assert) ++JSpec.stats.passes
535
+ },
536
+
537
+ // Add failing assertion
538
+
539
+ fail : function(message) {
540
+ this.assertions.push({ passed: false, message: message })
541
+ if (JSpec.assert) ++JSpec.stats.failures
542
+ },
543
+
544
+ // Run deferred assertions
545
+
546
+ runDeferredAssertions : function() {
547
+ each(this.assertions, function(assertion){
548
+ if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
549
+ })
550
+ },
551
+
552
+ // Find first failing assertion
553
+
554
+ failure : function() {
555
+ return find(this.assertions, function(assertion){
556
+ return !assertion.passed
557
+ })
558
+ },
559
+
560
+ // Find all failing assertions
561
+
562
+ failures : function() {
563
+ return select(this.assertions, function(assertion){
564
+ return !assertion.passed
565
+ })
566
+ },
567
+
568
+ // Weither or not the spec passed
569
+
570
+ passed : function() {
571
+ return !this.failure()
572
+ },
573
+
574
+ // Weither or not the spec requires implementation (no assertions)
575
+
576
+ requiresImplementation : function() {
577
+ return this.assertions.length == 0
578
+ },
579
+
580
+ // Sprite based assertions graph
581
+
582
+ assertionsGraph : function() {
583
+ return map(this.assertions, function(assertion){
584
+ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
585
+ }).join('')
586
+ }
587
+ })
588
+ },
589
+
590
+ Module : function(methods) {
591
+ extend(this, methods)
592
+ },
593
+
594
+ JSON : {
595
+
596
+ /**
597
+ * Generic sequences.
598
+ */
599
+
600
+ meta : {
601
+ '\b' : '\\b',
602
+ '\t' : '\\t',
603
+ '\n' : '\\n',
604
+ '\f' : '\\f',
605
+ '\r' : '\\r',
606
+ '"' : '\\"',
607
+ '\\' : '\\\\'
608
+ },
609
+
610
+ /**
611
+ * Escapable sequences.
612
+ */
613
+
614
+ escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
615
+
616
+ /**
617
+ * JSON encode _object_.
618
+ *
619
+ * @param {mixed} object
620
+ * @return {string}
621
+ * @api private
622
+ */
623
+
624
+ encode : function(object) {
625
+ var self = this
626
+ if (object == undefined || object == null) return 'null'
627
+ if (object === true) return 'true'
628
+ if (object === false) return 'false'
629
+ switch (typeof object) {
630
+ case 'number': return object
631
+ case 'string': return this.escapable.test(object) ?
632
+ '"' + object.replace(this.escapable, function (a) {
633
+ return typeof self.meta[a] === 'string' ? self.meta[a] :
634
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
635
+ }) + '"' :
636
+ '"' + object + '"'
637
+ case 'object':
638
+ if (object.constructor == Array)
639
+ return '[' + map(object, function(val){
640
+ return self.encode(val)
641
+ }).join(', ') + ']'
642
+ else if (object)
643
+ return '{' + map(object, function(key, val){
644
+ return self.encode(key) + ':' + self.encode(val)
645
+ }).join(', ') + '}'
646
+ }
647
+ return 'null'
648
+ }
649
+ },
650
+
651
+ // --- DSLs
652
+
653
+ DSLs : {
654
+ snake : {
655
+ expect : function(actual){
656
+ return JSpec.expect(actual)
657
+ },
658
+
659
+ describe : function(description, body) {
660
+ return JSpec.currentSuite.addSuite(description, body, false)
661
+ },
662
+
663
+ it : function(description, body) {
664
+ return JSpec.currentSuite.addSpec(description, body)
665
+ },
666
+
667
+ before : function(body) {
668
+ return JSpec.currentSuite.addHook('before', body)
669
+ },
670
+
671
+ after : function(body) {
672
+ return JSpec.currentSuite.addHook('after', body)
673
+ },
674
+
675
+ before_each : function(body) {
676
+ return JSpec.currentSuite.addHook('before_each', body)
677
+ },
678
+
679
+ after_each : function(body) {
680
+ return JSpec.currentSuite.addHook('after_each', body)
681
+ },
682
+
683
+ before_nested : function(body) {
684
+ return JSpec.currentSuite.addHook('before_nested', body)
685
+ },
686
+
687
+ after_nested : function(body){
688
+ return JSpec.currentSuite.addhook('after_nested', body)
689
+ },
690
+
691
+ shared_behaviors_for : function(description, body){
692
+ return JSpec.currentSuite.addSuite(description, body, true)
693
+ },
694
+
695
+ should_behave_like : function(description) {
696
+ return JSpec.shareBehaviorsOf(description)
697
+ }
698
+ }
699
+ },
700
+
701
+ // --- Methods
702
+
703
+ /**
704
+ * Check if _value_ is 'stop'. For use as a
705
+ * utility callback function.
706
+ *
707
+ * @param {mixed} value
708
+ * @return {bool}
709
+ * @api public
710
+ */
711
+
712
+ haveStopped : function(value) {
713
+ return value === 'stop'
714
+ },
715
+
716
+ /**
717
+ * Include _object_ which may be a hash or Module instance.
718
+ *
719
+ * @param {hash, Module} object
720
+ * @return {JSpec}
721
+ * @api public
722
+ */
723
+
724
+ include : function(object) {
725
+ var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
726
+ this.modules.push(module)
727
+ if ('init' in module) module.init()
728
+ if ('utilities' in module) extend(this.defaultContext, module.utilities)
729
+ if ('matchers' in module) this.addMatchers(module.matchers)
730
+ if ('reporters' in module) extend(this.reporters, module.reporters)
731
+ if ('DSLs' in module)
732
+ each(module.DSLs, function(name, methods){
733
+ JSpec.DSLs[name] = JSpec.DSLs[name] || {}
734
+ extend(JSpec.DSLs[name], methods)
735
+ })
736
+ return this
737
+ },
738
+
739
+ /**
740
+ * Add a module hook _name_, which is immediately
741
+ * called per module with the _args_ given. An array of
742
+ * hook return values is returned.
743
+ *
744
+ * @param {name} string
745
+ * @param {...} args
746
+ * @return {array}
747
+ * @api private
748
+ */
749
+
750
+ hook : function(name, args) {
751
+ args = toArray(arguments, 1)
752
+ return inject(JSpec.modules, [], function(results, module){
753
+ if (typeof module[name] == 'function')
754
+ results.push(JSpec.evalHook(module, name, args))
755
+ })
756
+ },
757
+
758
+ /**
759
+ * Eval _module_ hook _name_ with _args_. Evaluates in context
760
+ * to the module itself, JSpec, and JSpec.context.
761
+ *
762
+ * @param {Module} module
763
+ * @param {string} name
764
+ * @param {array} args
765
+ * @return {mixed}
766
+ * @api private
767
+ */
768
+
769
+ evalHook : function(module, name, args) {
770
+ hook('evaluatingHookBody', module, name)
771
+ return module[name].apply(module, args)
772
+ },
773
+
774
+ /**
775
+ * Same as hook() however accepts only one _arg_ which is
776
+ * considered immutable. This function passes the arg
777
+ * to the first module, then passes the return value of the last
778
+ * module called, to the following module.
779
+ *
780
+ * @param {string} name
781
+ * @param {mixed} arg
782
+ * @return {mixed}
783
+ * @api private
784
+ */
785
+
786
+ hookImmutable : function(name, arg) {
787
+ return inject(JSpec.modules, arg, function(result, module){
788
+ if (typeof module[name] == 'function')
789
+ return JSpec.evalHook(module, name, [result])
790
+ })
791
+ },
792
+
793
+ /**
794
+ * Find a shared example suite by its description or name.
795
+ * First searches parent tree of suites for shared behavior
796
+ * before falling back to global scoped nested behaviors.
797
+ *
798
+ * @param {string} description
799
+ * @return {Suite}
800
+ * @api private
801
+ */
802
+
803
+ findSharedBehavior : function(description) {
804
+ var behavior
805
+ return (behavior = JSpec.findLocalSharedBehavior(description))
806
+ ? behavior
807
+ : JSpec.findGlobalSharedBehavior(description)
808
+ },
809
+
810
+ /**
811
+ * Find a shared example suite within the current suite's
812
+ * parent tree by its description or name.
813
+ *
814
+ * @param {string} description
815
+ * @return {Suite}
816
+ * @api private
817
+ */
818
+
819
+ findLocalSharedBehavior : function(description) {
820
+ var behavior,
821
+ currentSuite = JSpec.currentSuite.suite
822
+ while (currentSuite)
823
+ if (behavior = find(currentSuite.suites, JSpec.suiteDescriptionPredicate(description)))
824
+ return behavior
825
+ else
826
+ currentSuite = currentSuite.suite
827
+ },
828
+
829
+ /**
830
+ * Find a shared example suite within the global
831
+ * scope by its description or name.
832
+ *
833
+ * @param {string} description
834
+ * @return {Suite}
835
+ * @api private
836
+ */
837
+
838
+ findGlobalSharedBehavior : function(description) {
839
+ return find(JSpec.suites, JSpec.suiteDescriptionPredicate(description))
840
+ },
841
+
842
+ /**
843
+ * Build a predicate that will match a suite based on name or description
844
+ *
845
+ * @param {string} description
846
+ * @return {function}
847
+ * @api private
848
+ */
849
+
850
+ suiteDescriptionPredicate : function(description) {
851
+ return function(suite){
852
+ return suite.name === description ||
853
+ suite.description === description
854
+ }
855
+ },
856
+
857
+ /**
858
+ * Share behaviors (specs) of the given suite with
859
+ * the current suite.
860
+ *
861
+ * @param {string} description
862
+ * @api public
863
+ */
864
+
865
+ shareBehaviorsOf : function(description) {
866
+ var suite = JSpec.findSharedBehavior(description)
867
+ if (suite)
868
+ JSpec.evalBody(suite.body)
869
+ else
870
+ throw new Error("failed to find shared behaviors named `" + description + "'")
871
+ },
872
+
873
+
874
+ /**
875
+ * Convert arguments to an array.
876
+ *
877
+ * @param {object} arguments
878
+ * @param {int} offset
879
+ * @return {array}
880
+ * @api public
881
+ */
882
+
883
+ toArray : function(arguments, offset) {
884
+ return Array.prototype.slice.call(arguments, offset || 0)
885
+ },
886
+
887
+ /**
888
+ * Return ANSI-escaped colored string.
889
+ *
890
+ * @param {string} string
891
+ * @param {string} color
892
+ * @return {string}
893
+ * @api public
894
+ */
895
+
896
+ color : function(string, color) {
897
+ if (option('disableColors')) {
898
+ return string
899
+ } else {
900
+ return "\u001B[" + {
901
+ bold : 1,
902
+ black : 30,
903
+ red : 31,
904
+ green : 32,
905
+ yellow : 33,
906
+ blue : 34,
907
+ magenta : 35,
908
+ cyan : 36,
909
+ white : 37
910
+ }[color] + 'm' + string + "\u001B[0m"
911
+ }
912
+ },
913
+
914
+ /**
915
+ * Default matcher message callback.
916
+ *
917
+ * @api private
918
+ */
919
+
920
+ defaultMatcherMessage : function(actual, expected, negate, name) {
921
+ return 'expected ' + puts(actual) + ' to ' +
922
+ (negate ? 'not ' : '') +
923
+ name.replace(/_/g, ' ') +
924
+ ' ' + (expected.length > 1 ?
925
+ puts.apply(this, expected.slice(1)) :
926
+ '')
927
+ },
928
+
929
+ /**
930
+ * Normalize a matcher message.
931
+ *
932
+ * When no messge callback is present the defaultMatcherMessage
933
+ * will be assigned, will suffice for most matchers.
934
+ *
935
+ * @param {hash} matcher
936
+ * @return {hash}
937
+ * @api public
938
+ */
939
+
940
+ normalizeMatcherMessage : function(matcher) {
941
+ if (typeof matcher.message != 'function')
942
+ matcher.message = this.defaultMatcherMessage
943
+ return matcher
944
+ },
945
+
946
+ /**
947
+ * Normalize a matcher body
948
+ *
949
+ * This process allows the following conversions until
950
+ * the matcher is in its final normalized hash state.
951
+ *
952
+ * - '==' becomes 'actual == expected'
953
+ * - 'actual == expected' becomes 'return actual == expected'
954
+ * - function(actual, expected) { return actual == expected } becomes
955
+ * { match : function(actual, expected) { return actual == expected }}
956
+ *
957
+ * @param {mixed} body
958
+ * @return {hash}
959
+ * @api public
960
+ */
961
+
962
+ normalizeMatcherBody : function(body) {
963
+ var captures
964
+ switch (body.constructor) {
965
+ case String:
966
+ if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
967
+ if (body.length < 4) body = 'actual ' + body + ' expected'
968
+ return { match: function(actual, expected) { return eval(body) }}
969
+
970
+ case Function:
971
+ return { match: body }
972
+
973
+ default:
974
+ return body
975
+ }
976
+ },
977
+
978
+ /**
979
+ * Get option value. This method first checks if
980
+ * the option key has been set via the query string,
981
+ * otherwise returning the options hash value.
982
+ *
983
+ * @param {string} key
984
+ * @return {mixed}
985
+ * @api public
986
+ */
987
+
988
+ option : function(key) {
989
+ return (value = query(key)) !== null ? value :
990
+ JSpec.options[key] || null
991
+ },
992
+
993
+ /**
994
+ * Check if object _a_, is equal to object _b_.
995
+ *
996
+ * @param {object} a
997
+ * @param {object} b
998
+ * @return {bool}
999
+ * @api private
1000
+ */
1001
+
1002
+ equal: function(a, b) {
1003
+ if (typeof a != typeof b) return
1004
+ if (a === b) return true
1005
+ if (a instanceof RegExp)
1006
+ return a.toString() === b.toString()
1007
+ if (a instanceof Date)
1008
+ return Number(a) === Number(b)
1009
+ if (typeof a != 'object') return
1010
+ if (a.length !== undefined)
1011
+ if (a.length !== b.length) return
1012
+ else
1013
+ for (var i = 0, len = a.length; i < len; ++i)
1014
+ if (!equal(a[i], b[i]))
1015
+ return
1016
+ for (var key in a)
1017
+ if (!equal(a[key], b[key]))
1018
+ return
1019
+ return true
1020
+ },
1021
+
1022
+ /**
1023
+ * Return last element of an array.
1024
+ *
1025
+ * @param {array} array
1026
+ * @return {object}
1027
+ * @api public
1028
+ */
1029
+
1030
+ last : function(array) {
1031
+ return array[array.length - 1]
1032
+ },
1033
+
1034
+ /**
1035
+ * Convert object(s) to a print-friend string.
1036
+ *
1037
+ * @param {...} object
1038
+ * @return {string}
1039
+ * @api public
1040
+ */
1041
+
1042
+ puts : function(object) {
1043
+ if (arguments.length > 1)
1044
+ return map(toArray(arguments), function(arg){
1045
+ return puts(arg)
1046
+ }).join(', ')
1047
+ if (object === undefined) return 'undefined'
1048
+ if (object === null) return 'null'
1049
+ if (object === true) return 'true'
1050
+ if (object === false) return 'false'
1051
+ if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
1052
+ if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector)
1053
+ if (object.jquery) return object.get(0).outerHTML
1054
+ if (object.nodeName) return object.outerHTML
1055
+ switch (object.constructor) {
1056
+ case Function: return object.name || object
1057
+ case String:
1058
+ return '"' + object
1059
+ .replace(/"/g, '\\"')
1060
+ .replace(/\n/g, '\\n')
1061
+ .replace(/\t/g, '\\t')
1062
+ + '"'
1063
+ case Array:
1064
+ return inject(object, '[', function(b, v){
1065
+ return b + ', ' + puts(v)
1066
+ }).replace('[,', '[') + ' ]'
1067
+ case Object:
1068
+ object.__hit__ = true
1069
+ return inject(object, '{', function(b, k, v) {
1070
+ if (k == '__hit__') return b
1071
+ return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v))
1072
+ }).replace('{,', '{') + ' }'
1073
+ default:
1074
+ return object.toString()
1075
+ }
1076
+ },
1077
+
1078
+ /**
1079
+ * Parse an XML String and return a 'document'.
1080
+ *
1081
+ * @param {string} text
1082
+ * @return {document}
1083
+ * @api public
1084
+ */
1085
+
1086
+ parseXML : function(text) {
1087
+ var xmlDoc
1088
+ if (window.DOMParser)
1089
+ xmlDoc = (new DOMParser()).parseFromString(text, "text/xml")
1090
+ else {
1091
+ xmlDoc = new ActiveXObject("Microsoft.XMLDOM")
1092
+ xmlDoc.async = "false"
1093
+ xmlDoc.loadXML(text)
1094
+ }
1095
+ return xmlDoc
1096
+ },
1097
+
1098
+ /**
1099
+ * Escape HTML.
1100
+ *
1101
+ * @param {string} html
1102
+ * @return {string}
1103
+ * @api public
1104
+ */
1105
+
1106
+ escape : function(html) {
1107
+ return html.toString()
1108
+ .replace(/&/gmi, '&amp;')
1109
+ .replace(/"/gmi, '&quot;')
1110
+ .replace(/>/gmi, '&gt;')
1111
+ .replace(/</gmi, '&lt;')
1112
+ },
1113
+
1114
+ /**
1115
+ * Perform an assertion without reporting.
1116
+ *
1117
+ * This method is primarily used for internal
1118
+ * matchers in order retain DRYness. May be invoked
1119
+ * like below:
1120
+ *
1121
+ * does('foo', 'eql', 'foo')
1122
+ * does([1,2], 'include', 1, 2)
1123
+ *
1124
+ * External hooks are not run for internal assertions
1125
+ * performed by does().
1126
+ *
1127
+ * @param {mixed} actual
1128
+ * @param {string} matcher
1129
+ * @param {...} expected
1130
+ * @return {mixed}
1131
+ * @api private
1132
+ */
1133
+
1134
+ does : function(actual, matcher, expected) {
1135
+ var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, toArray(arguments, 2))
1136
+ return assertion.run().result
1137
+ },
1138
+
1139
+ /**
1140
+ * Perform an assertion.
1141
+ *
1142
+ * expect(true).to('be', true)
1143
+ * expect('foo').not_to('include', 'bar')
1144
+ * expect([1, [2]]).to('include', 1, [2])
1145
+ *
1146
+ * @param {mixed} actual
1147
+ * @return {hash}
1148
+ * @api public
1149
+ */
1150
+
1151
+ expect : function(actual) {
1152
+ function assert(matcher, args, negate) {
1153
+ var expected = toArray(args, 1)
1154
+ matcher.negate = negate
1155
+ var assertion = new JSpec.Assertion(matcher, actual, expected, negate)
1156
+ hook('beforeAssertion', assertion)
1157
+ if (matcher.defer) assertion.run()
1158
+ else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
1159
+ return assertion.result
1160
+ }
1161
+
1162
+ function to(matcher) {
1163
+ return assert(matcher, arguments, false)
1164
+ }
1165
+
1166
+ function not_to(matcher) {
1167
+ return assert(matcher, arguments, true)
1168
+ }
1169
+
1170
+ return {
1171
+ to : to,
1172
+ should : to,
1173
+ not_to: not_to,
1174
+ should_not : not_to
1175
+ }
1176
+ },
1177
+
1178
+ /**
1179
+ * Strim whitespace or chars.
1180
+ *
1181
+ * @param {string} string
1182
+ * @param {string} chars
1183
+ * @return {string}
1184
+ * @api public
1185
+ */
1186
+
1187
+ strip : function(string, chars) {
1188
+ return string.
1189
+ replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
1190
+ replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
1191
+ },
1192
+
1193
+ /**
1194
+ * Call an iterator callback with arguments a, or b
1195
+ * depending on the arity of the callback.
1196
+ *
1197
+ * @param {function} callback
1198
+ * @param {mixed} a
1199
+ * @param {mixed} b
1200
+ * @return {mixed}
1201
+ * @api private
1202
+ */
1203
+
1204
+ callIterator : function(callback, a, b) {
1205
+ return callback.length == 1 ? callback(b) : callback(a, b)
1206
+ },
1207
+
1208
+ /**
1209
+ * Extend an object with another.
1210
+ *
1211
+ * @param {object} object
1212
+ * @param {object} other
1213
+ * @api public
1214
+ */
1215
+
1216
+ extend : function(object, other) {
1217
+ each(other, function(property, value){
1218
+ object[property] = value
1219
+ })
1220
+ },
1221
+
1222
+ /**
1223
+ * Iterate an object, invoking the given callback.
1224
+ *
1225
+ * @param {hash, array} object
1226
+ * @param {function} callback
1227
+ * @return {JSpec}
1228
+ * @api public
1229
+ */
1230
+
1231
+ each : function(object, callback) {
1232
+ if (object.constructor == Array)
1233
+ for (var i = 0, len = object.length; i < len; ++i)
1234
+ callIterator(callback, i, object[i])
1235
+ else
1236
+ for (var key in object)
1237
+ if (object.hasOwnProperty(key))
1238
+ callIterator(callback, key, object[key])
1239
+ },
1240
+
1241
+ /**
1242
+ * Iterate with memo.
1243
+ *
1244
+ * @param {hash, array} object
1245
+ * @param {object} memo
1246
+ * @param {function} callback
1247
+ * @return {object}
1248
+ * @api public
1249
+ */
1250
+
1251
+ inject : function(object, memo, callback) {
1252
+ each(object, function(key, value){
1253
+ memo = (callback.length == 2 ?
1254
+ callback(memo, value):
1255
+ callback(memo, key, value)) ||
1256
+ memo
1257
+ })
1258
+ return memo
1259
+ },
1260
+
1261
+ /**
1262
+ * Destub _object_'s _method_. When no _method_ is passed
1263
+ * all stubbed methods are destubbed. When no arguments
1264
+ * are passed every object found in JSpec.stubbed will be
1265
+ * destubbed.
1266
+ *
1267
+ * @param {mixed} object
1268
+ * @param {string} method
1269
+ * @api public
1270
+ */
1271
+
1272
+ destub : function(object, method) {
1273
+ var captures
1274
+ if (method) {
1275
+ if (object['__prototype__' + method])
1276
+ delete object[method]
1277
+ else
1278
+ object[method] = object['__original__' + method]
1279
+ delete object['__prototype__' + method]
1280
+ delete object['__original____' + method]
1281
+ }
1282
+ else if (object) {
1283
+ for (var key in object)
1284
+ if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
1285
+ destub(object, captures[1])
1286
+ }
1287
+ else
1288
+ while (JSpec.stubbed.length)
1289
+ destub(JSpec.stubbed.shift())
1290
+ },
1291
+
1292
+ /**
1293
+ * Stub _object_'s _method_.
1294
+ *
1295
+ * stub(foo, 'toString').and_return('bar')
1296
+ *
1297
+ * @param {mixed} object
1298
+ * @param {string} method
1299
+ * @return {hash}
1300
+ * @api public
1301
+ */
1302
+
1303
+ stub : function(object, method) {
1304
+ hook('stubbing', object, method)
1305
+ JSpec.stubbed.push(object)
1306
+ var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
1307
+ object[type + method] = object[method]
1308
+ object[method] = function(){}
1309
+ return {
1310
+ and_return : function(value) {
1311
+ if (typeof value == 'function') object[method] = value
1312
+ else object[method] = function(){ return value }
1313
+ }
1314
+ }
1315
+ },
1316
+
1317
+ /**
1318
+ * Map callback return values.
1319
+ *
1320
+ * @param {hash, array} object
1321
+ * @param {function} callback
1322
+ * @return {array}
1323
+ * @api public
1324
+ */
1325
+
1326
+ map : function(object, callback) {
1327
+ return inject(object, [], function(memo, key, value){
1328
+ memo.push(callIterator(callback, key, value))
1329
+ })
1330
+ },
1331
+
1332
+ /**
1333
+ * Returns the first matching expression or null.
1334
+ *
1335
+ * @param {hash, array} object
1336
+ * @param {function} callback
1337
+ * @return {mixed}
1338
+ * @api public
1339
+ */
1340
+
1341
+ any : function(object, callback) {
1342
+ return inject(object, null, function(state, key, value){
1343
+ if (state == undefined)
1344
+ return callIterator(callback, key, value) ? value : state
1345
+ })
1346
+ },
1347
+
1348
+ /**
1349
+ * Returns an array of values collected when the callback
1350
+ * given evaluates to true.
1351
+ *
1352
+ * @param {hash, array} object
1353
+ * @return {function} callback
1354
+ * @return {array}
1355
+ * @api public
1356
+ */
1357
+
1358
+ select : function(object, callback) {
1359
+ return inject(object, [], function(selected, key, value){
1360
+ if (callIterator(callback, key, value))
1361
+ selected.push(value)
1362
+ })
1363
+ },
1364
+
1365
+ /**
1366
+ * Define matchers.
1367
+ *
1368
+ * @param {hash} matchers
1369
+ * @api public
1370
+ */
1371
+
1372
+ addMatchers : function(matchers) {
1373
+ each(matchers, function(name, body){
1374
+ JSpec.addMatcher(name, body)
1375
+ })
1376
+ },
1377
+
1378
+ /**
1379
+ * Define a matcher.
1380
+ *
1381
+ * @param {string} name
1382
+ * @param {hash, function, string} body
1383
+ * @api public
1384
+ */
1385
+
1386
+ addMatcher : function(name, body) {
1387
+ hook('addingMatcher', name, body)
1388
+ if (name.indexOf(' ') != -1) {
1389
+ var matchers = name.split(/\s+/)
1390
+ var prefix = matchers.shift()
1391
+ each(matchers, function(name) {
1392
+ JSpec.addMatcher(prefix + '_' + name, body(name))
1393
+ })
1394
+ }
1395
+ this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
1396
+ this.matchers[name].name = name
1397
+ },
1398
+
1399
+ /**
1400
+ * Add a root suite to JSpec.
1401
+ *
1402
+ * @param {string} description
1403
+ * @param {body} function
1404
+ * @api public
1405
+ */
1406
+
1407
+ describe : function(description, body) {
1408
+ var suite = new JSpec.Suite(description, body, false)
1409
+ hook('addingSuite', suite)
1410
+ this.allSuites.push(suite)
1411
+ this.suites.push(suite)
1412
+ },
1413
+
1414
+ /**
1415
+ * Add a shared example suite to JSpec.
1416
+ *
1417
+ * @param {string} description
1418
+ * @param {body} function
1419
+ * @api public
1420
+ */
1421
+
1422
+ shared_behaviors_for : function(description, body) {
1423
+ var suite = new JSpec.Suite(description, body, true)
1424
+ hook('addingSuite', suite)
1425
+ this.allSuites.push(suite)
1426
+ this.suites.push(suite)
1427
+ },
1428
+
1429
+ /**
1430
+ * Return the contents of a function body.
1431
+ *
1432
+ * @param {function} body
1433
+ * @return {string}
1434
+ * @api public
1435
+ */
1436
+
1437
+ contentsOf : function(body) {
1438
+ return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
1439
+ },
1440
+
1441
+ /**
1442
+ * Evaluate a JSpec capture body.
1443
+ *
1444
+ * @param {function} body
1445
+ * @param {string} errorMessage (optional)
1446
+ * @return {Type}
1447
+ * @api private
1448
+ */
1449
+
1450
+ evalBody : function(body, errorMessage) {
1451
+ var dsl = this.DSL || this.DSLs.snake
1452
+ var matchers = this.matchers
1453
+ var context = this.context || this.defaultContext
1454
+ var contents = this.contentsOf(body)
1455
+ hook('evaluatingBody', dsl, matchers, context, contents)
1456
+ with (dsl){ with (context) { with (matchers) { eval(contents) }}}
1457
+ },
1458
+
1459
+ /**
1460
+ * Pre-process a string of JSpec.
1461
+ *
1462
+ * @param {string} input
1463
+ * @return {string}
1464
+ * @api private
1465
+ */
1466
+
1467
+ preprocess : function(input) {
1468
+ if (typeof input != 'string') return
1469
+ input = hookImmutable('preprocessing', input)
1470
+ return input.
1471
+ replace(/\t/g, ' ').
1472
+ replace(/\r\n|\n|\r/g, '\n').
1473
+ split('__END__')[0].
1474
+ replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
1475
+ replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
1476
+ replace(/shared_behaviors_for\s+(.*?)$/gm, 'shared_behaviors_for($1, function(){').
1477
+ replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
1478
+ replace(/^ *(before_nested|after_nested|before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
1479
+ replace(/^\s*end(?=\s|$)/gm, '});').
1480
+ replace(/-\{/g, 'function(){').
1481
+ replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
1482
+ replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
1483
+ replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
1484
+ replace(/, \)/g, ')').
1485
+ replace(/should\.not/g, 'should_not')
1486
+ },
1487
+
1488
+ /**
1489
+ * Create a range string which can be evaluated to a native array.
1490
+ *
1491
+ * @param {int} start
1492
+ * @param {int} end
1493
+ * @return {string}
1494
+ * @api public
1495
+ */
1496
+
1497
+ range : function(start, end) {
1498
+ var current = parseInt(start), end = parseInt(end), values = [current]
1499
+ if (end > current) while (++current <= end) values.push(current)
1500
+ else while (--current >= end) values.push(current)
1501
+ return '[' + values + ']'
1502
+ },
1503
+
1504
+ /**
1505
+ * Report on the results.
1506
+ *
1507
+ * @api public
1508
+ */
1509
+
1510
+ report : function() {
1511
+ this.duration = Number(new Date) - this.start
1512
+ hook('reporting', JSpec.options)
1513
+ new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options)
1514
+ },
1515
+
1516
+ /**
1517
+ * Run the spec suites. Options are merged
1518
+ * with JSpec options when present.
1519
+ *
1520
+ * @param {hash} options
1521
+ * @return {JSpec}
1522
+ * @api public
1523
+ */
1524
+
1525
+ run : function(options) {
1526
+ if (any(hook('running'), haveStopped)) return this
1527
+ if (options) extend(this.options, options)
1528
+ this.start = Number(new Date)
1529
+ each(this.suites, function(suite) { JSpec.runSuite(suite) })
1530
+ return this
1531
+ },
1532
+
1533
+ /**
1534
+ * Run a suite.
1535
+ *
1536
+ * @param {Suite} suite
1537
+ * @api public
1538
+ */
1539
+
1540
+ runSuite : function(suite) {
1541
+ if (!suite.isShared())
1542
+ {
1543
+ this.currentSuite = suite
1544
+ this.evalBody(suite.body)
1545
+ suite.ran = true
1546
+ hook('beforeSuite', suite), suite.hook('before'), suite.hook('before_nested')
1547
+ each(suite.specs, function(spec) {
1548
+ hook('beforeSpec', spec)
1549
+ suite.hook('before_each')
1550
+ JSpec.runSpec(spec)
1551
+ hook('afterSpec', spec)
1552
+ suite.hook('after_each')
1553
+ })
1554
+ if (suite.hasSuites()) {
1555
+ each(suite.suites, function(suite) {
1556
+ JSpec.runSuite(suite)
1557
+ })
1558
+ }
1559
+ hook('afterSuite', suite), suite.hook('after_nested'), suite.hook('after')
1560
+ this.stats.suitesFinished++
1561
+ }
1562
+ },
1563
+
1564
+ /**
1565
+ * Report a failure for the current spec.
1566
+ *
1567
+ * @param {string} message
1568
+ * @api public
1569
+ */
1570
+
1571
+ fail : function(message) {
1572
+ JSpec.currentSpec.fail(message)
1573
+ },
1574
+
1575
+ /**
1576
+ * Report a passing assertion for the current spec.
1577
+ *
1578
+ * @param {string} message
1579
+ * @api public
1580
+ */
1581
+
1582
+ pass : function(message) {
1583
+ JSpec.currentSpec.pass(message)
1584
+ },
1585
+
1586
+ /**
1587
+ * Run a spec.
1588
+ *
1589
+ * @param {Spec} spec
1590
+ * @api public
1591
+ */
1592
+
1593
+ runSpec : function(spec) {
1594
+ this.currentSpec = spec
1595
+ try { this.evalBody(spec.body) }
1596
+ catch (e) { fail(e) }
1597
+ spec.runDeferredAssertions()
1598
+ destub()
1599
+ this.stats.specsFinished++
1600
+ this.stats.assertions += spec.assertions.length
1601
+ },
1602
+
1603
+ /**
1604
+ * Require a dependency, with optional message.
1605
+ *
1606
+ * @param {string} dependency
1607
+ * @param {string} message (optional)
1608
+ * @return {JSpec}
1609
+ * @api public
1610
+ */
1611
+
1612
+ requires : function(dependency, message) {
1613
+ hook('requiring', dependency, message)
1614
+ try { eval(dependency) }
1615
+ catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
1616
+ return this
1617
+ },
1618
+
1619
+ /**
1620
+ * Query against the current query strings keys
1621
+ * or the queryString specified.
1622
+ *
1623
+ * @param {string} key
1624
+ * @param {string} queryString
1625
+ * @return {string, null}
1626
+ * @api private
1627
+ */
1628
+
1629
+ query : function(key, queryString) {
1630
+ var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
1631
+ return inject(queryString.split('&'), null, function(value, pair){
1632
+ parts = pair.split('=')
1633
+ return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
1634
+ })
1635
+ },
1636
+
1637
+ /**
1638
+ * Ad-hoc POST request for JSpec server usage.
1639
+ *
1640
+ * @param {string} uri
1641
+ * @param {string} data
1642
+ * @api private
1643
+ */
1644
+
1645
+ post : function(uri, data) {
1646
+ if (any(hook('posting', uri, data), haveStopped)) return
1647
+ var request = this.xhr()
1648
+ request.open('POST', uri, false)
1649
+ request.setRequestHeader('Content-Type', 'application/json')
1650
+ request.send(JSpec.JSON.encode(data))
1651
+ },
1652
+
1653
+ /**
1654
+ * Instantiate an XMLHttpRequest.
1655
+ *
1656
+ * Here we utilize IE's lame ActiveXObjects first which
1657
+ * allow IE access serve files via the file: protocol, otherwise
1658
+ * we then default to XMLHttpRequest.
1659
+ *
1660
+ * @return {XMLHttpRequest, ActiveXObject}
1661
+ * @api private
1662
+ */
1663
+
1664
+ xhr : function() {
1665
+ return this.ieXhr() || new JSpec.request
1666
+ },
1667
+
1668
+ /**
1669
+ * Return Microsoft piece of crap ActiveXObject.
1670
+ *
1671
+ * @return {ActiveXObject}
1672
+ * @api public
1673
+ */
1674
+
1675
+ ieXhr : function() {
1676
+ function object(str) {
1677
+ try { return new ActiveXObject(str) } catch(e) {}
1678
+ }
1679
+ return object('Msxml2.XMLHTTP.6.0') ||
1680
+ object('Msxml2.XMLHTTP.3.0') ||
1681
+ object('Msxml2.XMLHTTP') ||
1682
+ object('Microsoft.XMLHTTP')
1683
+ },
1684
+
1685
+ /**
1686
+ * Check for HTTP request support.
1687
+ *
1688
+ * @return {bool}
1689
+ * @api private
1690
+ */
1691
+
1692
+ hasXhr : function() {
1693
+ return JSpec.request || 'ActiveXObject' in main
1694
+ },
1695
+
1696
+ /**
1697
+ * Try loading _file_ returning the contents
1698
+ * string or null. Chain to locate / read a file.
1699
+ *
1700
+ * @param {string} file
1701
+ * @return {string}
1702
+ * @api public
1703
+ */
1704
+
1705
+ tryLoading : function(file) {
1706
+ try { return JSpec.load(file) } catch (e) {}
1707
+ },
1708
+
1709
+ /**
1710
+ * Load a _file_'s contents.
1711
+ *
1712
+ * @param {string} file
1713
+ * @param {function} callback
1714
+ * @return {string}
1715
+ * @api public
1716
+ */
1717
+
1718
+ load : function(file, callback) {
1719
+ if (any(hook('loading', file), haveStopped)) return
1720
+ if ('readFile' in main)
1721
+ return readFile(file)
1722
+ else if (this.hasXhr()) {
1723
+ var request = this.xhr()
1724
+ request.open('GET', file, false)
1725
+ request.send(null)
1726
+ if (request.readyState == 4 &&
1727
+ (request.status == 0 ||
1728
+ request.status.toString().charAt(0) == 2))
1729
+ return request.responseText
1730
+ }
1731
+ else
1732
+ throw new Error("failed to load `" + file + "'")
1733
+ },
1734
+
1735
+ /**
1736
+ * Load, pre-process, and evaluate a file.
1737
+ *
1738
+ * @param {string} file
1739
+ * @param {JSpec}
1740
+ * @api public
1741
+ */
1742
+
1743
+ exec : function(file) {
1744
+ if (any(hook('executing', file), haveStopped)) return this
1745
+ eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
1746
+ return this
1747
+ }
1748
+ }
1749
+
1750
+ // --- Node.js support
1751
+
1752
+ if (typeof GLOBAL === 'object' && typeof exports === 'object')
1753
+ quit = process.exit,
1754
+ print = require('sys').puts,
1755
+ readFile = require('fs').readFileSync
1756
+
1757
+ // --- Utility functions
1758
+
1759
+ var main = this,
1760
+ find = JSpec.any,
1761
+ utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
1762
+ error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/)
1763
+ while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift())
1764
+ if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
1765
+
1766
+ // --- Matchers
1767
+
1768
+ addMatchers({
1769
+ equal : "===",
1770
+ eql : "equal(actual, expected)",
1771
+ be : "alias equal",
1772
+ be_greater_than : ">",
1773
+ be_less_than : "<",
1774
+ be_at_least : ">=",
1775
+ be_at_most : "<=",
1776
+ be_a : "actual.constructor == expected",
1777
+ be_an : "alias be_a",
1778
+ be_an_instance_of : "actual instanceof expected",
1779
+ be_null : "actual == null",
1780
+ be_true : "actual == true",
1781
+ be_false : "actual == false",
1782
+ be_undefined : "typeof actual == 'undefined'",
1783
+ be_type : "typeof actual == expected",
1784
+ match : "typeof actual == 'string' ? actual.match(expected) : false",
1785
+ respond_to : "typeof actual[expected] == 'function'",
1786
+ have_length : "actual.length == expected",
1787
+ be_within : "actual >= expected[0] && actual <= last(expected)",
1788
+ have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
1789
+
1790
+ receive : { defer : true, match : function(actual, method, times) {
1791
+ var proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
1792
+ JSpec.currentSpec.assertions.push(proxy)
1793
+ return proxy
1794
+ }},
1795
+
1796
+ be_empty : function(actual) {
1797
+ if (actual.constructor == Object && actual.length == undefined)
1798
+ for (var key in actual)
1799
+ return false;
1800
+ return !actual.length
1801
+ },
1802
+
1803
+ include : function(actual) {
1804
+ for (var state = true, i = 1; i < arguments.length; i++) {
1805
+ var arg = arguments[i]
1806
+ switch (actual.constructor) {
1807
+ case String:
1808
+ case Number:
1809
+ case RegExp:
1810
+ case Function:
1811
+ state = actual.toString().indexOf(arg) !== -1
1812
+ break
1813
+
1814
+ case Object:
1815
+ state = arg in actual
1816
+ break
1817
+
1818
+ case Array:
1819
+ state = any(actual, function(value){ return equal(value, arg) })
1820
+ break
1821
+ }
1822
+ if (!state) return false
1823
+ }
1824
+ return true
1825
+ },
1826
+
1827
+ throw_error : { match : function(actual, expected, message) {
1828
+ try { actual() }
1829
+ catch (e) {
1830
+ this.e = e
1831
+ var assert = function(arg) {
1832
+ switch (arg.constructor) {
1833
+ case RegExp : return arg.test(e.message || e.toString())
1834
+ case String : return arg == (e.message || e.toString())
1835
+ case Function : return e instanceof arg || e.name == arg.name
1836
+ }
1837
+ }
1838
+ return message ? assert(expected) && assert(message) :
1839
+ expected ? assert(expected) :
1840
+ true
1841
+ }
1842
+ }, message : function(actual, expected, negate) {
1843
+ // TODO: refactor when actual is not in expected [0]
1844
+ var message_for = function(i) {
1845
+ if (expected[i] == undefined) return 'exception'
1846
+ switch (expected[i].constructor) {
1847
+ case RegExp : return 'exception matching ' + puts(expected[i])
1848
+ case String : return 'exception of ' + puts(expected[i])
1849
+ case Function : return expected[i].name || 'Error'
1850
+ }
1851
+ }
1852
+ var exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
1853
+ return 'expected ' + exception + (negate ? ' not ' : '' ) +
1854
+ ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
1855
+ }},
1856
+
1857
+ have : function(actual, length, property) {
1858
+ return actual[property] == null ? false : actual[property].length == length
1859
+ },
1860
+
1861
+ have_at_least : function(actual, length, property) {
1862
+ return actual[property] == null ? (length === 0) : actual[property].length >= length
1863
+ },
1864
+
1865
+ have_at_most :function(actual, length, property) {
1866
+ return actual[property] == null || actual[property].length <= length
1867
+ },
1868
+
1869
+ have_within : function(actual, range, property) {
1870
+ var length = actual[property] == undefined ? 0 : actual[property].length
1871
+ return length >= range.shift() && length <= range.pop()
1872
+ },
1873
+
1874
+ have_prop : function(actual, property, value) {
1875
+ var actualVal = actual[property], actualType = typeof actualVal
1876
+ return (actualType == 'function' || actualType == 'undefined') ? false :
1877
+ typeof value === 'undefined' ||
1878
+ does(actual[property],'eql',value)
1879
+ },
1880
+
1881
+ have_property : function(actual, property, value) {
1882
+ var actualVal = actual[property], actualType = typeof actualVal
1883
+ return (actualType == 'function' || actualType == 'undefined') ? false :
1884
+ typeof value === 'undefined' ||
1885
+ value === actualVal
1886
+ }
1887
+ })
1888
+
1889
+ })()