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.
- data/History.rdoc +33 -0
- data/Manifest +14 -0
- data/README.rdoc +95 -51
- data/bin/jspec +68 -14
- data/jspec.gemspec +3 -3
- data/lib/jspec.jquery.js +30 -24
- data/lib/jspec.js +428 -251
- data/server/browsers.rb +24 -0
- data/server/server.rb +114 -0
- data/spec/server.html +20 -0
- data/spec/spec.core.js +48 -1
- data/spec/spec.grammar.js +5 -0
- data/spec/spec.jquery.js +13 -7
- data/spec/spec.rhino.js +8 -0
- data/templates/default/lib/yourlib.core.js +1 -34
- data/templates/rhino/History.rdoc +4 -0
- data/templates/rhino/README.rdoc +29 -0
- data/templates/rhino/lib/yourlib.core.js +2 -0
- data/templates/rhino/spec/spec.core.js +8 -0
- data/templates/rhino/spec/spec.js +7 -0
- data/templates/server/History.rdoc +4 -0
- data/templates/server/README.rdoc +29 -0
- data/templates/server/lib/yourlib.core.js +2 -0
- data/templates/server/spec/spec.core.js +8 -0
- data/templates/server/spec/spec.html +15 -0
- metadata +16 -2
data/lib/jspec.js
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
|
6
6
|
JSpec = {
|
7
7
|
|
8
|
-
version : '1.1.
|
9
|
-
|
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
|
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()
|
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
|
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
|
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
|
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
|
248
|
-
|
249
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
236
|
+
addHook : function(hook, body) {
|
237
|
+
this.hooks[hook].push(body)
|
238
|
+
},
|
275
239
|
|
276
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
290
|
+
extend(this, {
|
291
|
+
body : body,
|
292
|
+
description : description,
|
293
|
+
assertions : [],
|
294
|
+
|
295
|
+
// Find first failing assertion
|
315
296
|
|
316
|
-
|
297
|
+
failure : function() {
|
298
|
+
return inject(this.assertions, null, function(failure, assertion){
|
299
|
+
return !assertion.passed && !failure ? assertion : failure
|
300
|
+
})
|
301
|
+
},
|
317
302
|
|
318
|
-
|
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
|
-
|
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
|
-
|
336
|
-
return !this.failure()
|
337
|
-
}
|
312
|
+
// Weither or not the spec passed
|
338
313
|
|
339
|
-
|
314
|
+
passed : function() {
|
315
|
+
return !this.failure()
|
316
|
+
},
|
340
317
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
368
|
-
|
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
|
-
|
477
|
-
this.currentSpec.assertions.push(
|
478
|
-
return
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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){
|
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
|
-
|
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.
|
622
|
-
replace(/describe (.*?)$/gm, 'this.
|
623
|
-
replace(/it (.*?)$/gm, 'this.
|
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(
|
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($
|
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
|
-
|
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
|
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 + ' ' +
|
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
|
848
|
+
* @api private
|
740
849
|
*/
|
741
850
|
|
742
851
|
query : function(key, queryString) {
|
743
|
-
queryString = (queryString ||
|
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
|
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
|
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
|
-
|
772
|
-
|
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 ('
|
778
|
-
|
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 :
|
837
|
-
|
838
|
-
|
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 :
|
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 :
|
1035
|
+
throw_error : function(actual, expected) {
|
866
1036
|
try { actual() }
|
867
1037
|
catch (e) {
|
868
|
-
if (expected == undefined) return true
|
869
|
-
|
870
|
-
expected.test(e)
|
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 :
|
1047
|
+
have : function(actual, length, property) {
|
875
1048
|
return actual[property].length == length
|
876
|
-
}
|
1049
|
+
},
|
877
1050
|
|
878
|
-
have_at_least :
|
1051
|
+
have_at_least : function(actual, length, property) {
|
879
1052
|
return actual[property].length >= length
|
880
|
-
}
|
1053
|
+
},
|
881
1054
|
|
882
|
-
have_at_most :
|
1055
|
+
have_at_most :function(actual, length, property) {
|
883
1056
|
return actual[property].length <= length
|
884
|
-
}
|
1057
|
+
},
|
885
1058
|
|
886
|
-
have_within :
|
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 :
|
892
|
-
|
893
|
-
|
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 :
|
897
|
-
|
898
|
-
|
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
|