Skip to content

Commit 9ced31d

Browse files
committed
feat(SELENIUM_PROMISE_MANAGER=0): Improve support for SELENIUM_PROMISE_MANAGER=0
There are three major ways this was done in this change: * In `callWhenIdle`, if `flow.isIdle` is not defined, we assume we are working with a `SimpleScheduler` instance, and so the flow is effectively idle. * In `initJasmineWd`, if `flow.reset` is not defined, we assume we are working with a `SimpleScheduler` instance, and so don't bother resetting the flow. * In `wrapInControlFlow`, we use `flow.promise` to create a new promise if possible. Since `new webdriver.promise.Promise()` would have always made a `ManagedPromise`, but `flow.promise` will do the right thing. * In `wrapCompare`, we avoid the webdriver library entirely, and never instance any extra promises. Using `webdriver.promise.when` and `webdriver.promise.all` could have been a problem if our instance of `webdriver` had the control flow turned on, but another instance somewhere did not (or even the same instance, but just at a different point in time). Instead we use the new `maybePromise` tool, which is a mess but is also exactly what we want. * In `specs/*`, we replace `webdriver.promise.fulfilled` with `webdriver.promise.when`. * In `specs/*`, a new version of `adapterSpec.js` and `errorSpec.js` are created: `asyncAwaitAdapterSpec.ts` and `asyncAwaitErrorSpec.ts`. I also also fixed a minor bug where we weren't correctly checking for promises inside an array of expected results. Before we had ```js expected = Array.prototype.slice.call(arguments, 0) ... webdriver.promise.isPromise(expected) ``` I thought about it for a little while, and there's no way that's correct. `expected` is an `Array<any>`, there's no way it has a `.then` function. Closes #69
1 parent f0d0f06 commit 9ced31d

21 files changed

+841
-106
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.log
22
node_modules
3-
spec/asyncAwaitSpec.js
3+
spec/asyncAwait*Spec.js
4+
spec/common.js

.jshintignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
./spec/asyncAwaitSpec.js
1+
./spec/asyncAwaitAdapterSpec.js
2+
./spec/asyncAwaitErrorSpec.js
3+
./spec/common.js

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@ available via several compilers. At the moment, they often break the WebDriver
6969
control flow.
7070
([GitHub issue](https://github.com/SeleniumHQ/selenium/issues/3037)). You can
7171
still use them, but if you do then you will have to use `await`/Promises for
72-
almost all your synchronization. See `spec/asyncAwaitSpec.ts` for details.
72+
almost all your synchronization. See `spec/asyncAwaitAdapterSpec.ts` and
73+
`spec/asyncAwaitErrorSpec.ts` for examples.

index.js

+36-33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
var webdriver = require('selenium-webdriver');
8+
var maybePromise = require('./maybePromise');
89

910
/**
1011
* Validates that the parameter is a function.
@@ -54,7 +55,7 @@ function validateString(stringtoValidate) {
5455
* @param {!Function} fn The function to call
5556
*/
5657
function callWhenIdle(flow, fn) {
57-
if (flow.isIdle()) {
58+
if (!flow.isIdle || flow.isIdle()) {
5859
fn();
5960
} else {
6061
flow.once(webdriver.promise.ControlFlow.EventType.IDLE, function() {
@@ -84,7 +85,14 @@ function wrapInControlFlow(flow, globalFn, fnName) {
8485
var testFn = fn.bind(this);
8586

8687
flow.execute(function controlFlowExecute() {
87-
return new webdriver.promise.Promise(function(fulfill, reject) {
88+
function newPromise(resolver) {
89+
if (typeof flow.promise == 'function') {
90+
return flow.promise(resolver);
91+
} else {
92+
return new webdriver.promise.Promise(resolver, flow);
93+
}
94+
}
95+
return newPromise(function(fulfill, reject) {
8896
function wrappedReject(err) {
8997
var wrappedErr = new Error(err);
9098
reject(wrappedErr);
@@ -106,7 +114,7 @@ function wrapInControlFlow(flow, globalFn, fnName) {
106114
fulfill(ret);
107115
}
108116
}
109-
}, flow);
117+
});
110118
}, 'Run ' + fnName + description + ' in control flow').then(
111119
callWhenIdle.bind(null, flow, done), function(err) {
112120
if (!err) {
@@ -173,15 +181,17 @@ function initJasmineWd(flow) {
173181
global.beforeAll = wrapInControlFlow(flow, global.beforeAll, 'beforeAll');
174182
global.afterAll = wrapInControlFlow(flow, global.afterAll, 'afterAll');
175183

176-
// On timeout, the flow should be reset. This will prevent webdriver tasks
177-
// from overflowing into the next test and causing it to fail or timeout
178-
// as well. This is done in the reporter instead of an afterEach block
179-
// to ensure that it runs after any afterEach() blocks with webdriver tasks
180-
// get to complete first.
181-
jasmine.getEnv().addReporter(new OnTimeoutReporter(function() {
182-
console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.');
183-
flow.reset();
184-
}));
184+
if (flow.reset) {
185+
// On timeout, the flow should be reset. This will prevent webdriver tasks
186+
// from overflowing into the next test and causing it to fail or timeout
187+
// as well. This is done in the reporter instead of an afterEach block
188+
// to ensure that it runs after any afterEach() blocks with webdriver tasks
189+
// get to complete first.
190+
jasmine.getEnv().addReporter(new OnTimeoutReporter(function() {
191+
console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.');
192+
flow.reset();
193+
}));
194+
}
185195
}
186196

187197
var originalExpect = global.expect;
@@ -196,6 +206,10 @@ global.expect = function(actual) {
196206
/**
197207
* Creates a matcher wrapper that resolves any promises given for actual and
198208
* expected values, as well as the `pass` property of the result.
209+
*
210+
* Wrapped matchers will return either `undefined` or a promise which resolves
211+
* when the matcher is complete, depending on if the matcher had to resolve any
212+
* promises.
199213
*/
200214
jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
201215
return function() {
@@ -205,16 +219,12 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
205219

206220
matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, '');
207221

208-
if (!webdriver.promise.isPromise(expectation.actual) &&
209-
!webdriver.promise.isPromise(expected)) {
210-
compare(expectation.actual, expected);
211-
} else {
212-
webdriver.promise.when(expectation.actual).then(function(actual) {
213-
return webdriver.promise.all(expected).then(function(expected) {
214-
return compare(actual, expected);
215-
});
222+
// Return either undefined or a promise of undefined
223+
return maybePromise(expectation.actual, function(actual) {
224+
return maybePromise.all(expected, function(expected) {
225+
return compare(actual, expected);
216226
});
217-
}
227+
});
218228

219229
function compare(actual, expected) {
220230
var args = expected.slice(0);
@@ -229,12 +239,9 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
229239

230240
var result = matcherCompare.apply(null, args);
231241

232-
if (webdriver.promise.isPromise(result.pass)) {
233-
return webdriver.promise.when(result.pass).then(compareDone);
234-
} else {
235-
return compareDone(result.pass);
236-
}
242+
return maybePromise(result.pass, compareDone);
237243

244+
// compareDone always returns undefined
238245
function compareDone(pass) {
239246
var message = '';
240247

@@ -268,13 +275,9 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) {
268275

269276
function defaultNegativeCompare() {
270277
var result = matcher.compare.apply(null, args);
271-
if (webdriver.promise.isPromise(result.pass)) {
272-
result.pass = result.pass.then(function(pass) {
273-
return !pass;
274-
});
275-
} else {
276-
result.pass = !result.pass;
277-
}
278+
result.pass = maybePromise(result.pass, function(pass) {
279+
return !pass;
280+
});
278281
return result;
279282
}
280283
}

maybePromise.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* This file implements jasminewd's peculiar alternatives to Promise.resolve()
3+
* and Promise.all(). Do not use the code from this file as pollyfill for
4+
* Promise.resolve() or Promise.all(). There are a number of reasons why this
5+
* implementation will cause unexpected errors in most codebases.
6+
*
7+
* Called "maybePromise" because both the parameters and the return values may
8+
* or may not be promises, and code execution may or may not be synchronous.
9+
*/
10+
11+
/**
12+
* Runs a callback synchronously against non-promise values and asynchronously
13+
* against promises. Similar to ES6's `Promise.resolve` except that it is
14+
* synchronous when possible and won't wrap the return value.
15+
*
16+
* This is not what you normally want. Normally you want the code to be
17+
* consistently asynchronous, and you want the result wrapped into a promise.
18+
* But because of webdriver's control flow, we're better off not introducing any
19+
* extra layers of promises or asynchronous activity.
20+
*
21+
* @param {*} val The value to call the callback with.
22+
* @param {!Function} callback The callback function
23+
* @return {*} If val isn't a promise, the return value of the callback is
24+
* directly returned. If val is a promise, a promise (generated by val.then)
25+
* resolving to the callback's return value is returned.
26+
*/
27+
var maybePromise = module.exports = function maybePromise(val, callback) {
28+
if (val && (typeof val.then == 'function')) {
29+
return val.then(callback);
30+
} else {
31+
return callback(val);
32+
}
33+
}
34+
35+
/**
36+
* Like maybePromise() but for an array of values. Analogous to `Promise.all`.
37+
*
38+
* @param {!Array<*>} vals An array of values to call the callback with
39+
* @param {!Function} callback the callback function
40+
* @return {*} If nothing in vals is a promise, the return value of the callback
41+
* is directly returned. Otherwise, a promise (generated by the .then
42+
* functions in vals) resolving to the callback's return value is returned.
43+
*/
44+
maybePromise.all = function all(vals, callback) {
45+
var resolved = new Array(vals.length);
46+
function resolveAt(i) {
47+
if (i >= vals.length) {
48+
return callback(resolved);
49+
} else {
50+
return maybePromise(vals[i], function(val) {
51+
resolved[i] = val;
52+
return resolveAt(i+1);
53+
});
54+
}
55+
}
56+
return resolveAt(0);
57+
}
58+

package.json

+13-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616
"selenium-webdriver": "3.0.1"
1717
},
1818
"devDependencies": {
19+
"@types/node": "^6.0.56",
20+
"@types/selenium-webdriver": "^2.53.38",
21+
"jasmine": "2.4.1",
1922
"jshint": "^2.9.4",
20-
"typescript": "^2.0.10"
23+
"selenium-webdriver": "2.53.3",
24+
"tslint": "^4.2.0",
25+
"tslint-eslint-rules": "^3.2.3",
26+
"typescript": "^2.0.10",
27+
"vrsource-tslint-rules": "^4.0.0"
2128
},
2229
"repository": {
2330
"type": "git",
@@ -26,8 +33,11 @@
2633
"main": "index.js",
2734
"scripts": {
2835
"jshint": "jshint index.js spec",
29-
"tsc": "tsc -t ES2015 spec/asyncAwaitSpec.ts",
30-
"pretest": "npm run jshint && npm run tsc",
36+
"tslint": "tslint spec/*.ts",
37+
"lint": "npm run jshint && npm run tslint",
38+
"tsc": "tsc",
39+
"clean": "rm spec/asyncAwait*Spec.js spec/common.js",
40+
"pretest": "npm run lint && npm run tsc",
3141
"test": "scripts/test.sh"
3242
},
3343
"license": "MIT",

scripts/test.sh

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,59 @@
1+
LIB_SPECS="spec/support/lib_specs.json"
12
PASSING_SPECS="spec/support/passing_specs.json"
23
FAILING_SPECS="spec/support/failing_specs.json"
4+
NO_CF_PASSING_SPECS="spec/support/no_cf_passing_specs.json"
5+
NO_CF_FAILING_SPECS="spec/support/no_cf_failing_specs.json"
36
CMD_BASE="node node_modules/.bin/jasmine JASMINE_CONFIG_PATH="
47

5-
echo "### running passing specs"
8+
# Run unit tests
9+
10+
echo "### running all unit tests"
11+
CMD=$CMD_BASE$LIB_SPECS
12+
echo "### $CMD"
13+
$CMD
14+
[ "$?" -eq 0 ] || exit 1
15+
echo
16+
17+
18+
# Run all tests when the control flow is enabled
19+
20+
export SELENIUM_PROMISE_MANAGER=1
21+
22+
echo "### running all passing specs"
623
CMD=$CMD_BASE$PASSING_SPECS
724
echo "### $CMD"
825
$CMD
926
[ "$?" -eq 0 ] || exit 1
1027
echo
1128

12-
EXPECTED_RESULTS="18 specs, 16 failures"
13-
echo "### running failing specs (expecting $EXPECTED_RESULTS)"
29+
EXPECTED_RESULTS="38 specs, 34 failures"
30+
echo "### running all failing specs (expecting $EXPECTED_RESULTS)"
1431
CMD=$CMD_BASE$FAILING_SPECS
1532
echo "### $CMD"
1633
res=`$CMD 2>/dev/null`
1734
results_line=`echo "$res" | tail -2 | head -1`
1835
echo "result: $results_line"
1936
[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1
2037

38+
# Run only the async/await tests when the control flow is disabled
39+
40+
export SELENIUM_PROMISE_MANAGER=0
41+
42+
echo "### running async/await passing specs"
43+
CMD=$CMD_BASE$NO_CF_PASSING_SPECS
44+
echo "### $CMD"
45+
$CMD
46+
[ "$?" -eq 0 ] || exit 1
47+
echo
48+
49+
EXPECTED_RESULTS="19 specs, 17 failures"
50+
echo "### running async/await failing specs (expecting $EXPECTED_RESULTS)"
51+
CMD=$CMD_BASE$NO_CF_FAILING_SPECS
52+
echo "### $CMD"
53+
res=`$CMD 2>/dev/null`
54+
results_line=`echo "$res" | tail -2 | head -1`
55+
echo "result: $results_line"
56+
[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1
57+
58+
2159
echo "all pass"

spec/adapterSpec.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('webdriverJS Jasmine adapter', function() {
138138

139139
fakeDriver.getValueList().then(function(list) {
140140
var result = list.map(function(webElem) {
141-
var webElemsPromise = webdriver.promise.fulfilled(webElem).then(function(webElem) {
141+
var webElemsPromise = webdriver.promise.when(webElem).then(function(webElem) {
142142
return [webElem];
143143
});
144144
return webdriver.promise.fullyResolved(checkTexts(webElemsPromise));
@@ -244,6 +244,12 @@ describe('webdriverJS Jasmine adapter', function() {
244244
});
245245

246246
describe('native promises', function() {
247+
it('should have done argument override return returned promise', function(done) {
248+
var ret = new Promise(function() {});
249+
done();
250+
return ret;
251+
});
252+
247253
var currentTest = null;
248254

249255
it('should wait for webdriver events sent from native promise', function() {

0 commit comments

Comments
 (0)