visionmedia-jspec 1.1.3 → 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,8 +5,8 @@
5
5
 
6
6
  JSpec = {
7
7
 
8
- version : '1.1.3',
9
- main : this,
8
+ version : '1.1.4',
9
+ file : '',
10
10
  suites : [],
11
11
  matchers : {},
12
12
  stats : { specs : 0, assertions : 0, failures : 0, passes : 0 },
@@ -29,6 +29,7 @@
29
29
  * To reset (usually in after hook) simply set to null like below:
30
30
  *
31
31
  * JSpec.context = null
32
+ *
32
33
  */
33
34
 
34
35
  defaultContext : {
@@ -41,106 +42,7 @@
41
42
  },
42
43
 
43
44
  // --- Objects
44
-
45
- /**
46
- * Matcher.
47
- *
48
- * There are many ways to define a matcher within JSpec. The first being
49
- * a string that is less than 4 characters long, which is considered a simple
50
- * binary operation between two expressions. For example the matcher '==' simply
51
- * evaluates to 'actual == expected'.
52
- *
53
- * The second way to create a matcher is with a larger string, which is evaluated,
54
- * and then returned such as 'actual.match(expected)'.
55
- *
56
- * You may alias simply by starting a string with 'alias', such as 'be' : 'alias eql'.
57
- *
58
- * Finally an object may be used, and must contain a 'match' method, which is passed
59
- * both the expected, and actual values. Optionally a 'message' method may be used to
60
- * specify a custom message. Example:
61
- *
62
- * match : function(actual, expected) {
63
- * return typeof actual == expected
64
- * }
65
- *
66
- * @param {string} name
67
- * @param {hash, string} matcher
68
- * @param {object} actual
69
- * @param {array} expected
70
- * @param {bool} negate
71
- * @return {Matcher}
72
- * @api private
73
- */
74
-
75
- Matcher : function (name, matcher, actual, expected, negate) {
76
- self = this
77
- this.name = name
78
- this.message = ''
79
- this.passed = false
80
-
81
- // Define matchers from strings
82
-
83
- if (typeof matcher == 'string') {
84
- if (matcher.match(/^alias (\w+)/)) matcher = JSpec.matchers[matcher.match(/^alias (\w+)/)[1]]
85
- if (matcher.length < 4) body = 'actual ' + matcher + ' expected'
86
- else body = matcher
87
- matcher = { match : function(actual, expected) { return eval(body) } }
88
- }
89
-
90
- // Generate matcher message
91
-
92
- function generateMessage() {
93
- // TODO: clone expected instead of unshifting in this.match()
94
- expectedMessage = print.apply(this, expected.slice(1))
95
- return 'expected ' + print(actual) + ' to ' + (negate ? ' not ' : '') + name.replace(/_/g, ' ') + ' ' + expectedMessage
96
- }
97
-
98
- // Set message to matcher callback invocation or auto-generated message
99
-
100
- function setMessage() {
101
- self.message = typeof matcher.message == 'function' ?
102
- matcher.message(actual, expected, negate):
103
- generateMessage()
104
- }
105
-
106
- // Pass the matcher
107
-
108
- function pass() {
109
- setMessage()
110
- JSpec.stats.passes += 1
111
- self.passed = true
112
- }
113
-
114
- // Fail the matcher
115
-
116
- function fail() {
117
- setMessage()
118
- JSpec.stats.failures += 1
119
- }
120
-
121
- // Return result of match
122
-
123
- this.match = function() {
124
- expected.unshift(actual == null ? null : actual.valueOf())
125
- return matcher.match.apply(JSpec, expected)
126
- }
127
-
128
- // Boolean match result
129
-
130
- this.passes = function() {
131
- this.result = this.match()
132
- return negate? !this.result : this.result
133
- }
134
-
135
- // Performs match, and passes / fails the matcher
136
-
137
- this.exec = function() {
138
- this.passes() ? pass() : fail()
139
- return this
140
- }
141
- },
142
-
143
-
45
+
144
46
  formatters : {
145
47
 
146
48
  /**
@@ -156,6 +58,7 @@
156
58
  DOM : function(results, options) {
157
59
  id = option('reportToId') || 'jspec'
158
60
  report = document.getElementById(id)
61
+ failuresOnly = option('failuresOnly')
159
62
  classes = results.stats.failures ? 'has-failures' : ''
160
63
  if (!report) error('requires the element #' + id + ' to output its reports')
161
64
 
@@ -165,14 +68,13 @@
165
68
  <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
166
69
  </div><table class="suites">'
167
70
 
168
- function renderSuite(suite) {
169
- failuresOnly = option('failuresOnly')
71
+ renderSuite = function(suite) {
170
72
  displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
171
73
  if (displaySuite && suite.hasSpecs()) {
172
74
  markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>'
173
75
  each(suite.specs, function(i, spec){
174
76
  markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
175
- if (spec.requiresImplementation() && !failuresOnly)
77
+ if (spec.requiresImplementation())
176
78
  markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>'
177
79
  else if (spec.passed() && !failuresOnly)
178
80
  markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
@@ -184,7 +86,7 @@
184
86
  }
185
87
  }
186
88
 
187
- function renderSuites(suites) {
89
+ renderSuites = function(suites) {
188
90
  each(suites, function(suite){
189
91
  renderSuite(suite)
190
92
  if (suite.hasSuites()) renderSuites(suite.suites)
@@ -197,6 +99,51 @@
197
99
 
198
100
  report.innerHTML = markup
199
101
  },
102
+
103
+ /**
104
+ * Terminal formatter.
105
+ *
106
+ * @api public
107
+ */
108
+
109
+ Terminal : function(results, options) {
110
+ failuresOnly = option('failuresOnly')
111
+ puts(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
112
+ color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n")
113
+
114
+ indent = function(string) {
115
+ return string.replace(/^(.)/gm, ' $1')
116
+ }
117
+
118
+ renderSuite = function(suite) {
119
+ displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
120
+ if (displaySuite && suite.hasSpecs()) {
121
+ puts(color(' ' + suite.description, 'bold'))
122
+ results.each(suite.specs, function(spec){
123
+ assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
124
+ return graph + color('.', assertion.passed ? 'green' : 'red')
125
+ })
126
+ if (spec.requiresImplementation())
127
+ puts(color(' ' + spec.description, 'blue') + assertionsGraph)
128
+ else if (spec.passed() && !failuresOnly)
129
+ puts(color(' ' + spec.description, 'green') + assertionsGraph)
130
+ else
131
+ puts(color(' ' + spec.description, 'red') + assertionsGraph +
132
+ "\n" + indent(spec.failure().message) + "\n")
133
+ })
134
+ puts('')
135
+ }
136
+ }
137
+
138
+ renderSuites = function(suites) {
139
+ each(suites, function(suite){
140
+ renderSuite(suite)
141
+ if (suite.hasSuites()) renderSuites(suite.suites)
142
+ })
143
+ }
144
+
145
+ renderSuites(results.suites)
146
+ },
200
147
 
201
148
  /**
202
149
  * Console formatter, tested with Firebug and Safari 4.
@@ -208,7 +155,7 @@
208
155
  console.log('')
209
156
  console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
210
157
 
211
- function renderSuite(suite) {
158
+ renderSuite = function(suite) {
212
159
  if (suite.ran) {
213
160
  console.group(suite.description)
214
161
  results.each(suite.specs, function(spec){
@@ -224,7 +171,7 @@
224
171
  }
225
172
  }
226
173
 
227
- function renderSuites(suites) {
174
+ renderSuites = function(suites) {
228
175
  each(suites, function(suite){
229
176
  renderSuite(suite)
230
177
  if (suite.hasSuites()) renderSuites(suite.suites)
@@ -234,7 +181,31 @@
234
181
  renderSuites(results.suites)
235
182
  }
236
183
  },
237
-
184
+
185
+ Assertion : function(matcher, actual, expected, negate) {
186
+ extend(this, {
187
+ message : '',
188
+ passed : false,
189
+ actual : actual,
190
+ negate : negate,
191
+ matcher : matcher,
192
+ expected : expected,
193
+ record : function(result) {
194
+ result ? JSpec.stats.passes++ : JSpec.stats.failures++
195
+ },
196
+
197
+ exec : function() {
198
+ // TODO: remove unshifting of expected
199
+ expected.unshift(actual == null ? null : actual.valueOf())
200
+ result = matcher.match.apply(JSpec, expected)
201
+ this.passed = negate ? !result : result
202
+ this.record(this.passed)
203
+ if (!this.passed) this.message = matcher.message(actual, expected, negate, matcher.name)
204
+ return this
205
+ }
206
+ })
207
+ },
208
+
238
209
  /**
239
210
  * Specification Suite block object.
240
211
  *
@@ -244,62 +215,67 @@
244
215
  */
245
216
 
246
217
  Suite : function(description, body) {
247
- this.body = body, this.suites = [], this.specs = []
248
- this.description = description, this.ran = false
249
- this.hooks = { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }
218
+ extend(this, {
219
+ body: body,
220
+ description: description,
221
+ suites: [],
222
+ specs: [],
223
+ ran: false,
224
+ hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
225
+
226
+ // Add a spec to the suite
250
227
 
251
- // Add a spec to the suite
228
+ it : function(description, body) {
229
+ spec = new JSpec.Spec(description, body)
230
+ this.specs.push(spec)
231
+ spec.suite = this
232
+ },
252
233
 
253
- this.addSpec = function(description, body) {
254
- spec = new JSpec.Spec(description, body)
255
- this.specs.push(spec)
256
- spec.suite = this
257
- }
258
-
259
- // Add a hook to the suite
260
-
261
- this.addHook = function(hook, body) {
262
- this.hooks[hook].push(body)
263
- }
264
-
265
- // Add a nested suite
266
-
267
- this.addSuite = function(description, body) {
268
- suite = new JSpec.Suite(description, body)
269
- suite.description = this.description + ' ' + suite.description
270
- this.suites.push(suite)
271
- suite.suite = this
272
- }
234
+ // Add a hook to the suite
273
235
 
274
- // Invoke a hook in context to this suite
236
+ addHook : function(hook, body) {
237
+ this.hooks[hook].push(body)
238
+ },
275
239
 
276
- this.hook = function(hook) {
277
- each(this.hooks[hook], function(body) {
278
- JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
279
- })
280
- }
281
-
282
- // Check if nested suites are present
283
-
284
- this.hasSuites = function() {
285
- return this.suites.length
286
- }
287
-
288
- // Check if this suite has specs
289
-
290
- this.hasSpecs = function() {
291
- return this.specs.length
292
- }
293
-
294
- // Check if the entire suite passed
240
+ // Add a nested suite
295
241
 
296
- this.passed = function() {
297
- var passed = true
298
- each(this.specs, function(spec){
299
- if (!spec.passed()) passed = false
300
- })
301
- return passed
302
- }
242
+ describe : function(description, body) {
243
+ suite = new JSpec.Suite(description, body)
244
+ suite.description = this.description + ' ' + suite.description
245
+ this.suites.push(suite)
246
+ suite.suite = this
247
+ },
248
+
249
+ // Invoke a hook in context to this suite
250
+
251
+ hook : function(hook) {
252
+ each(this.hooks[hook], function(body) {
253
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
254
+ })
255
+ },
256
+
257
+ // Check if nested suites are present
258
+
259
+ hasSuites : function() {
260
+ return this.suites.length
261
+ },
262
+
263
+ // Check if this suite has specs
264
+
265
+ hasSpecs : function() {
266
+ return this.specs.length
267
+ },
268
+
269
+ // Check if the entire suite passed
270
+
271
+ passed : function() {
272
+ var passed = true
273
+ each(this.specs, function(spec){
274
+ if (!spec.passed()) passed = false
275
+ })
276
+ return passed
277
+ }
278
+ })
303
279
  },
304
280
 
305
281
  /**
@@ -311,48 +287,136 @@
311
287
  */
312
288
 
313
289
  Spec : function(description, body) {
314
- this.body = body, this.description = description, this.assertions = []
290
+ extend(this, {
291
+ body : body,
292
+ description : description,
293
+ assertions : [],
294
+
295
+ // Find first failing assertion
315
296
 
316
- // Find first failing assertion
297
+ failure : function() {
298
+ return inject(this.assertions, null, function(failure, assertion){
299
+ return !assertion.passed && !failure ? assertion : failure
300
+ })
301
+ },
317
302
 
318
- this.failure = function() {
319
- return inject(this.assertions, null, function(failure, assertion){
320
- return !assertion.passed && !failure ? assertion : failure
321
- })
322
- }
323
-
324
- // Find all failing assertions
325
-
326
- this.failures = function() {
327
- return inject(this.assertions, [], function(failures, assertion){
328
- if (!assertion.passed) failures.push(assertion)
329
- return failures
330
- })
331
- }
303
+ // Find all failing assertions
332
304
 
333
- // Weither or not the spec passed
305
+ failures : function() {
306
+ return inject(this.assertions, [], function(failures, assertion){
307
+ if (!assertion.passed) failures.push(assertion)
308
+ return failures
309
+ })
310
+ },
334
311
 
335
- this.passed = function() {
336
- return !this.failure()
337
- }
312
+ // Weither or not the spec passed
338
313
 
339
- // Weither or not the spec requires implementation (no assertions)
314
+ passed : function() {
315
+ return !this.failure()
316
+ },
340
317
 
341
- this.requiresImplementation = function() {
342
- return this.assertions.length == 0
343
- }
344
-
345
- // Sprite based assertions graph
346
-
347
- this.assertionsGraph = function() {
348
- return map(this.assertions, function(assertion){
349
- return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
350
- }).join('')
351
- }
318
+ // Weither or not the spec requires implementation (no assertions)
319
+
320
+ requiresImplementation : function() {
321
+ return this.assertions.length == 0
322
+ },
323
+
324
+ // Sprite based assertions graph
325
+
326
+ assertionsGraph : function() {
327
+ return map(this.assertions, function(assertion){
328
+ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
329
+ }).join('')
330
+ }
331
+ })
352
332
  },
353
333
 
354
334
  // --- Methods
355
335
 
336
+ /**
337
+ * Return ANSI-escaped colored string.
338
+ *
339
+ * @param {string} string
340
+ * @param {string} color
341
+ * @return {string}
342
+ * @api public
343
+ */
344
+
345
+ color : function(string, color) {
346
+ return "\u001B[" + {
347
+ bold : 1,
348
+ black : 30,
349
+ red : 31,
350
+ green : 32,
351
+ yellow : 33,
352
+ blue : 34,
353
+ magenta : 35,
354
+ cyan : 36,
355
+ white : 37,
356
+ }[color] + 'm' + string + "\u001B[0m"
357
+ },
358
+
359
+ /**
360
+ * Default matcher message callback.
361
+ *
362
+ * @api private
363
+ */
364
+
365
+ defaultMatcherMessage : function(actual, expected, negate, name) {
366
+ return 'expected ' + print(actual) + ' to ' +
367
+ (negate ? 'not ' : '') +
368
+ name.replace(/_/g, ' ') +
369
+ ' ' + print.apply(this, expected.slice(1))
370
+ },
371
+
372
+ /**
373
+ * Normalize a matcher message.
374
+ *
375
+ * When no messge callback is present the defaultMatcherMessage
376
+ * will be assigned, will suffice for most matchers.
377
+ *
378
+ * @param {hash} matcher
379
+ * @return {hash}
380
+ * @api public
381
+ */
382
+
383
+ normalizeMatcherMessage : function(matcher) {
384
+ if (typeof matcher.message != 'function')
385
+ matcher.message = this.defaultMatcherMessage
386
+ return matcher
387
+ },
388
+
389
+ /**
390
+ * Normalize a matcher body
391
+ *
392
+ * This process allows the following conversions until
393
+ * the matcher is in its final normalized hash state.
394
+ *
395
+ * - '==' becomes 'actual == expected'
396
+ * - 'actual == expected' becomes 'return actual == expected'
397
+ * - function(actual, expected) { return actual == expected } becomes
398
+ * { match : function(actual, expected) { return actual == expected }}
399
+ *
400
+ * @param {mixed} body
401
+ * @return {hash}
402
+ * @api public
403
+ */
404
+
405
+ normalizeMatcherBody : function(body) {
406
+ switch (body.constructor) {
407
+ case String:
408
+ if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
409
+ if (body.length < 4) body = 'actual ' + body + ' expected'
410
+ return { match : function(actual, expected) { return eval(body) }}
411
+
412
+ case Function:
413
+ return { match : body }
414
+
415
+ default:
416
+ return body
417
+ }
418
+ },
419
+
356
420
  /**
357
421
  * Get option value. This method first checks if
358
422
  * the option key has been set via the query string,
@@ -364,8 +428,8 @@
364
428
  */
365
429
 
366
430
  option : function(key) {
367
- if ((value = query(key)) !== null) return value
368
- else return JSpec.options[key] || null
431
+ return (value = query(key)) !== null ? value :
432
+ JSpec.options[key] || null
369
433
  },
370
434
 
371
435
  /**
@@ -473,9 +537,9 @@
473
537
 
474
538
  match : function(actual, negate, name, expected) {
475
539
  if (typeof negate == 'string') negate = negate == 'should' ? false : true
476
- matcher = new this.Matcher(name, this.matchers[name], actual, expected, negate)
477
- this.currentSpec.assertions.push(matcher.exec())
478
- return matcher.result
540
+ assertion = new JSpec.Assertion(this.matchers[name], actual, expected, negate)
541
+ this.currentSpec.assertions.push(assertion.exec())
542
+ return assertion.passed
479
543
  },
480
544
 
481
545
  /**
@@ -493,7 +557,7 @@
493
557
  if (object.hasOwnProperty(key))
494
558
  callback.length == 1 ?
495
559
  callback.call(JSpec, object[key]):
496
- callback.call(JSpec, key, object[key])
560
+ callback.call(JSpec, key, object[key])
497
561
  }
498
562
  return JSpec
499
563
  },
@@ -512,7 +576,7 @@
512
576
  each(object, function(key, value){
513
577
  initial = callback.length == 2 ?
514
578
  callback.call(JSpec, initial, value):
515
- callback.call(JSpec, initial, key, value) || initial
579
+ callback.call(JSpec, initial, key, value) || initial
516
580
  })
517
581
  return initial
518
582
  },
@@ -531,6 +595,20 @@
531
595
  replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
532
596
  replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
533
597
  },
598
+
599
+ /**
600
+ * Extend an object with another.
601
+ *
602
+ * @param {object} object
603
+ * @param {object} other
604
+ * @api public
605
+ */
606
+
607
+ extend : function(object, other) {
608
+ each(other, function(property, value){
609
+ object[property] = value
610
+ })
611
+ },
534
612
 
535
613
  /**
536
614
  * Map callback return values.
@@ -545,7 +623,7 @@
545
623
  return inject(object, [], function(memo, key, value){
546
624
  memo.push(callback.length == 1 ?
547
625
  callback.call(JSpec, value):
548
- callback.call(JSpec, key, value))
626
+ callback.call(JSpec, key, value))
549
627
  })
550
628
  },
551
629
 
@@ -563,7 +641,7 @@
563
641
  if (state) return true
564
642
  return callback.length == 1 ?
565
643
  callback.call(JSpec, value):
566
- callback.call(JSpec, key, value)
644
+ callback.call(JSpec, key, value)
567
645
  })
568
646
  },
569
647
 
@@ -576,7 +654,24 @@
576
654
  */
577
655
 
578
656
  addMatchers : function(matchers) {
579
- each(matchers, function(name, body){ this.matchers[name] = body })
657
+ each(matchers, function(name, body){
658
+ this.addMatcher(name, body)
659
+ })
660
+ return this
661
+ },
662
+
663
+ /**
664
+ * Define a matcher.
665
+ *
666
+ * @param {string} name
667
+ * @param {hash, function, string} body
668
+ * @return {JSpec}
669
+ * @api public
670
+ */
671
+
672
+ addMatcher : function(name, body) {
673
+ this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
674
+ this.matchers[name].name = name
580
675
  return this
581
676
  },
582
677
 
@@ -589,7 +684,7 @@
589
684
  * @api public
590
685
  */
591
686
 
592
- addSuite : function(description, body) {
687
+ describe : function(description, body) {
593
688
  this.suites.push(new JSpec.Suite(description, body))
594
689
  return this
595
690
  },
@@ -618,16 +713,16 @@
618
713
 
619
714
  preprocess : function(input) {
620
715
  return input.
621
- replace(/describe (.*?)$/m, 'JSpec.addSuite($1, function(){').
622
- replace(/describe (.*?)$/gm, 'this.addSuite($1, function(){').
623
- replace(/it (.*?)$/gm, 'this.addSpec($1, function(){').
716
+ replace(/describe (.*?)$/m, 'JSpec.describe($1, function(){').
717
+ replace(/describe (.*?)$/gm, 'this.describe($1, function(){').
718
+ replace(/it (.*?)$/gm, 'this.it($1, function(){').
624
719
  replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'this.addHook("$1", function(){').
625
720
  replace(/end(?= |\n|$)/gm, '});').
626
- replace(/-{/g, 'function(){').
721
+ replace(/-\{/g, 'function(){').
627
722
  replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
628
723
  replace(/([\s\(]+)\./gm, '$1this.').
629
724
  replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)').
630
- replace(/(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, 'JSpec.match($1, "$2", "$3", [$4]);')
725
+ replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 JSpec.match($2, "$3", "$4", [$5]);')
631
726
  },
632
727
 
633
728
  /**
@@ -649,25 +744,26 @@
649
744
  /**
650
745
  * Report on the results.
651
746
  *
652
- * @return {JSpec}
653
747
  * @api public
654
748
  */
655
749
 
656
750
  report : function() {
657
751
  this.options.formatter ?
658
752
  new this.options.formatter(this, this.options):
659
- new this.formatters.DOM(this, this.options)
660
- return this
753
+ new this.formatters.DOM(this, this.options)
661
754
  },
662
755
 
663
756
  /**
664
- * Run the spec suites.
757
+ * Run the spec suites. Options are merged
758
+ * with JSpec options when present.
665
759
  *
760
+ * @param {hash} options
666
761
  * @return {JSpec}
667
762
  * @api public
668
763
  */
669
764
 
670
- run : function() {
765
+ run : function(options) {
766
+ if (options) extend(this.options, options)
671
767
  if (option('profile')) console.group('Profile')
672
768
  each(this.suites, function(suite) { this.runSuite(suite) })
673
769
  if (option('profile')) console.groupEnd()
@@ -699,6 +795,18 @@
699
795
  }
700
796
  return this
701
797
  },
798
+
799
+ /**
800
+ * Report a failure for the current spec.
801
+ *
802
+ * @param {string} message
803
+ * @api public
804
+ */
805
+
806
+ fail : function(message) {
807
+ JSpec.currentSpec.assertions.push({ passed : false, message : message })
808
+ JSpec.stats.failures++
809
+ },
702
810
 
703
811
  /**
704
812
  * Run a spec.
@@ -711,7 +819,8 @@
711
819
  this.currentSpec = spec
712
820
  this.stats.specs++
713
821
  if (option('profile')) console.time(spec.description)
714
- this.evalBody(spec.body, "Error in spec '" + spec.description + "': ")
822
+ try { this.evalBody(spec.body) }
823
+ catch (e) { fail(e) }
715
824
  if (option('profile')) console.timeEnd(spec.description)
716
825
  this.stats.assertions += spec.assertions.length
717
826
  },
@@ -726,7 +835,7 @@
726
835
 
727
836
  requires : function(dependency, message) {
728
837
  try { eval(dependency) }
729
- catch (e) { error('depends on ' + dependency + ' ' + (message || '')) }
838
+ catch (e) { error('JSpec depends on ' + dependency + ' ' + message, '') }
730
839
  },
731
840
 
732
841
  /**
@@ -736,14 +845,14 @@
736
845
  * @param {string} key
737
846
  * @param {string} queryString
738
847
  * @return {string, null}
739
- * @api public
848
+ * @api private
740
849
  */
741
850
 
742
851
  query : function(key, queryString) {
743
- queryString = (queryString || window.location.search || '').substring(1)
852
+ queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
744
853
  return inject(queryString.split('&'), null, function(value, pair){
745
854
  parts = pair.split('=')
746
- return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
855
+ return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
747
856
  })
748
857
  },
749
858
 
@@ -756,7 +865,59 @@
756
865
  */
757
866
 
758
867
  error : function(message, e) {
759
- throw 'jspec: ' + message + (e ? e.message : '') + ' near line ' + e.line
868
+ throw (message ? message : '') + e.toString() +
869
+ (e.line ? ' near line ' + e.line : '') +
870
+ (JSpec.file ? ' in ' + JSpec.file : '')
871
+ },
872
+
873
+ /**
874
+ * Ad-hoc POST request for JSpec server usage.
875
+ *
876
+ * @param {string} url
877
+ * @param {string} data
878
+ * @api private
879
+ */
880
+
881
+ post : function(url, data) {
882
+ request = this.xhr()
883
+ request.open('POST', url, false)
884
+ request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
885
+ request.send(data)
886
+ },
887
+
888
+ /**
889
+ * Report back to server with statistics.
890
+ *
891
+ * @api private
892
+ */
893
+
894
+ reportToServer : function() {
895
+ JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures)
896
+ if ('close' in window) window.close()
897
+ },
898
+
899
+ /**
900
+ * Instantiate an XMLHttpRequest.
901
+ *
902
+ * @return {ActiveXObject, XMLHttpRequest}
903
+ * @api private
904
+ */
905
+
906
+ xhr : function() {
907
+ return window.ActiveXObject ?
908
+ new ActiveXObject("Microsoft.XMLHTTP"):
909
+ new XMLHttpRequest()
910
+ },
911
+
912
+ /**
913
+ * Check for HTTP request support.
914
+ *
915
+ * @return {bool}
916
+ * @api private
917
+ */
918
+
919
+ hasXhr : function() {
920
+ return 'XMLHttpRequest' in main || 'ActiveXObject' in main
760
921
  },
761
922
 
762
923
  /**
@@ -768,14 +929,15 @@
768
929
  */
769
930
 
770
931
  load : function(file) {
771
- if ('XMLHttpRequest' in this.main) {
772
- request = new XMLHttpRequest
932
+ this.file = file
933
+ if (this.hasXhr()) {
934
+ request = this.xhr()
773
935
  request.open('GET', file, false)
774
936
  request.send(null)
775
937
  if (request.readyState == 4) return request.responseText
776
938
  }
777
- else if ('load' in this.main)
778
- load(file) // TODO: workaround for IO issue / preprocessing
939
+ else if ('readFile' in main)
940
+ return readFile(file)
779
941
  else
780
942
  error('cannot load ' + file)
781
943
  },
@@ -796,32 +958,38 @@
796
958
 
797
959
  // --- Utility functions
798
960
 
961
+ main = this
962
+ puts = print
799
963
  map = JSpec.map
800
964
  any = JSpec.any
801
965
  last = JSpec.last
966
+ fail = JSpec.fail
802
967
  range = JSpec.range
803
968
  each = JSpec.each
804
969
  option = JSpec.option
805
970
  inject = JSpec.inject
806
971
  error = JSpec.error
807
972
  escape = JSpec.escape
973
+ extend = JSpec.extend
808
974
  print = JSpec.print
809
975
  hash = JSpec.hash
810
976
  query = JSpec.query
811
977
  strip = JSpec.strip
978
+ color = JSpec.color
812
979
  addMatchers = JSpec.addMatchers
813
980
 
814
981
  // --- Matchers
815
982
 
816
983
  addMatchers({
817
- be : "alias eql",
818
984
  equal : "===",
985
+ be : "alias equal",
819
986
  be_greater_than : ">",
820
987
  be_less_than : "<",
821
988
  be_at_least : ">=",
822
989
  be_at_most : "<=",
823
990
  be_a : "actual.constructor == expected",
824
991
  be_an : "alias be_a",
992
+ be_an_instance_of : "actual instanceof expected",
825
993
  be_null : "actual == null",
826
994
  be_empty : "actual.length == 0",
827
995
  be_true : "actual == true",
@@ -833,12 +1001,14 @@
833
1001
  be_within : "actual >= expected[0] && actual <= last(expected)",
834
1002
  have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
835
1003
 
836
- eql : { match : function(actual, expected) {
837
- if (actual.constructor == Array || actual.constructor == Object) return hash(actual) == hash(expected)
838
- else return actual == expected
839
- }},
1004
+ eql : function(actual, expected) {
1005
+ return actual.constructor == Array ||
1006
+ actual instanceof Object ?
1007
+ hash(actual) == hash(expected):
1008
+ actual == expected
1009
+ },
840
1010
 
841
- include : { match : function(actual) {
1011
+ include : function(actual) {
842
1012
  for (state = true, i = 1; i < arguments.length; i++) {
843
1013
  arg = arguments[i]
844
1014
  switch (actual.constructor) {
@@ -860,43 +1030,50 @@
860
1030
  if (!state) return false
861
1031
  }
862
1032
  return true
863
- }},
1033
+ },
864
1034
 
865
- throw_error : { match : function(actual, expected) {
1035
+ throw_error : function(actual, expected) {
866
1036
  try { actual() }
867
1037
  catch (e) {
868
- if (expected == undefined) return true
869
- else return expected.constructor == RegExp ?
870
- expected.test(e) : e.toString() == expected
1038
+ if (expected == undefined) return true
1039
+ switch (expected.constructor) {
1040
+ case RegExp: return expected.test(e)
1041
+ case Function: return e instanceof expected
1042
+ case String: return expected == e.toString()
1043
+ }
871
1044
  }
872
- }},
1045
+ },
873
1046
 
874
- have : { match : function(actual, length, property) {
1047
+ have : function(actual, length, property) {
875
1048
  return actual[property].length == length
876
- }},
1049
+ },
877
1050
 
878
- have_at_least : { match : function(actual, length, property) {
1051
+ have_at_least : function(actual, length, property) {
879
1052
  return actual[property].length >= length
880
- }},
1053
+ },
881
1054
 
882
- have_at_most : { match : function(actual, length, property) {
1055
+ have_at_most :function(actual, length, property) {
883
1056
  return actual[property].length <= length
884
- }},
1057
+ },
885
1058
 
886
- have_within : { match : function(actual, range, property) {
1059
+ have_within : function(actual, range, property) {
887
1060
  length = actual[property].length
888
1061
  return length >= range.shift() && length <= range.pop()
889
- }},
1062
+ },
890
1063
 
891
- have_prop : { match : function(actual, property, value) {
892
- if (actual[property] == null || typeof actual[property] == 'function') return false
893
- return value == null ? true : JSpec.matchers['eql'].match(actual[property], value)
894
- }},
1064
+ have_prop : function(actual, property, value) {
1065
+ return actual[property] == null ||
1066
+ actual[property] instanceof Function ? false:
1067
+ value == null ? true:
1068
+ JSpec.matchers.eql.match(actual[property], value)
1069
+ },
895
1070
 
896
- have_property : { match : function(actual, property, value) {
897
- if (actual[property] == null || typeof actual[property] == 'function') return false
898
- return value == null ? true : value === actual[property]
899
- }}
1071
+ have_property : function(actual, property, value) {
1072
+ return actual[property] == null ||
1073
+ actual[property] instanceof Function ? false:
1074
+ value == null ? true:
1075
+ value === actual[property]
1076
+ }
900
1077
  })
901
1078
 
902
1079
  // --- Expose