Skip to content

Commit

Permalink
chore(promises): clean up driver providers and browser control flow (a…
Browse files Browse the repository at this point in the history
…ngular#5034)

Driver providers and tests:

- Use native promises over q promises in driver providers
- Remove driverProviderUseExistingWebDriver since the generation of the selenium server is already accomplished when providing a selenium address in driverProvider.ts. Also clean up docs and tests.
- Enabled the driverProviderLocal tests
- Clean up JSDocs for q.promise

Basic lib spec:

- Remove auto unwrap test for a WebElement. Reference PR angular#3471

Browser:

- Remove control flow from waitForAngularEnabled, waitForAngular, and angularAppRoot in the Browser class.
  • Loading branch information
cnishina committed Mar 23, 2019
1 parent 69791ad commit e22065c
Show file tree
Hide file tree
Showing 22 changed files with 186 additions and 466 deletions.
34 changes: 0 additions & 34 deletions docs/server-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,37 +108,3 @@ Protractor can test directly against Chrome and Firefox without using a Selenium
- `directConnect: true` - Your test script communicates directly Chrome Driver or Firefox Driver, bypassing any Selenium Server. If this is true, settings for `seleniumAddress` and `seleniumServerJar` will be ignored. If you attempt to use a browser other than Chrome or Firefox an error will be thrown.

The advantage of directly connecting to browser drivers is that your test scripts may start up and run faster.

Re-using an Existing WebDriver
------------------------------

The use case for re-using an existing WebDriver is when you have existing
`selenium-webdriver` code and are already in control of how the WebDriver is
created, but would also like Protractor to use the same browser, so you can
use protractor's element locators and the rest of its API. This could be
done with the `attachSession` driver provider, but the `attachSession` API is
being removed in `selenium-webdriver` 4.0.0.

Instead of a protractor config file, you create a config object in your test
setup code, and add your already-created WebDriver object and base URL.

```javascript
const ProtractorConfigParser = require('protractor/built/configParser').ConfigParser;
const ProtractorRunner = require('protractor/built/runner').Runner;

const ptorConfig = new ProtractorConfigParser().config_;
ptorConfig.baseUrl = myExistingBaseUrl;
ptorConfig.seleniumWebDriver = myExistingWebDriver;
ptorConfig.noGlobals = true; // local preference

// looks similar to protractor/built/runner.js run()
const ptorRunner = new ProtractorRunner(ptorConfig);
ptorRunner.driverProvider_.setupEnv();
const browser = ptorRunner.createBrowser();
ptorRunner.setupGlobals_(browser); // now you can access protractor.$, etc.
```

Note that this driver provider leaves you in control of quitting the driver,
but that also means Protractor API calls that expect the driver to properly
quit and/or restart the browser, e.g. `restart`, `restartSync`, and
`forkNewDriverInstance`, will not behave as documented.
240 changes: 100 additions & 140 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,24 +193,18 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
* this method is called use the new app root. Pass nothing to get a promise that
* resolves to the value of the selector.
*
* @param {string|webdriver.promise.Promise<string>} value The new selector.
* @param {string|webdriver.promise.Promise<string>} valuePromise The new selector.
* @returns A promise that resolves with the value of the selector.
*/
angularAppRoot(value: string|wdpromise.Promise<string> = null): wdpromise.Promise<string> {
return this.driver.controlFlow().execute(() => {
if (value != null) {
return wdpromise.when(value).then((value: string) => {
this.internalRootEl = value;
if (this.bpClient) {
const bpCommandPromise = this.bpClient.setWaitParams(value);
// Convert to webdriver promise as best as possible
return wdpromise.when(bpCommandPromise as any).then(() => this.internalRootEl);
}
return this.internalRootEl;
});
async angularAppRoot(valuePromise: string|wdpromise.Promise<string> = null): Promise<string> {
if (valuePromise != null) {
const value = await valuePromise;
this.internalRootEl = value;
if (this.bpClient) {
await this.bpClient.setWaitParams(value);
}
return wdpromise.when(this.internalRootEl);
}, `Set angular root selector to ${value}`);
}
return this.internalRootEl;
}

/**
Expand Down Expand Up @@ -417,23 +411,17 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
* Call waitForAngularEnabled() without passing a value to read the current
* state without changing it.
*/
waitForAngularEnabled(enabled: boolean|wdpromise.Promise<boolean> = null):
wdpromise.Promise<boolean> {
if (enabled != null) {
const ret = this.driver.controlFlow().execute(() => {
return wdpromise.when(enabled).then((enabled: boolean) => {
if (this.bpClient) {
logger.debug('Setting waitForAngular' + !enabled);
const bpCommandPromise = this.bpClient.setWaitEnabled(enabled);
// Convert to webdriver promise as best as possible
return wdpromise.when(bpCommandPromise as any).then(() => enabled);
}
});
}, `Set proxy synchronization enabled to ${enabled}`);
async waitForAngularEnabled(enabledPromise: boolean|wdpromise.Promise<boolean> = null):
Promise<boolean> {
if (enabledPromise != null) {
const enabled = await enabledPromise;
if (this.bpClient) {
logger.debug('Setting waitForAngular' + !enabled);
await this.bpClient.setWaitEnabled(enabled);
}
this.internalIgnoreSynchronization = !enabled;
return ret;
}
return wdpromise.when(!this.ignoreSynchronization);
return !this.ignoreSynchronization;
}

/**
Expand Down Expand Up @@ -602,15 +590,15 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
* @template T
*/
private executeAsyncScript_(script: string|Function, description: string, ...scriptArgs: any[]):
wdpromise.Promise<any> {
Promise<any> {
if (typeof script === 'function') {
script = 'return (' + script + ').apply(null, arguments);';
}
return this.driver.schedule(
new Command(CommandName.EXECUTE_ASYNC_SCRIPT)
.setParameter('script', script)
.setParameter('args', scriptArgs),
description);
new Command(CommandName.EXECUTE_ASYNC_SCRIPT)
.setParameter('script', script)
.setParameter('args', scriptArgs),
description) as Promise<any>;
}

/**
Expand All @@ -624,116 +612,90 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
* @returns {!webdriver.promise.Promise} A promise that will resolve to the
* scripts return value.
*/
waitForAngular(opt_description?: string): wdpromise.Promise<any> {
async waitForAngular(opt_description?: string): Promise<any> {
let description = opt_description ? ' - ' + opt_description : '';
if (this.ignoreSynchronization) {
return this.driver.controlFlow().execute(() => {
return true;
}, 'Ignore Synchronization Protractor.waitForAngular()');
return true;
}

let runWaitForAngularScript: () => wdpromise.Promise<any> = () => {
let runWaitForAngularScript = async(): Promise<any> => {
if (this.plugins_.skipAngularStability() || this.bpClient) {
return this.driver.controlFlow().execute(() => {
return wdpromise.when(null);
}, 'bpClient or plugin stability override');
return null;
} else {
// Need to wrap this so that we read rootEl in the control flow, not synchronously.
return this.angularAppRoot().then((rootEl: string) => {
return this.executeAsyncScript_(
clientSideScripts.waitForAngular, 'Protractor.waitForAngular()' + description,
rootEl);
});
let rootEl = await this.angularAppRoot();
return this.executeAsyncScript_(
clientSideScripts.waitForAngular, `Protractor.waitForAngular() ${description}`, rootEl);
}
};

return runWaitForAngularScript()
.then((browserErr: Function) => {
if (browserErr) {
throw new Error(
'Error while waiting for Protractor to ' +
'sync with the page: ' + JSON.stringify(browserErr));
try {
let browserErr = await runWaitForAngularScript();
if (browserErr) {
throw new Error(
'Error while waiting for Protractor to ' +
'sync with the page: ' + JSON.stringify(browserErr));
}
await this.plugins_.waitForPromise(this);

await this.driver.wait(async () => {
let results = await this.plugins_.waitForCondition(this);
return results.reduce((x, y) => x && y, true);
}, this.allScriptsTimeout, 'Plugins.waitForCondition()');
} catch (err) {
let timeout: RegExpExecArray;
if (/asynchronous script timeout/.test(err.message)) {
// Timeout on Chrome
timeout = /-?[\d\.]*\ seconds/.exec(err.message);
} else if (/Timed out waiting for async script/.test(err.message)) {
// Timeout on Firefox
timeout = /-?[\d\.]*ms/.exec(err.message);
} else if (/Timed out waiting for an asynchronous script/.test(err.message)) {
// Timeout on Safari
timeout = /-?[\d\.]*\ ms/.exec(err.message);
}
if (timeout) {
let errMsg = `Timed out waiting for asynchronous Angular tasks to finish after ` +
`${timeout}. This may be because the current page is not an Angular ` +
`application. Please see the FAQ for more details: ` +
`https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular`;
if (description.indexOf(' - Locator: ') == 0) {
errMsg += '\nWhile waiting for element with locator' + description;
}
let pendingTimeoutsPromise: wdpromise.Promise<any>;
if (this.trackOutstandingTimeouts_) {
pendingTimeoutsPromise = this.executeScriptWithDescription(
'return window.NG_PENDING_TIMEOUTS',
'Protractor.waitForAngular() - getting pending timeouts' + description);
} else {
pendingTimeoutsPromise = wdpromise.when({});
}
let pendingHttpsPromise = this.executeScriptWithDescription(
clientSideScripts.getPendingHttpRequests,
'Protractor.waitForAngular() - getting pending https' + description,
this.internalRootEl);

let arr = await Promise.all([pendingTimeoutsPromise, pendingHttpsPromise]);

let pendingTimeouts = arr[0] || [];
let pendingHttps = arr[1] || [];

let key: string, pendingTasks: string[] = [];
for (key in pendingTimeouts) {
if (pendingTimeouts.hasOwnProperty(key)) {
pendingTasks.push(' - $timeout: ' + pendingTimeouts[key]);
}
})
.then(
() => {
return this.driver.controlFlow()
.execute(
() => {
return this.plugins_.waitForPromise(this);
},
'Plugins.waitForPromise()')
.then(() => {
return this.driver.wait(() => {
return this.plugins_.waitForCondition(this).then((results: boolean[]) => {
return results.reduce((x, y) => x && y, true);
});
}, this.allScriptsTimeout, 'Plugins.waitForCondition()');
});
},
(err: Error) => {
let timeout: RegExpExecArray;
if (/asynchronous script timeout/.test(err.message)) {
// Timeout on Chrome
timeout = /-?[\d\.]*\ seconds/.exec(err.message);
} else if (/Timed out waiting for async script/.test(err.message)) {
// Timeout on Firefox
timeout = /-?[\d\.]*ms/.exec(err.message);
} else if (/Timed out waiting for an asynchronous script/.test(err.message)) {
// Timeout on Safari
timeout = /-?[\d\.]*\ ms/.exec(err.message);
}
if (timeout) {
let errMsg = `Timed out waiting for asynchronous Angular tasks to finish after ` +
`${timeout}. This may be because the current page is not an Angular ` +
`application. Please see the FAQ for more details: ` +
`https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular`;
if (description.indexOf(' - Locator: ') == 0) {
errMsg += '\nWhile waiting for element with locator' + description;
}
let pendingTimeoutsPromise: wdpromise.Promise<any>;
if (this.trackOutstandingTimeouts_) {
pendingTimeoutsPromise = this.executeScriptWithDescription(
'return window.NG_PENDING_TIMEOUTS',
'Protractor.waitForAngular() - getting pending timeouts' + description);
} else {
pendingTimeoutsPromise = wdpromise.when({});
}
let pendingHttpsPromise = this.executeScriptWithDescription(
clientSideScripts.getPendingHttpRequests,
'Protractor.waitForAngular() - getting pending https' + description,
this.internalRootEl);

return wdpromise.all([pendingTimeoutsPromise, pendingHttpsPromise])
.then(
(arr: any[]) => {
let pendingTimeouts = arr[0] || [];
let pendingHttps = arr[1] || [];

let key: string, pendingTasks: string[] = [];
for (key in pendingTimeouts) {
if (pendingTimeouts.hasOwnProperty(key)) {
pendingTasks.push(' - $timeout: ' + pendingTimeouts[key]);
}
}
for (key in pendingHttps) {
pendingTasks.push(' - $http: ' + pendingHttps[key].url);
}
if (pendingTasks.length) {
errMsg += '. \nThe following tasks were pending:\n';
errMsg += pendingTasks.join('\n');
}
err.message = errMsg;
throw err;
},
() => {
err.message = errMsg;
throw err;
});
} else {
throw err;
}
});
}
for (key in pendingHttps) {
pendingTasks.push(' - $http: ' + pendingHttps[key].url);
}
if (pendingTasks.length) {
errMsg += '. \nThe following tasks were pending:\n';
errMsg += pendingTasks.join('\n');
}
err.message = errMsg;
}
throw err;
}
}

/**
Expand Down Expand Up @@ -978,16 +940,14 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
.then(() => {
// Reset bpClient sync
if (this.bpClient) {
return this.driver.controlFlow().execute(() => {
return this.bpClient.setWaitEnabled(!this.internalIgnoreSynchronization);
});
return this.bpClient.setWaitEnabled(!this.internalIgnoreSynchronization);
}
})
.then(() => {
// Run Plugins
return this.driver.controlFlow().execute(() => {
if (!this.ignoreSynchronization) {
return this.plugins_.onPageStable(this);
});
}
})
.then(() => null);
}
Expand Down
8 changes: 0 additions & 8 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {WebDriver} from 'selenium-webdriver';

import {PluginConfig} from './plugins';

export interface Config {
Expand Down Expand Up @@ -238,12 +236,6 @@ export interface Config {
*/
firefoxPath?: string;

// ---- 8. To re-use an existing WebDriver object ---------------------------

// This would not appear in a configuration file. Instead a configuration
// object would be created that includes an existing webdriver.
seleniumWebDriver?: WebDriver;

// ---------------------------------------------------------------------------
// ----- What tests to run ---------------------------------------------------
// ---------------------------------------------------------------------------
Expand Down
12 changes: 4 additions & 8 deletions lib/driverProviders/attachSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* It is responsible for setting up the account object, tearing
* it down, and setting up the driver correctly.
*/
import * as q from 'q';
import {promise as wdpromise, WebDriver} from 'selenium-webdriver';
import {WebDriver} from 'selenium-webdriver';

import {Config} from '../config';
import {Logger} from '../logger';
Expand All @@ -22,13 +21,12 @@ export class AttachSession extends DriverProvider {

/**
* Configure and launch (if applicable) the object's environment.
* @return {q.promise} A promise which will resolve when the environment is
* @return {Promise} A promise which will resolve when the environment is
* ready to test.
*/
protected setupDriverEnv(): q.Promise<any> {
protected async setupDriverEnv(): Promise<any> {
logger.info('Using the selenium server at ' + this.config_.seleniumAddress);
logger.info('Using session id - ' + this.config_.seleniumSessionId);
return q(undefined);
}

/**
Expand All @@ -50,7 +48,5 @@ export class AttachSession extends DriverProvider {
*
* @public
*/
quitDriver(): wdpromise.Promise<void> {
return wdpromise.when(undefined);
}
async quitDriver(): Promise<void> {}
}
Loading

0 comments on commit e22065c

Please sign in to comment.