Skip to content

Commit 64dc679

Browse files
committed
Add support for asynchronous tests with pending state.
1 parent f1665e5 commit 64dc679

File tree

3 files changed

+193
-44
lines changed

3 files changed

+193
-44
lines changed

lib/sandbox/execute.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ module.exports = function (bridge, glob) {
3434
// For caching required information provided during
3535
// initialization which will be used during execution
3636
let initializationOptions = {},
37-
initializeExecution;
37+
initializeExecution,
38+
39+
// Tests state in the context of the current execution
40+
testsState = {};
3841

3942
/**
4043
* @param {Object} options
@@ -214,7 +217,7 @@ module.exports = function (bridge, glob) {
214217
var eventId = timers.setEvent(callback);
215218

216219
bridge.dispatch(executionRequestEventName, options.cursor, id, eventId, request);
217-
}, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), {
220+
}, testsState, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), {
218221
disabledAPIs: initializationOptions.disabledAPIs
219222
})
220223
),

lib/sandbox/pmapi-setup-runner.js

Lines changed: 185 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1+
/* eslint-disable function-paren-newline */
2+
/* eslint-disable one-var */
13
/**
24
* @fileOverview
35
*
46
* This module externally sets up the test runner on pm api. Essentially, it does not know the insides of pm-api and
57
* does the job completely from outside with minimal external dependency
68
*/
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+
};
823

924
/**
1025
* @module {PMAPI~setupTestRunner}
1126
* @private
1227
*
1328
* @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
1531
* receives the AssertionInfo object outlining details of the assertion
1632
*/
17-
module.exports = function (pm, onAssertionComplete) {
33+
module.exports = function (pm, testsState, onAssertion) {
1834
var assertionIndex = 0,
1935

2036
/**
@@ -23,26 +39,71 @@ module.exports = function (pm, onAssertionComplete) {
2339
* @note This is put in a function since this needs to be done from a number of place and having a single
2440
* function reduces the chance of bugs
2541
*
42+
* @param {String} testId -
2643
* @param {String} name -
2744
* @param {Boolean} skipped -
2845
*
2946
* @returns {PMAPI~AssertionInfo}
3047
*/
31-
getAssertionObject = function (name, skipped) {
48+
getAssertionObject = function (testId, name, skipped) {
3249
/**
3350
* @typeDef {AssertionInfo}
3451
* @private
3552
*/
3653
return {
54+
testId: testId,
3755
name: String(name),
3856
async: false,
3957
skipped: Boolean(skipped),
4058
passed: true,
59+
pending: !skipped,
4160
error: null,
4261
index: assertionIndex++ // increment the assertion counter (do it before asserting)
4362
};
4463
},
4564

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+
46107
/**
47108
* Simple function to mark an assertion as failed
48109
*
@@ -57,59 +118,143 @@ module.exports = function (pm, onAssertionComplete) {
57118
markAssertionAsFailure = function (assertionData, err) {
58119
assertionData.error = err;
59120
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;
60169
};
61170

62171
/**
63172
* @param {String} name -
173+
* @param {Object} [options] -
64174
* @param {Function} assert -
65175
* @chainable
66176
*/
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);
69197

70198
// if there is no assertion function, we simply move on
71199
if (typeof assert !== FUNCTION) {
72-
onAssertionComplete(assertionData);
200+
// Sending `options` as empty to force resolve the test
201+
processAssertion(_testId, assertionData, {});
73202

74203
return pm;
75204
}
76205

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+
}
98245
}
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);
103254
}
104255
}
105-
// if the assertion function does not expect a callback, we synchronously execute the same
106256
else {
107-
try { assert(); }
108-
catch (e) {
109-
markAssertionAsFailure(assertionData, e);
110-
}
111-
112-
onAssertionComplete(assertionData);
257+
processAssertion(_testId, assertionData, options);
113258
}
114259

115260
return pm; // make it chainable
@@ -121,7 +266,7 @@ module.exports = function (pm, onAssertionComplete) {
121266
*/
122267
pm.test.skip = function (name) {
123268
// trigger the assertion events with skips
124-
onAssertionComplete(getAssertionObject(name, true));
269+
processAssertion(name, getAssertionObject(uuid(), name, true), {});
125270

126271
return pm; // chainable
127272
};

lib/sandbox/pmapi.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ const _ = require('lodash'),
4444
*
4545
* @param {Execution} execution -
4646
* @param {Function} onRequest -
47+
* @param {Object} testsState -
4748
* @param {Function} onAssertion -
4849
* @param {Object} cookieStore -
4950
* @param {Object} [options] -
5051
* @param {Array.<String>} [options.disabledAPIs] -
5152
*/
52-
function Postman (execution, onRequest, onAssertion, cookieStore, options = {}) {
53+
function Postman (execution, onRequest, testsState, onAssertion, cookieStore, options = {}) {
5354
// @todo - ensure runtime passes data in a scope format
5455
let iterationData = new VariableScope();
5556

@@ -254,7 +255,7 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {})
254255
}, options.disabledAPIs);
255256

256257
// extend pm api with test runner abilities
257-
setupTestRunner(this, onAssertion);
258+
setupTestRunner(this, testsState, onAssertion);
258259

259260
// add response assertions
260261
if (this.response) {

0 commit comments

Comments
 (0)