From ebe26e23316adb98d67d4b9150cc2dfed93b09d9 Mon Sep 17 00:00:00 2001 From: Sammy Jelin Date: Fri, 16 Dec 2016 22:53:26 -0800 Subject: [PATCH] feat(SELENIUM_PROMISE_MANAGER): Support `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`, there's no way it has a `.then` function. Closes https://github.com/angular/jasminewd/issues/69 --- .gitignore | 3 +- .jshintignore | 4 +- README.md | 3 +- index.js | 69 +++--- maybePromise.js | 58 +++++ package.json | 16 +- scripts/test.sh | 44 +++- spec/adapterSpec.js | 8 +- spec/asyncAwaitAdapterSpec.ts | 294 ++++++++++++++++++++++++++ spec/asyncAwaitErrorSpec.ts | 151 +++++++++++++ spec/asyncAwaitSpec.ts | 34 --- spec/{common.js => common.ts} | 52 ++--- spec/errorSpec.js | 4 + spec/maybePromiseSpec.js | 155 ++++++++++++++ spec/support/failing_specs.json | 2 +- spec/support/lib_specs.json | 6 + spec/support/no_cf_failing_specs.json | 6 + spec/support/no_cf_passing_specs.json | 6 + spec/support/passing_specs.json | 3 +- tsconfig.json | 14 ++ tslint.json | 15 ++ 21 files changed, 841 insertions(+), 106 deletions(-) create mode 100644 maybePromise.js create mode 100644 spec/asyncAwaitAdapterSpec.ts create mode 100644 spec/asyncAwaitErrorSpec.ts delete mode 100644 spec/asyncAwaitSpec.ts rename spec/{common.js => common.ts} (60%) create mode 100644 spec/maybePromiseSpec.js create mode 100644 spec/support/lib_specs.json create mode 100644 spec/support/no_cf_failing_specs.json create mode 100644 spec/support/no_cf_passing_specs.json create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore index 77dfc75..5f64002 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.log node_modules -spec/asyncAwaitSpec.js +spec/asyncAwait*Spec.js +spec/common.js diff --git a/.jshintignore b/.jshintignore index 4979ff3..b358848 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1 +1,3 @@ -./spec/asyncAwaitSpec.js +./spec/asyncAwaitAdapterSpec.js +./spec/asyncAwaitErrorSpec.js +./spec/common.js diff --git a/README.md b/README.md index 3c4f603..a27d4ed 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,5 @@ available via several compilers. At the moment, they often break the WebDriver control flow. ([GitHub issue](https://github.com/SeleniumHQ/selenium/issues/3037)). You can still use them, but if you do then you will have to use `await`/Promises for -almost all your synchronization. See `spec/asyncAwaitSpec.ts` for details. +almost all your synchronization. See `spec/asyncAwaitAdapterSpec.ts` and +`spec/asyncAwaitErrorSpec.ts` for examples. diff --git a/index.js b/index.js index ab1b10a..5875a0f 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ */ var webdriver = require('selenium-webdriver'); +var maybePromise = require('./maybePromise'); /** * Validates that the parameter is a function. @@ -54,7 +55,7 @@ function validateString(stringtoValidate) { * @param {!Function} fn The function to call */ function callWhenIdle(flow, fn) { - if (flow.isIdle()) { + if (!flow.isIdle || flow.isIdle()) { fn(); } else { flow.once(webdriver.promise.ControlFlow.EventType.IDLE, function() { @@ -84,7 +85,14 @@ function wrapInControlFlow(flow, globalFn, fnName) { var testFn = fn.bind(this); flow.execute(function controlFlowExecute() { - return new webdriver.promise.Promise(function(fulfill, reject) { + function newPromise(resolver) { + if (typeof flow.promise == 'function') { + return flow.promise(resolver); + } else { + return new webdriver.promise.Promise(resolver, flow); + } + } + return newPromise(function(fulfill, reject) { function wrappedReject(err) { var wrappedErr = new Error(err); reject(wrappedErr); @@ -106,7 +114,7 @@ function wrapInControlFlow(flow, globalFn, fnName) { fulfill(ret); } } - }, flow); + }); }, 'Run ' + fnName + description + ' in control flow').then( callWhenIdle.bind(null, flow, done), function(err) { if (!err) { @@ -173,15 +181,17 @@ function initJasmineWd(flow) { global.beforeAll = wrapInControlFlow(flow, global.beforeAll, 'beforeAll'); global.afterAll = wrapInControlFlow(flow, global.afterAll, 'afterAll'); - // On timeout, the flow should be reset. This will prevent webdriver tasks - // from overflowing into the next test and causing it to fail or timeout - // as well. This is done in the reporter instead of an afterEach block - // to ensure that it runs after any afterEach() blocks with webdriver tasks - // get to complete first. - jasmine.getEnv().addReporter(new OnTimeoutReporter(function() { - console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.'); - flow.reset(); - })); + if (flow.reset) { + // On timeout, the flow should be reset. This will prevent webdriver tasks + // from overflowing into the next test and causing it to fail or timeout + // as well. This is done in the reporter instead of an afterEach block + // to ensure that it runs after any afterEach() blocks with webdriver tasks + // get to complete first. + jasmine.getEnv().addReporter(new OnTimeoutReporter(function() { + console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.'); + flow.reset(); + })); + } } var originalExpect = global.expect; @@ -196,6 +206,10 @@ global.expect = function(actual) { /** * Creates a matcher wrapper that resolves any promises given for actual and * expected values, as well as the `pass` property of the result. + * + * Wrapped matchers will return either `undefined` or a promise which resolves + * when the matcher is complete, depending on if the matcher had to resolve any + * promises. */ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) { return function() { @@ -205,16 +219,12 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) { matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, ''); - if (!webdriver.promise.isPromise(expectation.actual) && - !webdriver.promise.isPromise(expected)) { - compare(expectation.actual, expected); - } else { - webdriver.promise.when(expectation.actual).then(function(actual) { - return webdriver.promise.all(expected).then(function(expected) { - return compare(actual, expected); - }); + // Return either undefined or a promise of undefined + return maybePromise(expectation.actual, function(actual) { + return maybePromise.all(expected, function(expected) { + return compare(actual, expected); }); - } + }); function compare(actual, expected) { var args = expected.slice(0); @@ -229,12 +239,9 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) { var result = matcherCompare.apply(null, args); - if (webdriver.promise.isPromise(result.pass)) { - return webdriver.promise.when(result.pass).then(compareDone); - } else { - return compareDone(result.pass); - } + return maybePromise(result.pass, compareDone); + // compareDone always returns undefined function compareDone(pass) { var message = ''; @@ -268,13 +275,9 @@ jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) { function defaultNegativeCompare() { var result = matcher.compare.apply(null, args); - if (webdriver.promise.isPromise(result.pass)) { - result.pass = result.pass.then(function(pass) { - return !pass; - }); - } else { - result.pass = !result.pass; - } + result.pass = maybePromise(result.pass, function(pass) { + return !pass; + }); return result; } } diff --git a/maybePromise.js b/maybePromise.js new file mode 100644 index 0000000..48d4d95 --- /dev/null +++ b/maybePromise.js @@ -0,0 +1,58 @@ +/** + * This file implements jasminewd's peculiar alternatives to Promise.resolve() + * and Promise.all(). Do not use the code from this file as pollyfill for + * Promise.resolve() or Promise.all(). There are a number of reasons why this + * implementation will cause unexpected errors in most codebases. + * + * Called "maybePromise" because both the parameters and the return values may + * or may not be promises, and code execution may or may not be synchronous. + */ + +/** + * Runs a callback synchronously against non-promise values and asynchronously + * against promises. Similar to ES6's `Promise.resolve` except that it is + * synchronous when possible and won't wrap the return value. + * + * This is not what you normally want. Normally you want the code to be + * consistently asynchronous, and you want the result wrapped into a promise. + * But because of webdriver's control flow, we're better off not introducing any + * extra layers of promises or asynchronous activity. + * + * @param {*} val The value to call the callback with. + * @param {!Function} callback The callback function + * @return {*} If val isn't a promise, the return value of the callback is + * directly returned. If val is a promise, a promise (generated by val.then) + * resolving to the callback's return value is returned. + */ +var maybePromise = module.exports = function maybePromise(val, callback) { + if (val && (typeof val.then == 'function')) { + return val.then(callback); + } else { + return callback(val); + } +} + +/** + * Like maybePromise() but for an array of values. Analogous to `Promise.all`. + * + * @param {!Array<*>} vals An array of values to call the callback with + * @param {!Function} callback the callback function + * @return {*} If nothing in vals is a promise, the return value of the callback + * is directly returned. Otherwise, a promise (generated by the .then + * functions in vals) resolving to the callback's return value is returned. + */ +maybePromise.all = function all(vals, callback) { + var resolved = new Array(vals.length); + function resolveAt(i) { + if (i >= vals.length) { + return callback(resolved); + } else { + return maybePromise(vals[i], function(val) { + resolved[i] = val; + return resolveAt(i+1); + }); + } + } + return resolveAt(0); +} + diff --git a/package.json b/package.json index ef5f9a7..e337843 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,15 @@ "selenium-webdriver": "3.0.1" }, "devDependencies": { + "@types/node": "^6.0.56", + "@types/selenium-webdriver": "^2.53.38", + "jasmine": "2.4.1", "jshint": "^2.9.4", - "typescript": "^2.0.10" + "selenium-webdriver": "2.53.3", + "tslint": "^4.2.0", + "tslint-eslint-rules": "^3.2.3", + "typescript": "^2.0.10", + "vrsource-tslint-rules": "^4.0.0" }, "repository": { "type": "git", @@ -26,8 +33,11 @@ "main": "index.js", "scripts": { "jshint": "jshint index.js spec", - "tsc": "tsc -t ES2015 spec/asyncAwaitSpec.ts", - "pretest": "npm run jshint && npm run tsc", + "tslint": "tslint spec/*.ts", + "lint": "npm run jshint && npm run tslint", + "tsc": "tsc", + "clean": "rm spec/asyncAwait*Spec.js spec/common.js", + "pretest": "npm run lint && npm run tsc", "test": "scripts/test.sh" }, "license": "MIT", diff --git a/scripts/test.sh b/scripts/test.sh index 0194052..d11a5d1 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,16 +1,33 @@ +LIB_SPECS="spec/support/lib_specs.json" PASSING_SPECS="spec/support/passing_specs.json" FAILING_SPECS="spec/support/failing_specs.json" +NO_CF_PASSING_SPECS="spec/support/no_cf_passing_specs.json" +NO_CF_FAILING_SPECS="spec/support/no_cf_failing_specs.json" CMD_BASE="node node_modules/.bin/jasmine JASMINE_CONFIG_PATH=" -echo "### running passing specs" +# Run unit tests + +echo "### running all unit tests" +CMD=$CMD_BASE$LIB_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + + +# Run all tests when the control flow is enabled + +export SELENIUM_PROMISE_MANAGER=1 + +echo "### running all passing specs" CMD=$CMD_BASE$PASSING_SPECS echo "### $CMD" $CMD [ "$?" -eq 0 ] || exit 1 echo -EXPECTED_RESULTS="18 specs, 16 failures" -echo "### running failing specs (expecting $EXPECTED_RESULTS)" +EXPECTED_RESULTS="38 specs, 34 failures" +echo "### running all failing specs (expecting $EXPECTED_RESULTS)" CMD=$CMD_BASE$FAILING_SPECS echo "### $CMD" res=`$CMD 2>/dev/null` @@ -18,4 +35,25 @@ results_line=`echo "$res" | tail -2 | head -1` echo "result: $results_line" [ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1 +# Run only the async/await tests when the control flow is disabled + +export SELENIUM_PROMISE_MANAGER=0 + +echo "### running async/await passing specs" +CMD=$CMD_BASE$NO_CF_PASSING_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + +EXPECTED_RESULTS="19 specs, 17 failures" +echo "### running async/await failing specs (expecting $EXPECTED_RESULTS)" +CMD=$CMD_BASE$NO_CF_FAILING_SPECS +echo "### $CMD" +res=`$CMD 2>/dev/null` +results_line=`echo "$res" | tail -2 | head -1` +echo "result: $results_line" +[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1 + + echo "all pass" diff --git a/spec/adapterSpec.js b/spec/adapterSpec.js index bb09093..401a667 100644 --- a/spec/adapterSpec.js +++ b/spec/adapterSpec.js @@ -138,7 +138,7 @@ describe('webdriverJS Jasmine adapter', function() { fakeDriver.getValueList().then(function(list) { var result = list.map(function(webElem) { - var webElemsPromise = webdriver.promise.fulfilled(webElem).then(function(webElem) { + var webElemsPromise = webdriver.promise.when(webElem).then(function(webElem) { return [webElem]; }); return webdriver.promise.fullyResolved(checkTexts(webElemsPromise)); @@ -244,6 +244,12 @@ describe('webdriverJS Jasmine adapter', function() { }); describe('native promises', function() { + it('should have done argument override return returned promise', function(done) { + var ret = new Promise(function() {}); + done(); + return ret; + }); + var currentTest = null; it('should wait for webdriver events sent from native promise', function() { diff --git a/spec/asyncAwaitAdapterSpec.ts b/spec/asyncAwaitAdapterSpec.ts new file mode 100644 index 0000000..64ed381 --- /dev/null +++ b/spec/asyncAwaitAdapterSpec.ts @@ -0,0 +1,294 @@ +import {promise as wdpromise, WebElement} from 'selenium-webdriver'; +import {getFakeDriver, getMatchers} from './common.js'; + +declare function expect(actual: any): any; +declare function describe(description: string, tests: Function): void; +declare function it(description: string, test?: Function, timeout?: number): any; +declare function xit(description: string, test?: Function, timeout?: number): any; +declare function beforeEach(setup: Function): void; +declare function beforeAll(setup: Function): void; +declare function afterEach(setup: Function): void; +declare function afterAll(setup: Function): void; +declare var jasmine; + +/** + * This file is very similar to adapterSpec.ts, but we use async/await instead + * of the WebDriver Control Flow for synchronization. These tests are desgined + * to work regardless of if the WebDriver Control Flow is disabled. + */ +const fakeDriver = getFakeDriver(); + +/* jshint esversion: 6 */ +describe('webdriverJS Jasmine adapter plain', function() { + it('should pass normal synchronous tests', function() { + expect(true).toBe(true); + }); + + it('should allow an empty it block and mark as pending'); + + xit('should allow a spec marked as pending with xit', function() { + expect(true).toBe(false); + }); +}); + +describe('context', function() { + beforeEach(function() { + this.foo = 0; + }); + + it('can use the `this` to share state', function() { + expect(this.foo).toEqual(0); + this.bar = 'test pollution?'; + }); + + it('prevents test pollution by having an empty `this` created for the next spec', function() { + expect(this.foo).toEqual(0); + expect(this.bar).toBe(undefined); + }); +}); + +describe('webdriverJS Jasmine adapter', function() { + // Shorten this and you should see tests timing out. + jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; + let beforeEachMsg: string; + + beforeEach(function() { + jasmine.addMatchers(getMatchers()); + }); + + beforeEach(async function() { + await fakeDriver.setUp().then(function(value) { + beforeEachMsg = value; + }); + }); + + afterEach(function() { + beforeEachMsg = ''; + }); + + it('should only allow initializing once', function() { + expect(require('../index.js').init).toThrow( + Error('JasmineWd already initialized when init() was called')); + }); + + it('should pass normal synchronous tests', function() { + expect(true).toEqual(true); + }); + + it('should compare a promise to a primitive', async function() { + // You need `await` before `expect` if the expect needs to unwrap promises + await expect(fakeDriver.getValueA()).toEqual('a'); + await expect(fakeDriver.getValueB()).toEqual('b'); + }); + + it('beforeEach should wait for control flow', async function() { + // But you can also just add `await` wherever you like + await expect(beforeEachMsg).toEqual('setup done'); + }); + + it('should wait till the expect to run the flow', async function() { + const promiseA = fakeDriver.getValueA(); + // isPending() is only defined for WebDriver's ManagedPromise + if (!promiseA.isPending) { + promiseA.isPending = () => { return true; }; + } + + await expect(promiseA.isPending()).toBe(true); + const expectation = expect(promiseA).toEqual('a'); + await expect(promiseA.isPending()).toBe(true); + + // We still need to wait for the expectation to finish, since the control + // flow might be disabled + await expectation; + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).toEqual(fakeDriver.getOtherValueA()); + }); + + it('should still allow use of the underlying promise', async function() { + const promiseA = fakeDriver.getValueA(); + await promiseA.then(function(value) { + expect(value).toEqual('a'); + }); + }); + + it('should allow scheduling of tasks', async function() { + await fakeDriver.sleep(300); + await expect(fakeDriver.getValueB()).toEqual('b'); + }); + + it('should allow the use of custom matchers', async function() { + await expect(500).toBeLotsMoreThan(3); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(33); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(fakeDriver.getSmallNumber()); + await expect(fakeDriver.getSmallNumber()).not.toBeLotsMoreThan(fakeDriver.getBigNumber()); + }); + + it('should allow custom matchers to return a promise', async function() { + await expect(fakeDriver.getDisplayedElement()).toBeDisplayed(); + await expect(fakeDriver.getHiddenElement()).not.toBeDisplayed(); + }); + + it('should pass multiple arguments to matcher', async function() { + // Passing specific precision + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.1, 1); + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.1, 2); + + // Using default precision (2) + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.1); + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.14); + }); + + it('should allow iterating through arrays', async function() { + // This is a convoluted test which shows a real issue which + // cropped up in version changes to the selenium-webdriver module. + // See https://github.com/angular/protractor/pull/2263 + const checkTexts = async function(webElems: wdpromise.Promise wdpromise.Promise}>>) { + const texts = webElems.then(function(arr) { + const results = arr.map(function(webElem) { + return webElem.getText(); + }); + return wdpromise.all(results); + }); + + await expect(texts).not.toContain('e'); + + return true; + }; + + await fakeDriver.getValueList().then(function(list) { + const result = list.map(function(webElem) { + const webElemsPromise = wdpromise.when(webElem).then(function(webElem) { + return [webElem]; + }); + return wdpromise.fullyResolved(checkTexts(webElemsPromise)); + }); + return wdpromise.all(result); + }); + }); + + describe('not', async function() { + it('should still pass normal synchronous tests', async function() { + expect(4).not.toEqual(5); + }); + + it('should compare a promise to a primitive', async function() { + await expect(fakeDriver.getValueA()).not.toEqual('b'); + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).not.toEqual(fakeDriver.getValueB()); + }); + + it('should allow custom matchers to return a promise when actual is not a promise', async function() { + await expect(fakeDriver.displayedElement).toBeDisplayed(); + await expect(fakeDriver.hiddenElement).not.toBeDisplayed(); + }); + }); + + it('should throw an error with a WebElement actual value', function() { + const webElement = new WebElement(fakeDriver as any, 'idstring'); + + expect(function() { + expect(webElement).toEqual(4); + }).toThrow(Error('expect called with WebElement argument, expected a Promise. ' + + 'Did you mean to use .getText()?')); + }); + + it('should pass after the timed out tests', async function() { + await expect(fakeDriver.getValueA()).toEqual('a'); + }); + + describe('should work for both synchronous and asynchronous tests', function() { + let x: number; + + beforeEach(function() { + x = 0; + }); + + afterEach(function() { + expect(x).toBe(1); + }); + + it('should execute a synchronous test', function() { + x = 1; + }); + + it('should execute an asynchronous test', function(done) { + setTimeout(function(){ + x = 1; + done(); + }, 500); + }); + }); + + describe('beforeAll and afterAll', function() { + let asyncValue: number; + let setupMsg: string; + + beforeAll(function(done) { + setTimeout(function() { + asyncValue = 5; + done(); + }, 500); + }); + + beforeAll(async function() { + await fakeDriver.setUp().then(function(msg) { + setupMsg = msg; + }); + }); + + afterAll(function() { + setupMsg = ''; + }); + + it('should have set asyncValue', function() { + expect(asyncValue).toEqual(5); + }); + + it('should wait for control flow', function() { + expect(setupMsg).toEqual('setup done'); + }); + }); + + describe('it return value', function() { + const spec1 = it('test1') as any; + const spec2 = it('test2', function() {}) as any; + const spec3 = it('test3', function() {}, 1) as any; + + it('should return the spec', function() { + expect(spec1.description).toBe('test1'); + expect(spec2.description).toBe('test2'); + expect(spec3.description).toBe('test3'); + }); + }); + + describe('native promises', function() { + it('should have done argument override returned promise', async function(done) { + const ret = new Promise(function() {}); + done(); + await ret; + }); + + let currentTest: string = null; + + it('should wait for webdriver events sent from native promise', function() { + currentTest = 'A'; + return new Promise(function(resolve) { + setTimeout(async function() { + await fakeDriver.sleep(100).then(function() { + expect(currentTest).toBe('A'); + }); + resolve(); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + currentTest = 'B'; + setTimeout(done, 200); + }); + }); +}); diff --git a/spec/asyncAwaitErrorSpec.ts b/spec/asyncAwaitErrorSpec.ts new file mode 100644 index 0000000..ab2157c --- /dev/null +++ b/spec/asyncAwaitErrorSpec.ts @@ -0,0 +1,151 @@ +import {getFakeDriver, getMatchers} from './common.js'; + +declare function expect(actual: any): any; +declare function describe(description: string, tests: Function): void; +declare function it(description: string, test?: Function, timeout?: number): any; +declare function xit(description: string, test?: Function, timeout?: number): any; +declare function beforeEach(setup: Function): void; +declare function beforeAll(setup: Function): void; +declare function afterEach(setup: Function): void; +declare function afterAll(setup: Function): void; +declare var jasmine; + + +/** + * This file is very similar to errorSpec.ts, but we use async/await instead of + * the WebDriver Control Flow for synchronization. These tests are desgined to + * work regardless of if the WebDriver Control Flow is disabled. + */ + +const fakeDriver = getFakeDriver(); + +/* jshint esversion: 6 */ +describe('Timeout cases', function() { + it('should timeout after 200ms', async function(done) { + // The returned promise is ignored and jasminewd will wait for the `done` + // callback to be called + await expect(fakeDriver.getValueA()).toEqual('a'); + }, 200); + + it('should timeout after 300ms', async function() { + await fakeDriver.sleep(9999); + await expect(fakeDriver.getValueB()).toEqual('b'); + }, 300); + + it('should pass after the timed out tests', function() { + expect(true).toEqual(true); + }); +}); + +describe('things that should fail', function() { + beforeEach(function() { + jasmine.addMatchers(getMatchers()); + }); + + it('should pass errors from done callback', function(done) { + done.fail('an error from done.fail'); + }); + + it('should error asynchronously in promise callbacks', async function() { + await fakeDriver.sleep(50).then(function() { + expect(true).toEqual(false); + }); + }); + + it('should error asynchronously within done callback', function(done) { + setTimeout(async function() { + await expect(false).toEqual(true); + done(); + }, 200); + }); + + it('should fail normal synchronous tests', function() { + expect(true).toBe(false); + }); + + it('should fail when an error is thrown', function() { + throw new Error('I am an intentional error'); + }); + + it('should compare a promise to a primitive', async function() { + await expect(fakeDriver.getValueA()).toEqual('d'); + await expect(fakeDriver.getValueB()).toEqual('e'); + }); + + it('should wait till the expect to run the flow', async function() { + const promiseA = fakeDriver.getValueA(); + // isPending() is only defined for WebDriver's ManagedPromise + if (!promiseA.isPending) { + promiseA.isPending = () => { return true; }; + } + + await expect(promiseA.isPending()).toBe(true); + const expectation = expect(promiseA).toEqual('a'); + await expect(promiseA.isPending()).toBe(false); + + // We still need to wait for the expectation to finish, since the control + // flow might be disabled + await expectation; + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).toEqual(fakeDriver.getValueB()); + }); + + it('should still allow use of the underlying promise', async function() { + const promiseA = fakeDriver.getValueA(); + await promiseA.then(function(value) { + expect(value).toEqual('b'); + }); + }); + + it('should allow scheduling of tasks', async function() { + await fakeDriver.sleep(300); + await expect(fakeDriver.getValueB()).toEqual('c'); + }); + + it('should allow the use of custom matchers', async function() { + await expect(1000).toBeLotsMoreThan(999); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(1110); + await expect(fakeDriver.getBigNumber()).not.toBeLotsMoreThan(fakeDriver.getSmallNumber()); + await expect(fakeDriver.getSmallNumber()).toBeLotsMoreThan(fakeDriver.getBigNumber()); + }); + + it('should allow custom matchers to return a promise', async function() { + await expect(fakeDriver.getDisplayedElement()).not.toBeDisplayed(); + await expect(fakeDriver.getHiddenElement()).toBeDisplayed(); + }); + + it('should pass multiple arguments to matcher', async function() { + // Passing specific precision + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.5, 1); + + // Using default precision (2) + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.1); + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.14); + }); + + describe('native promises', function() { + it('should have done argument override return returned promise', async function(done) { + await new Promise(function() {}); + }); + + let testADone = false; + + it('should handle rejection from native promise', function() { + return new Promise(async function(resolve, reject) { + setTimeout(async function() { + await fakeDriver.sleep(100).then(function() { + testADone = true; + }); + reject('Rejected promise'); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + expect(testADone).toBe(true); // this test actually passes + setTimeout(done, 200); + }); + }); +}); diff --git a/spec/asyncAwaitSpec.ts b/spec/asyncAwaitSpec.ts deleted file mode 100644 index e094354..0000000 --- a/spec/asyncAwaitSpec.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* Here we have an example of using async/await with jasminewd. We are using - * typescript to gain access to async/await, but mostly this should look like - * normal javascript to you. - * - * The key thing to note here is that once you use async/await, the webdriver - * control flow is no longer reliable. This means you have to use async/await - * or promises for all your asynchronous behavior. In Protractor this would - * mean putting `await` before every line interacting with the browser. In this - * example, we have to put `await` before `driver.sleep()`. - */ -"use strict"; - -// Declare globals -declare var describe; -declare var it; -declare var expect; -declare var require; - -let driver = require('./common.js').getFakeDriver(); - -describe('async function', function() { - let sharedVal: any; - it('should wait on async functions', async function() { - sharedVal = await driver.getValueA(); // Async unwraps this to 'a' - expect(sharedVal).toBe('a'); - await driver.sleep(1000); // Normally you wouldn't need to `await` this, but - // the control flow is broken for async functions. - sharedVal = await driver.getValueB(); - }); - - it('should have waited until the end of the last it() block', function() { - expect(sharedVal).toBe('b'); - }); -}); diff --git a/spec/common.js b/spec/common.ts similarity index 60% rename from spec/common.js rename to spec/common.ts index 6786640..799c9fa 100644 --- a/spec/common.js +++ b/spec/common.ts @@ -1,110 +1,110 @@ -var webdriver = require('selenium-webdriver'); +import {promise as wdpromise, WebElement} from 'selenium-webdriver'; -var flow = webdriver.promise.controlFlow(); +const flow = wdpromise.controlFlow(); require('../index.js').init(flow); -exports.getFakeDriver = function() { +export function getFakeDriver() { return { controlFlow: function() { return flow; }, - sleep: function(ms) { + sleep: function(ms: number) { return flow.timeout(ms); }, setUp: function() { return flow.execute(function() { - return webdriver.promise.fulfilled('setup done'); + return wdpromise.when('setup done'); }, 'setUp'); }, getValueA: function() { return flow.execute(function() { - return webdriver.promise.delayed(500).then(function() { - return webdriver.promise.fulfilled('a'); + return wdpromise.delayed(500).then(function() { + return wdpromise.when('a'); }); }, 'getValueA'); }, getOtherValueA: function() { return flow.execute(function() { - return webdriver.promise.fulfilled('a'); + return wdpromise.when('a'); }, 'getOtherValueA'); }, getValueB: function() { return flow.execute(function() { - return webdriver.promise.fulfilled('b'); + return wdpromise.when('b'); }, 'getValueB'); }, getBigNumber: function() { return flow.execute(function() { - return webdriver.promise.fulfilled(1111); + return wdpromise.when(1111); }, 'getBigNumber'); }, getSmallNumber: function() { return flow.execute(function() { - return webdriver.promise.fulfilled(11); + return wdpromise.when(11); }, 'getSmallNumber'); }, getDecimalNumber: function() { return flow.execute(function() { - return webdriver.promise.fulfilled(3.14159); + return wdpromise.when(3.14159); }, 'getDecimalNumber'); }, getDisplayedElement: function() { return flow.execute(function() { - return webdriver.promise.fulfilled({ + return wdpromise.when({ isDisplayed: function() { - return webdriver.promise.fulfilled(true); + return wdpromise.when(true); } }); }, 'getDisplayedElement'); }, getHiddenElement: function() { return flow.execute(function() { - return webdriver.promise.fulfilled({ + return wdpromise.when({ isDisplayed: function() { - return webdriver.promise.fulfilled(false); + return wdpromise.when(false); } }); }, 'getHiddenElement'); }, getValueList: function() { return flow.execute(function() { - return webdriver.promise.fulfilled([{ + return wdpromise.when([{ getText: function() { - return flow.execute(function() { return webdriver.promise.fulfilled('a');}); + return flow.execute(function() { return wdpromise.when('a');}); } }, { getText: function() { - return flow.execute(function() { return webdriver.promise.fulfilled('b');}); + return flow.execute(function() { return wdpromise.when('b');}); } }, { getText: function() { - return flow.execute(function() { return webdriver.promise.fulfilled('c');}); + return flow.execute(function() { return wdpromise.when('c');}); } }, { getText: function() { - return flow.execute(function() { return webdriver.promise.fulfilled('d');}); + return flow.execute(function() { return wdpromise.when('d');}); } }]); }, 'getValueList'); }, displayedElement: { isDisplayed: function() { - return webdriver.promise.fulfilled(true); + return wdpromise.when(true); } }, hiddenElement: { isDisplayed: function() { - return webdriver.promise.fulfilled(false); + return wdpromise.when(false); } } }; }; -exports.getMatchers = function() { +export function getMatchers() { return { toBeLotsMoreThan: function() { return { - compare: function(actual, expected) { + compare: function(actual: number, expected: number) { return { pass: actual > expected + 100 }; @@ -114,7 +114,7 @@ exports.getMatchers = function() { // Example custom matcher returning a promise that resolves to true/false. toBeDisplayed: function() { return { - compare: function(actual, expected) { + compare: function(actual: WebElement, expected: void) { return { pass: actual.isDisplayed() }; diff --git a/spec/errorSpec.js b/spec/errorSpec.js index 8f3ad88..a8a2fb7 100644 --- a/spec/errorSpec.js +++ b/spec/errorSpec.js @@ -104,6 +104,10 @@ describe('things that should fail', function() { }); describe('native promises', function() { + it('should have done argument override return returned promise', function(done) { + return new Promise(function() {}); + }); + var testADone = false; it('should handle rejection from native promise', function() { diff --git a/spec/maybePromiseSpec.js b/spec/maybePromiseSpec.js new file mode 100644 index 0000000..57d173f --- /dev/null +++ b/spec/maybePromiseSpec.js @@ -0,0 +1,155 @@ +var maybePromise = require('../maybePromise.js'); +var webdriver = require('selenium-webdriver'); + +describe('maybePromise', function() { + // Helper values + var num = 588.79; // From Math.random() + var str = 'qqqpqc0'; // From math.random().toString(36); + var obj = { num: num, str: str, obj: obj, then: true }; + function idFun(x) { return x; } + function promiseMe(x) { + var promise = { then: function(callback) { return callback(x); } }; + spyOn(promise, 'then').and.callThrough(); + return promise; + } + + describe('singletons', function() { + it('should be able to use non-promises', function(done) { + maybePromise(num, function(n) { + expect(n).toBe(num); + done(); + }); + }); + + it('should not wrap non-promise values', function() { + expect(maybePromise(num, idFun)).toBe(num); + expect(maybePromise(str, idFun)).toBe(str); + expect(maybePromise(obj, idFun)).toBe(obj); + }); + + it('should be able to use promises', function(done) { + maybePromise(promiseMe(str), function(s) { + expect(s).toBe(str); + done(); + }); + }); + + it('should use a promise\'s own then() function without any wrapping', function() { + var promise = promiseMe(num); + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise(promise, callback)).toBe(num); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + + promise = promiseMe(str); + callback.calls.reset(); + expect(maybePromise(promise, callback)).toBe(str); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + + promise = promiseMe(obj); + callback.calls.reset(); + expect(maybePromise(promise, callback)).toBe(obj); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + }); + + it('should work with a real promise implementation', function(done) { + var promise = webdriver.promise.when(str); + maybePromise(promise, function(s) { + expect(s).toBe(str); + return webdriver.promise.when(num); + }).then(function(n) { + expect(n).toEqual(num); + done(); + }); + }); + + it('should fail in an expected way with poorly implemented promises', function() { + var badPromise = promiseMe(obj); + badPromise.then.and.stub(); + badPromise.then.and.returnValue(str); + var callback = jasmine.createSpy('callback'); + var ret = maybePromise(badPromise, callback); + expect(badPromise.then).toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + expect(ret).toBe(str); + }); + }); + + describe('.all', function() { + it('should work with an empty array, without wrapping', function() { + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise.all([], callback)).toEqual([]); + expect(callback).toHaveBeenCalled(); + }); + + it('should work with an array of non-promises', function(done) { + var arr = [num, str, obj]; + maybePromise.all(arr, function(a) { + expect(a).toEqual(arr); + done(); + }); + }); + + it('should not wrap non-promise values', function() { + var arr = [num, str, obj]; + expect(maybePromise.all(arr, idFun)).toEqual(arr); + }); + + it('should work with array of promises', function(done) { + var arr = [num, str, obj]; + maybePromise.all(arr.map(promiseMe), function(a) { + expect(a).toEqual(arr); + done(); + }); + }); + + it('should use promise\'s own then() function without any wrapping', function() { + var arr = [num, str, obj]; + var promiseArr = arr.map(promiseMe); + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise.all(promiseArr, callback)).toEqual(arr); + expect(callback.calls.count()).toBe(1); + for (var i = 0; i < promiseArr.length; i++) { + expect(promiseArr[i].then.calls.count()).toBe(1); + } + }); + + it('should work with a real promise implementation', function(done) { + var arr = [str, obj]; + maybePromise.all(arr.map(webdriver.promise.when), function(a) { + expect(a).toEqual(arr); + return webdriver.promise.when(num); + }).then(function(n) { + expect(n).toEqual(num); + done(); + }); + }); + + it('should work with a mix of promises and non-promises', function(done) { + var arr = [num, promiseMe(str), webdriver.promise.when(obj), + webdriver.promise.when(str), webdriver.promise.when(num), + str, promiseMe(num), obj, promiseMe(obj)]; // Random order + maybePromise.all(arr, function(resolved) { + maybePromise(webdriver.promise.all(arr), function(wdResolved) { + expect(resolved).toEqual(wdResolved); + done(); + }); + }); + }); + + it('should fail in an expected way with poorly implemented promises', function() { + var arr = [num, promiseMe(str), str, promiseMe(num), obj, promiseMe(obj)]; // Random order + var badPromise = promiseMe(obj); + badPromise.then.and.stub(); + badPromise.then.and.returnValue(str); + arr.push(badPromise); + var callback = jasmine.createSpy('callback'); + var ret = maybePromise.all(arr, callback); + expect(badPromise.then).toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + expect(ret).toBe(str); + }); + }); +}); diff --git a/spec/support/failing_specs.json b/spec/support/failing_specs.json index 4545241..1803eff 100644 --- a/spec/support/failing_specs.json +++ b/spec/support/failing_specs.json @@ -1,6 +1,6 @@ { "spec_dir": "spec", "spec_files": [ - "errorSpec.js" + "*rrorSpec.js" ] } diff --git a/spec/support/lib_specs.json b/spec/support/lib_specs.json new file mode 100644 index 0000000..d4709ea --- /dev/null +++ b/spec/support/lib_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "spec", + "spec_files": [ + "maybePromiseSpec.js" + ] +} diff --git a/spec/support/no_cf_failing_specs.json b/spec/support/no_cf_failing_specs.json new file mode 100644 index 0000000..8e2f61f --- /dev/null +++ b/spec/support/no_cf_failing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "spec", + "spec_files": [ + "asyncAwaitErrorSpec.js" + ] +} diff --git a/spec/support/no_cf_passing_specs.json b/spec/support/no_cf_passing_specs.json new file mode 100644 index 0000000..f0e3a83 --- /dev/null +++ b/spec/support/no_cf_passing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "spec", + "spec_files": [ + "asyncAwaitAdapterSpec.js" + ] +} diff --git a/spec/support/passing_specs.json b/spec/support/passing_specs.json index ced6d64..24152a9 100644 --- a/spec/support/passing_specs.json +++ b/spec/support/passing_specs.json @@ -1,7 +1,6 @@ { "spec_dir": "spec", "spec_files": [ - "adapterSpec.js", - "asyncAwaitSpec.js" + "*dapterSpec.js" ] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..902ccdb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": false, + "declaration": false, + "removeComments": false, + "noImplicitAny": false + }, + "include": [ + "spec/*.ts" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..5935ad9 --- /dev/null +++ b/tslint.json @@ -0,0 +1,15 @@ +{ + "rulesDirectory": [ + "node_modules/vrsource-tslint-rules/rules", + "node_modules/tslint-eslint-rules/dist/rules" + ], + "rules": { + "no-duplicate-imports": true, + "no-duplicate-variable": true, + "no-jasmine-focus": true, + "no-var-keyword": true, + "semicolon": [true], + "variable-name": [true, "ban-keywords"], + "no-inner-declarations": [true, "function"] + } +}