1 | {util, deferred, parallel} = require 'also'
|
2 | {AssertionError} = require 'assert'
|
3 | facto = require 'facto'
|
4 | Loader = require './loader'
|
5 | colors = require 'colors'
|
6 | Does = require 'does'
|
7 | does = Does does: mode: 'spec'
|
8 | should = require 'should'
|
9 |
|
10 | config =
|
11 |
|
12 | #
|
13 | # **ipso should be run in repo root**
|
14 | #
|
15 |
|
16 | dir: process.cwd()
|
17 | modules: {}
|
18 |
|
19 | {loadModules, loadModulesSync} = Loader.create config
|
20 |
|
21 | #
|
22 | # `ipso( testFunction )` - Decorates a test function
|
23 | # --------------------------------------------------
|
24 | #
|
25 | # * All ipso tests are asynchronous - done is called in the background on the nextTick
|
26 | # if the testFunction signature itself did not contain 'done' at argument1
|
27 | #
|
28 |
|
29 | module.exports = ipso = (actualTestFunction) ->
|
30 |
|
31 | return testFunctionForMocha = (done) ->
|
32 |
|
33 | fnArgsArray = util.argsOf actualTestFunction
|
34 |
|
35 | argsToInjectIntoTest = []
|
36 |
|
37 | unless done?
|
38 |
|
39 | #
|
40 | # ### Injecting into describe() or context()
|
41 | #
|
42 |
|
43 | if fnArgsArray[0] is 'done' or fnArgsArray[0] is 'facto'
|
44 |
|
45 | console.log 'ipso cannot inject done into describe() or context()'.red
|
46 | return
|
47 |
|
48 | does.activate context: @, mode: 'spec', spec: null, resolver: null
|
49 | argsToInjectIntoTest.push Module for Module in loadModulesSync( fnArgsArray, does )
|
50 | actualTestFunction.apply @, argsToInjectIntoTest
|
51 | return
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | #
|
57 | # ### Injecting into hook or it()
|
58 | #
|
59 |
|
60 | does.activate context: @, mode: 'spec', spec: @test, resolver: done
|
61 |
|
62 |
|
63 | #
|
64 | # * testResolver wraps mocha's done into a proxy that call it via
|
65 | # does.asset(... for function expectations that mocha is not aware of.
|
66 | #
|
67 |
|
68 | testResolver = (metadata) ->
|
69 |
|
70 | does.assert( done ).then(
|
71 |
|
72 | (result) ->
|
73 |
|
74 | #
|
75 | # * does.assert(... does not call done if nothing failed
|
76 | #
|
77 |
|
78 | if fnArgsArray[0] is 'facto' then facto metadata
|
79 | done()
|
80 |
|
81 |
|
82 | (error) ->
|
83 |
|
84 | #
|
85 | # * does.assert(... already called done - to fail the mocha test
|
86 | #
|
87 |
|
88 | if fnArgsArray[0] is 'facto' then facto metadata
|
89 |
|
90 | (notify) ->
|
91 |
|
92 | #
|
93 | # * later...
|
94 | #
|
95 |
|
96 | )
|
97 |
|
98 | #
|
99 | # * testResolver is only injected if arg1 is done or facto
|
100 | #
|
101 |
|
102 | if fnArgsArray[0] is 'done' or fnArgsArray[0] is 'facto'
|
103 |
|
104 | argsToInjectIntoTest.push testResolver
|
105 | arg1 = fnArgsArray.shift()
|
106 |
|
107 | loadModules( fnArgsArray, does ).then(
|
108 |
|
109 | #
|
110 | # * loader resolved with list of Modules refs to inject
|
111 | #
|
112 |
|
113 | (Modules) =>
|
114 |
|
115 | argsToInjectIntoTest.push Module for Module in Modules
|
116 |
|
117 | try promise = actualTestFunction.apply @, argsToInjectIntoTest
|
118 | catch error
|
119 |
|
120 | does.reset().then -> done error
|
121 | return
|
122 |
|
123 | if arg1 isnt 'done' and arg1 isnt 'facto'
|
124 |
|
125 | #
|
126 | # * test did not "request" done or facto (ie. synchronous)
|
127 | # but this test wrapper got a done from mocha, it needs
|
128 | # to be called.
|
129 | #
|
130 |
|
131 | try if promise.then? and @test.type is 'test'
|
132 | return does.reset().then ->
|
133 | done new Error 'Synchronous test returned promise. Inject test resolver (done or facto).'
|
134 |
|
135 |
|
136 | testResolver()
|
137 | return
|
138 |
|
139 | #
|
140 | # * redirect AssertionError being raised in a promise chain
|
141 | # back into mocha's test resolver
|
142 | #
|
143 |
|
144 | try if promise.then? then promise.then (->), (error) ->
|
145 |
|
146 | does.reset().then -> done error
|
147 |
|
148 |
|
149 | #
|
150 | # * loader rejection into done() - error loading module
|
151 | #
|
152 |
|
153 | (error) ->
|
154 |
|
155 | does.reset().then -> done error
|
156 |
|
157 |
|
158 | )
|
159 |
|
160 |
|
161 | #
|
162 | # convenience {ipso, mock, tag} = require 'ipso'
|
163 | #
|
164 |
|
165 | ipso.ipso = ipso
|
166 | ipso.mock = (name) ->
|
167 |
|
168 | object =
|
169 | title: name
|
170 | is: (mock) ->
|
171 | if typeof mock is 'object' then return object.should.equal mock
|
172 | name.should.equal mock
|
173 |
|
174 | #
|
175 | # experiment - may become property expetations
|
176 | #
|
177 |
|
178 | with: (list) ->
|
179 |
|
180 | object[key] = list[key] for key of list
|
181 | return object
|
182 |
|
183 | #
|
184 | # TODO: tagged?
|
185 | #
|
186 |
|
187 | return does.spectateSync name: name, tagged: true, object
|
188 |
|
189 |
|
190 | ipso.Mock = (name) ->
|
191 |
|
192 | #
|
193 | # Mock() (with capital M) mocks a class definition.
|
194 | #
|
195 | # !!EXPERIMENT!!, pending a properly implemented method
|
196 | # that stubs the prototype
|
197 | #
|
198 | #
|
199 |
|
200 | #
|
201 | # * create the mock for injection into subsequent hooks and
|
202 | # tests where .does() can be called upon to create future
|
203 | # instanceMethod expectations.
|
204 | #
|
205 |
|
206 | mockObject = ipso.mock name
|
207 |
|
208 | return klass = class
|
209 |
|
210 | #
|
211 | # * with() as class method to configure the base set
|
212 | # of function and property stubs for each future
|
213 | # instance of the class
|
214 | #
|
215 |
|
216 | @with = ->
|
217 |
|
218 | mockObject.with.apply @, arguments
|
219 | return klass
|
220 |
|
221 | #
|
222 | # assemble instance from current stub set
|
223 | # ---------------------------------------
|
224 | #
|
225 | # * will contain stubs (properties and functions) as set
|
226 | # up in the mock().with()
|
227 | #
|
228 | # * will also includes further ammendments created with
|
229 | # .does() in subsequent hooks (or in the test itself)
|
230 | #
|
231 |
|
232 | constructor: ->
|
233 |
|
234 | stubs = does.getSync(name).object
|
235 |
|
236 | #
|
237 | # * run the special case constructor spy if present
|
238 | #
|
239 |
|
240 | if typeof stubs.$constructor is 'function'
|
241 |
|
242 | stubs.$constructor.apply @, arguments
|
243 |
|
244 | #
|
245 | # * create the object properties and function from
|
246 | # the current contents of the mock stub set.
|
247 | #
|
248 |
|
249 | for stub of stubs
|
250 |
|
251 | continue if stub is '$constructor'
|
252 | @[stub] = stubs[stub]
|
253 |
|
254 |
|
255 |
|
256 | ipso.tag = deferred (action, list) ->
|
257 |
|
258 | #
|
259 | # not necessary to carry the promise, this is a synchronous call
|
260 | # but remains potentially async for future use
|
261 | #
|
262 |
|
263 | parallel( for tag of list
|
264 |
|
265 | do (tag) -> -> does.spectateSync
|
266 |
|
267 | name: tag
|
268 | tagged: true
|
269 | list[tag]
|
270 |
|
271 | ).then action.resolve, action.reject, action.notify
|
272 |
|
273 |
|
274 | ipso.define = require './define'
|
275 |
|
276 |
|
277 | ipso.does = does
|
278 |
|
279 |
|
280 | module.exports.once = (fn) -> do (done = false) -> ->
|
281 |
|
282 | return if done
|
283 | done = true
|
284 | fn.apply @, arguments
|
285 |
|
286 |
|