visionmedia-jspec 1.1.3 → 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|