visionmedia-jspec 1.1.3 → 1.1.4

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.
@@ -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