diff --git a/.gitignore b/.gitignore index 047b48e..a3c91eb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ selenium/ # Build artifacts built/ +spec/built/ node_modules/ website/bower_components/ website/build/ diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..2442be8 --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +./spec/built/* diff --git a/gulpfile.js b/gulpfile.js index b678b8d..18259e9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -60,7 +60,7 @@ gulp.task('checkVersion', function(done) { }); gulp.task('built:copy', function(done) { - return gulp.src(['lib/**/*.js','lib/index.d.ts']) + return gulp.src(['lib/**/*.js']) .pipe(gulp.dest('built/')); done(); }); @@ -94,14 +94,17 @@ gulp.task('tsc', function(done) { runSpawn(done, 'node', ['node_modules/typescript/bin/tsc']); }); +gulp.task('tsc:spec', function(done) { + runSpawn(done, 'node', ['node_modules/typescript/bin/tsc', '-p', 'ts_spec_config.json']); +}); + gulp.task('prepublish', function(done) { - runSequence('checkVersion', 'jshint', 'tsc', - 'built:copy', done); + runSequence('checkVersion', 'jshint', 'tsc', 'built:copy', 'tsc:spec', done); }); gulp.task('pretest', function(done) { runSequence('checkVersion', - ['webdriver:update', 'jshint', 'tslint', 'format'], 'tsc', 'built:copy', done); + ['webdriver:update', 'jshint', 'tslint', 'format'], 'tsc', 'built:copy', 'tsc:spec', done); }); gulp.task('default',['prepublish']); diff --git a/lib/config.ts b/lib/config.ts index d4dee0c..4e2ed6a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -601,6 +601,29 @@ export interface Config { */ disableChecks?: boolean; + /** + * Enable/disable the WebDriver Control Flow. + * + * WebDriverJS (and by extention, Protractor) uses a Control Flow to manage the order in which + * commands are executed and promises are resolved (see docs/control-flow.md for details). + * However, as syntax like `async`/`await` are being introduced, WebDriverJS has decided to + * deprecate the control flow, and have users manage the asynchronous activity themselves + * (details here: https://github.com/SeleniumHQ/selenium/issues/2969). + * + * At the moment, the WebDriver Control Flow is still enabled by default. You can disable it by + * setting the environment variable `SELENIUM_PROMISE_MANAGER` to `0`. In a webdriver release in + * Q4 2017, the Control Flow will be disabled by default, but you will be able to re-enable it by + * setting `SELENIUM_PROMISE_MANAGER` to `1`. At a later point, the control flow will be removed + * for good. + * + * If you don't like managing environment variables, you can set this option in your config file, + * and Protractor will handle enabling/disabling the control flow for you. Setting this option + * is higher priority than the `SELENIUM_PROMISE_MANAGER` environment variable. + * + * @type {boolean=} + */ + SELENIUM_PROMISE_MANAGER?: boolean; + seleniumArgs?: any[]; jvmArgs?: string[]; configDir?: string; diff --git a/lib/runner.ts b/lib/runner.ts index 0b17ebd..309fdd6 100644 --- a/lib/runner.ts +++ b/lib/runner.ts @@ -343,6 +343,10 @@ export class Runner extends EventEmitter { throw new Error('Spec patterns did not match any files.'); } + if (this.config_.SELENIUM_PROMISE_MANAGER != null) { + (wdpromise as any).USE_PROMISE_MANAGER = this.config_.SELENIUM_PROMISE_MANAGER; + } + // 0) Wait for debugger return q(this.ready_) .then(() => { diff --git a/package.json b/package.json index 9f59c56..737cad6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/chalk": "^0.4.28", "@types/glob": "^5.0.29", "@types/jasmine": "^2.5.38", + "@types/jasminewd2": "^2.0.0", "@types/minimatch": "^2.0.28", "@types/minimist": "^1.1.28", "@types/optimist": "^0.0.29", diff --git a/scripts/test.js b/scripts/test.js index b2f8421..d5c458e 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -38,6 +38,7 @@ var passingTests = [ 'node built/cli.js spec/noGlobalsConf.js', 'node built/cli.js spec/angular2Conf.js', 'node built/cli.js spec/hybridConf.js', + 'node built/cli.js spec/built/noCFSmokeConf.js', 'node scripts/driverProviderAttachSession.js', 'node scripts/errorTest.js', // Interactive Element Explorer tasks diff --git a/spec/ts/noCF/smoke_spec.ts b/spec/ts/noCF/smoke_spec.ts new file mode 100644 index 0000000..6707e35 --- /dev/null +++ b/spec/ts/noCF/smoke_spec.ts @@ -0,0 +1,157 @@ +// Based off of spec/basic/elements_spec.js +import {promise as ppromise, browser, element, by, By, $, $$, ExpectedConditions, ElementFinder} from '../../..'; + +describe('ElementFinder', function() { + it('should return the same result as browser.findElement', async function() { + await browser.get('index.html#/form'); + const nameByElement = element(by.binding('username')); + + await expect(nameByElement.getText()).toEqual( + browser.findElement(by.binding('username')).getText()); + }); + + it('should wait to grab the WebElement until a method is called', async function() { + // These should throw no error before a page is loaded. + const usernameInput = element(by.model('username')); + const name = element(by.binding('username')); + + await browser.get('index.html#/form'); + + await expect(name.getText()).toEqual('Anon'); + + await usernameInput.clear(); + await usernameInput.sendKeys('Jane'); + await expect(name.getText()).toEqual('Jane'); + }); + + it('should chain element actions', async function() { + await browser.get('index.html#/form'); + + const usernameInput = element(by.model('username')); + const name = element(by.binding('username')); + + await expect(name.getText()).toEqual('Anon'); + + await ((usernameInput.clear() as any) as ElementFinder).sendKeys('Jane'); + await expect(name.getText()).toEqual('Jane'); + }); + + it('chained call should wait to grab the WebElement until a method is called', + async function() { + // These should throw no error before a page is loaded. + const reused = element(by.id('baz')). + element(by.binding('item.reusedBinding')); + + await browser.get('index.html#/conflict'); + + await expect(reused.getText()).toEqual('Inner: inner'); + await expect(reused.isPresent()).toBe(true); + }); + + it('should differentiate elements with the same binding by chaining', + async function() { + await browser.get('index.html#/conflict'); + + const outerReused = element(by.binding('item.reusedBinding')); + const innerReused = + element(by.id('baz')).element(by.binding('item.reusedBinding')); + + await expect(outerReused.getText()).toEqual('Outer: outer'); + await expect(innerReused.getText()).toEqual('Inner: inner'); + }); + + it('should chain deeper than 2', async function() { + // These should throw no error before a page is loaded. + const reused = element(by.css('body')).element(by.id('baz')). + element(by.binding('item.reusedBinding')); + + await browser.get('index.html#/conflict'); + + await expect(reused.getText()).toEqual('Inner: inner'); + }); + + it('should allow handling errors', async function() { + await browser.get('index.html#/form'); + try { + await $('.nopenopenope').getText(); + + // The above line should have throw an error. Fail. + await expect(true).toEqual(false); + } catch (e) { + await expect(true).toEqual(true); + } + }); + + it('should allow handling chained errors', async function() { + await browser.get('index.html#/form'); + try { + await $('.nopenopenope').$('furthernope').getText(); + + // The above line should have throw an error. Fail. + await expect(true).toEqual(false); + } catch (e) { + await expect(true).toEqual(true); + } + }); + + it('should keep a reference to the original locator', async function() { + await browser.get('index.html#/form'); + + const byCss = by.css('body'); + const byBinding = by.binding('greet'); + + await expect(element(byCss).locator()).toEqual(byCss); + await expect(element(byBinding).locator()).toEqual(byBinding); + }); + + it('should propagate exceptions', async function() { + await browser.get('index.html#/form'); + + const invalidElement = element(by.binding('INVALID')); + const successful = invalidElement.getText().then(function() { + return true; + } as any as (() => ppromise.Promise), function() { + return false; + } as any as (() => ppromise.Promise)); + await expect(successful).toEqual(false); + }); + + it('should be returned from a helper without infinite loops', async function() { + await browser.get('index.html#/form'); + const helperPromise = ppromise.when(true).then(function() { + return element(by.binding('greeting')); + }); + + await helperPromise.then(async function(finalResult: ElementFinder) { + await expect(finalResult.getText()).toEqual('Hiya'); + } as any as (() => ppromise.Promise)); + }); + + it('should be usable in WebDriver functions', async function() { + await browser.get('index.html#/form'); + const greeting = element(by.binding('greeting')); + await browser.executeScript('arguments[0].scrollIntoView', greeting); + }); + + it('should allow null as success handler', async function() { + await browser.get('index.html#/form'); + + const name = element(by.binding('username')); + + await expect(name.getText()).toEqual('Anon'); + await expect( + name.getText().then(null, function() {}) + ).toEqual('Anon'); + + }); + + it('should check equality correctly', async function() { + await browser.get('index.html#/form'); + + const usernameInput = element(by.model('username')); + const name = element(by.binding('username')); + + await expect(usernameInput.equals(usernameInput)).toEqual(true); + await expect(usernameInput.equals(name)).toEqual(false); + }); +}); diff --git a/spec/ts/noCFSmokeConf.ts b/spec/ts/noCFSmokeConf.ts new file mode 100644 index 0000000..f0bc5ef --- /dev/null +++ b/spec/ts/noCFSmokeConf.ts @@ -0,0 +1,18 @@ +import {Config} from '../..'; +const env = require('../environment.js'); + +export let config: Config = { + seleniumAddress: env.seleniumAddress, + + framework: 'jasmine', + + specs: [ + 'noCF/smoke_spec.js' + ], + + capabilities: env.capabilities, + + baseUrl: env.baseUrl + '/ng1/', + + SELENIUM_PROMISE_MANAGER: false +}; diff --git a/ts_spec_config.json b/ts_spec_config.json new file mode 100644 index 0000000..e460c7a --- /dev/null +++ b/ts_spec_config.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "removeComments": false, + "noImplicitAny": true, + "outDir": "spec/built", + "types": [ + "jasmine", "jasminewd2", "node", + "chalk", "glob", "minimatch", + "minimist", "optimist", "q", + "selenium-webdriver" + ] + }, + "include": [ + "spec/ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index f3889d4..1ef128d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noImplicitAny": true, "outDir": "built/", "types": [ - "jasmine", "node", + "jasmine", "jasminewd2", "node", "chalk", "glob", "minimatch", "minimist", "optimist", "q", "selenium-webdriver"