jcov 1.0.0

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