1
+ /* eslint-disable function-paren-newline */
2
+ /* eslint-disable one-var */
1
3
/**
2
4
* @fileOverview
3
5
*
4
6
* This module externally sets up the test runner on pm api. Essentially, it does not know the insides of pm-api and
5
7
* does the job completely from outside with minimal external dependency
6
8
*/
7
- const FUNCTION = 'function' ;
9
+ const _ = require ( 'lodash' ) ,
10
+ FUNCTION = 'function' ,
11
+ uuid = require ( '../vendor/uuid' ) ,
12
+
13
+ OPTIONS = {
14
+ When : 'when' ,
15
+ RunCount : 'runCount' ,
16
+ RunUntil : 'runUntil'
17
+ } ,
18
+ OPTION_TYPE = {
19
+ [ OPTIONS . When ] : 'function' ,
20
+ [ OPTIONS . RunCount ] : 'number' ,
21
+ [ OPTIONS . RunUntil ] : 'number'
22
+ } ;
8
23
9
24
/**
10
25
* @module {PMAPI~setupTestRunner}
11
26
* @private
12
27
*
13
28
* @param {PMAPI } pm - an instance of PM API that it needs
14
- * @param {Function } onAssertionComplete - is the trigger function that is called every time a test is executed and it
29
+ * @param {Object } testsState - State of all the tests for the current execution
30
+ * @param {Function } onAssertion - is the trigger function that is called every time a test is encountered and it
15
31
* receives the AssertionInfo object outlining details of the assertion
16
32
*/
17
- module . exports = function ( pm , onAssertionComplete ) {
33
+ module . exports = function ( pm , testsState , onAssertion ) {
18
34
var assertionIndex = 0 ,
19
35
20
36
/**
@@ -23,26 +39,71 @@ module.exports = function (pm, onAssertionComplete) {
23
39
* @note This is put in a function since this needs to be done from a number of place and having a single
24
40
* function reduces the chance of bugs
25
41
*
42
+ * @param {String } testId -
26
43
* @param {String } name -
27
44
* @param {Boolean } skipped -
28
45
*
29
46
* @returns {PMAPI~AssertionInfo }
30
47
*/
31
- getAssertionObject = function ( name , skipped ) {
48
+ getAssertionObject = function ( testId , name , skipped ) {
32
49
/**
33
50
* @typeDef {AssertionInfo}
34
51
* @private
35
52
*/
36
53
return {
54
+ testId : testId ,
37
55
name : String ( name ) ,
38
56
async : false ,
39
57
skipped : Boolean ( skipped ) ,
40
58
passed : true ,
59
+ pending : ! skipped ,
41
60
error : null ,
42
61
index : assertionIndex ++ // increment the assertion counter (do it before asserting)
43
62
} ;
44
63
} ,
45
64
65
+ generateTestId = function ( eventName , testName , assertFn , options ) {
66
+ return [
67
+ eventName ,
68
+ testName ,
69
+ assertFn ? assertFn . toString ( ) : '' ,
70
+ JSON . stringify ( options )
71
+ ] . join ( '' ) ;
72
+ } ,
73
+
74
+ getDefaultTestState = function ( options ) {
75
+ return {
76
+ ...( options ? _ . pick ( options , _ . values ( OPTIONS ) ) : { } ) ,
77
+ testId : uuid ( ) ,
78
+ timer : null ,
79
+ currRunCount : 0 ,
80
+ pending : true
81
+ } ;
82
+ } ,
83
+
84
+ isOptionConfigured = function ( options , optionName ) {
85
+ return _ . has ( options , optionName ) && typeof options [ optionName ] === OPTION_TYPE [ optionName ] ;
86
+ } ,
87
+
88
+
89
+ validateOptions = function ( options ) {
90
+ if ( ! options || typeof options !== 'object' ) {
91
+ throw new Error ( 'Invalid test option: options is not an object' ) ;
92
+ }
93
+
94
+ const supportedOptions = _ . values ( OPTIONS ) ;
95
+
96
+ Object . keys ( options ) . forEach ( ( optionName ) => {
97
+ if ( ! supportedOptions . includes ( optionName ) ) {
98
+ throw new Error ( `Invalid test option: ${ optionName } is not a supported option` ) ;
99
+ }
100
+
101
+ if ( typeof options [ optionName ] !== OPTION_TYPE [ optionName ] ) {
102
+ throw new Error ( `Invalid test options: ${ optionName } is not a ${ OPTION_TYPE [ optionName ] } ` ) ;
103
+ }
104
+ } ) ;
105
+ } ,
106
+
46
107
/**
47
108
* Simple function to mark an assertion as failed
48
109
*
@@ -57,59 +118,143 @@ module.exports = function (pm, onAssertionComplete) {
57
118
markAssertionAsFailure = function ( assertionData , err ) {
58
119
assertionData . error = err ;
59
120
assertionData . passed = false ;
121
+ } ,
122
+
123
+ processAssertion = function ( _testId , assertionData , options ) {
124
+ const testState = testsState [ _testId ] ;
125
+
126
+ if ( ! testState . pending ) {
127
+ return ;
128
+ }
129
+
130
+ const shouldResolve = Boolean (
131
+ assertionData . error || // TODO: Make conditions (test status) to mark a test resolved, configurable.
132
+ assertionData . skipped ||
133
+ _ . isEmpty ( options ) ||
134
+ ! testState ||
135
+ isOptionConfigured ( options , OPTIONS . RunCount ) && testState . runCount === testState . currRunCount ||
136
+ isOptionConfigured ( options , OPTIONS . RunUntil ) && ! testState . timer
137
+ ) ;
138
+
139
+ testState . pending = assertionData . pending = ! shouldResolve ;
140
+
141
+ // Tests without options does not need to be tracked
142
+ if ( _ . isEmpty ( options ) ) {
143
+ delete testsState [ _testId ] ;
144
+ }
145
+
146
+ onAssertion ( assertionData ) ;
147
+ } ,
148
+
149
+ processOptions = function ( _testId , assertionData , options ) {
150
+ const testState = testsState [ _testId ] ,
151
+ shouldRun = testState . pending &&
152
+ ( isOptionConfigured ( options , OPTIONS . When ) ? Boolean ( options . when ( ) ) : true ) &&
153
+ ( isOptionConfigured ( options , OPTIONS . RunCount ) ? testState . currRunCount < options . runCount : true ) ;
154
+
155
+ if ( shouldRun ) {
156
+ testState . currRunCount ++ ;
157
+
158
+ const startTimer = isOptionConfigured ( options , OPTIONS . RunUntil ) && ! testState . timer ;
159
+
160
+ if ( startTimer ) {
161
+ testState . timer = setTimeout ( ( ) => {
162
+ testState . timer = null ;
163
+ processAssertion ( _testId , assertionData , options ) ;
164
+ } , testState . runUntil ) ;
165
+ }
166
+ }
167
+
168
+ return shouldRun ;
60
169
} ;
61
170
62
171
/**
63
172
* @param {String } name -
173
+ * @param {Object } [options] -
64
174
* @param {Function } assert -
65
175
* @chainable
66
176
*/
67
- pm . test = function ( name , assert ) {
68
- var assertionData = getAssertionObject ( name , false ) ;
177
+ pm . test = function ( name , options , assert ) {
178
+ if ( typeof options === FUNCTION ) {
179
+ assert = options ;
180
+ options = { } ;
181
+ }
182
+
183
+ if ( _ . isNil ( options ) || typeof options !== 'object' ) {
184
+ options = { } ;
185
+ }
186
+
187
+ // TODO: Make generateTestId safe i.e handle invalid `options` as well
188
+ const _testId = generateTestId ( pm . info . eventName , name , assert , options ) ;
189
+
190
+ if ( ! testsState [ _testId ] ) {
191
+ testsState [ _testId ] = getDefaultTestState ( options ) ;
192
+ }
193
+
194
+ const testState = testsState [ _testId ] ,
195
+ testId = testState . testId ,
196
+ assertionData = getAssertionObject ( testId , name , false ) ;
69
197
70
198
// if there is no assertion function, we simply move on
71
199
if ( typeof assert !== FUNCTION ) {
72
- onAssertionComplete ( assertionData ) ;
200
+ // Sending `options` as empty to force resolve the test
201
+ processAssertion ( _testId , assertionData , { } ) ;
73
202
74
203
return pm ;
75
204
}
76
205
77
- // if a callback function was sent, then we know that the test is asynchronous
78
- if ( assert . length ) {
79
- try {
80
- assertionData . async = true ; // flag that this was an async test (would be useful later)
81
-
82
- // we execute assertion, but pass it a completion function, which, in turn, raises the completion
83
- // event. we do not need to worry about timers here since we are assuming that some timer within the
84
- // sandbox had actually been the source of async calls and would take care of this
85
- assert ( function ( err ) {
86
- // at first we double check that no synchronous error has happened from the catch block below
87
- if ( assertionData . error && assertionData . passed === false ) {
88
- return ;
89
- }
90
-
91
- // user triggered a failure of the assertion, so we mark it the same
92
- if ( err ) {
93
- markAssertionAsFailure ( assertionData , err ) ;
94
- }
95
-
96
- onAssertionComplete ( assertionData ) ;
97
- } ) ;
206
+ try { validateOptions ( options ) ; }
207
+ catch ( e ) {
208
+ markAssertionAsFailure ( assertionData , e ) ;
209
+ processAssertion ( _testId , assertionData , options ) ;
210
+
211
+ return pm ;
212
+ }
213
+
214
+
215
+ const shouldRun = processOptions ( _testId , assertionData , options ) ;
216
+
217
+ if ( shouldRun ) {
218
+ // if a callback function was sent, then we know that the test is asynchronous
219
+ if ( assert . length ) {
220
+ try {
221
+ assertionData . async = true ; // flag that this was an async test (would be useful later)
222
+
223
+ // we execute assertion, but pass it a completion function, which, in turn, raises the completion
224
+ // event. we do not need to worry about timers here since we are assuming that some timer within the
225
+ // sandbox had actually been the source of async calls and would take care of this
226
+ assert ( function ( err ) {
227
+ // at first we double check that no synchronous error has happened from the catch block below
228
+ if ( assertionData . error && assertionData . passed === false ) {
229
+ return ;
230
+ }
231
+
232
+ // user triggered a failure of the assertion, so we mark it the same
233
+ if ( err ) {
234
+ markAssertionAsFailure ( assertionData , err ) ;
235
+ }
236
+
237
+ processAssertion ( _testId , assertionData , options ) ;
238
+ } ) ;
239
+ }
240
+ // in case a synchronous error occurs in the the async assertion, we still bail out.
241
+ catch ( e ) {
242
+ markAssertionAsFailure ( assertionData , e ) ;
243
+ processAssertion ( _testId , assertionData , options ) ;
244
+ }
98
245
}
99
- // in case a synchronous error occurs in the the async assertion, we still bail out.
100
- catch ( e ) {
101
- markAssertionAsFailure ( assertionData , e ) ;
102
- onAssertionComplete ( assertionData ) ;
246
+ // if the assertion function does not expect a callback, we synchronously execute the same
247
+ else {
248
+ try { assert ( ) ; }
249
+ catch ( e ) {
250
+ markAssertionAsFailure ( assertionData , e ) ;
251
+ }
252
+
253
+ processAssertion ( _testId , assertionData , options ) ;
103
254
}
104
255
}
105
- // if the assertion function does not expect a callback, we synchronously execute the same
106
256
else {
107
- try { assert ( ) ; }
108
- catch ( e ) {
109
- markAssertionAsFailure ( assertionData , e ) ;
110
- }
111
-
112
- onAssertionComplete ( assertionData ) ;
257
+ processAssertion ( _testId , assertionData , options ) ;
113
258
}
114
259
115
260
return pm ; // make it chainable
@@ -121,7 +266,7 @@ module.exports = function (pm, onAssertionComplete) {
121
266
*/
122
267
pm . test . skip = function ( name ) {
123
268
// trigger the assertion events with skips
124
- onAssertionComplete ( getAssertionObject ( name , true ) ) ;
269
+ processAssertion ( name , getAssertionObject ( uuid ( ) , name , true ) , { } ) ;
125
270
126
271
return pm ; // chainable
127
272
} ;
0 commit comments