Skip to content
This repository was archived by the owner on Jul 29, 2024. It is now read-only.

Commit ef2fe60

Browse files
heathkitjuliemr
authored andcommitted
feat(blockingproxy): Add synchronization with BlockingProxy. (#3813)
This adds support for BlockingProxy behind the flag --useBlockingProxy. If set, the driver providers will start a proxy during their setup phase, passing the selenium address to the proxy and starting a webdriver client that talks to the proxy. Starting a proxy for each driver provider isn't strictly necessary. However, when we run with multiple capabilities it's easier to handle the logging if each Protractor instance has it's own proxy. Known issues: - Doesn't work with directConnect. You can get the address of chromedriver by mucking around in Selenium internals, but this probably changed for Selenium 3.0 and I doubt it's worth figuring out until we upgrade. - Doesn't yet work with webDriverProxy (but it's an easy fix)
1 parent a03037e commit ef2fe60

18 files changed

+209
-31
lines changed

lib/bpRunner.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {ChildProcess, fork} from 'child_process';
2+
import * as q from 'q';
3+
4+
import {Config} from './config';
5+
import {Logger} from './logger';
6+
7+
const BP_PATH = require.resolve('blocking-proxy/built/lib/bin.js');
8+
9+
let logger = new Logger('BlockingProxy');
10+
11+
export class BlockingProxyRunner {
12+
bpProcess: ChildProcess;
13+
public port: number;
14+
15+
constructor(private config: Config) {}
16+
17+
start() {
18+
return q.Promise((resolve, reject) => {
19+
this.checkSupportedConfig();
20+
21+
let args = [
22+
'--fork', '--seleniumAddress', this.config.seleniumAddress, '--rootElement',
23+
this.config.rootElement
24+
];
25+
this.bpProcess = fork(BP_PATH, args, {silent: true});
26+
logger.info('Starting BlockingProxy with args: ' + args.toString());
27+
this.bpProcess
28+
.on('message',
29+
(data) => {
30+
this.port = data['port'];
31+
resolve(data['port']);
32+
})
33+
.on('error',
34+
(err) => {
35+
reject(new Error('Unable to start BlockingProxy ' + err));
36+
})
37+
.on('exit', (code: number, signal: number) => {
38+
reject(new Error('BP exited with ' + code));
39+
logger.error('Exited with ' + code);
40+
logger.error('signal ' + signal);
41+
});
42+
43+
this.bpProcess.stdout.on('data', (msg: Buffer) => {
44+
logger.debug(msg.toString().trim());
45+
});
46+
47+
this.bpProcess.stderr.on('data', (msg: Buffer) => {
48+
logger.error(msg.toString().trim());
49+
});
50+
51+
process.on('exit', () => {
52+
this.bpProcess.kill();
53+
})
54+
})
55+
}
56+
57+
checkSupportedConfig() {
58+
if (this.config.directConnect) {
59+
throw new Error('BlockingProxy not yet supported with directConnect!');
60+
}
61+
}
62+
}

lib/browser.ts

+59-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {BPClient} from 'blocking-proxy';
12
import {ActionSequence, By, Capabilities, Command as WdCommand, FileDetector, ICommandName, Options, promise as wdpromise, Session, TargetLocator, TouchSequence, until, WebDriver, WebElement} from 'selenium-webdriver';
23
import * as url from 'url';
34

@@ -141,6 +142,12 @@ export class ProtractorBrowser extends Webdriver {
141142
*/
142143
driver: WebDriver;
143144

145+
/**
146+
* The client used to control the BlockingProxy. If unset, BlockingProxy is
147+
* not being used and Protractor will handle client-side synchronization.
148+
*/
149+
bpClient: BPClient;
150+
144151
/**
145152
* Helper function for finding elements.
146153
*
@@ -186,9 +193,26 @@ export class ProtractorBrowser extends Webdriver {
186193
* tests to become flaky. This should be used only when necessary, such as
187194
* when a page continuously polls an API using $timeout.
188195
*
196+
* This property is deprecated - please use waitForAngularEnabled instead.
197+
*
198+
* @deprecated
189199
* @type {boolean}
190200
*/
191-
ignoreSynchronization: boolean;
201+
set ignoreSynchronization(value) {
202+
this.driver.controlFlow().execute(() => {
203+
if (this.bpClient) {
204+
logger.debug('Setting waitForAngular' + value);
205+
this.bpClient.setSynchronization(!value);
206+
}
207+
}, `Set proxy synchronization to ${value}`);
208+
this.internalIgnoreSynchronization = value;
209+
}
210+
211+
get ignoreSynchronization() {
212+
return this.internalIgnoreSynchronization;
213+
}
214+
215+
internalIgnoreSynchronization: boolean;
192216

193217
/**
194218
* Timeout in milliseconds to wait for pages to load when calling `get`.
@@ -272,7 +296,7 @@ export class ProtractorBrowser extends Webdriver {
272296

273297
constructor(
274298
webdriverInstance: WebDriver, opt_baseUrl?: string, opt_rootElement?: string,
275-
opt_untrackOutstandingTimeouts?: boolean) {
299+
opt_untrackOutstandingTimeouts?: boolean, opt_blockingProxyUrl?: string) {
276300
super();
277301
// These functions should delegate to the webdriver instance, but should
278302
// wait for Angular to sync up before performing the action. This does not
@@ -291,6 +315,10 @@ export class ProtractorBrowser extends Webdriver {
291315
});
292316

293317
this.driver = webdriverInstance;
318+
if (opt_blockingProxyUrl) {
319+
logger.info('Starting BP client for ' + opt_blockingProxyUrl);
320+
this.bpClient = new BPClient(opt_blockingProxyUrl);
321+
}
294322
this.element = buildElementHelper(this);
295323
this.$ = build$(this.element, By);
296324
this.$$ = build$$(this.element, By);
@@ -325,6 +353,22 @@ export class ProtractorBrowser extends Webdriver {
325353
this.ExpectedConditions = new ProtractorExpectedConditions(this);
326354
}
327355

356+
/**
357+
* If set to false, Protractor will not wait for Angular $http and $timeout
358+
* tasks to complete before interacting with the browser. This can cause
359+
* flaky tests, but should be used if, for instance, your app continuously
360+
* polls an API with $timeout.
361+
*
362+
* Call waitForAngularEnabled() without passing a value to read the current
363+
* state without changing it.
364+
*/
365+
waitForAngularEnabled(enabled: boolean = null): boolean {
366+
if (enabled != null) {
367+
this.ignoreSynchronization = !enabled;
368+
}
369+
return !this.ignoreSynchronization;
370+
}
371+
328372
/**
329373
* Get the processed configuration object that is currently being run. This
330374
* will contain the specs and capabilities properties of the current runner
@@ -445,7 +489,7 @@ export class ProtractorBrowser extends Webdriver {
445489
}
446490

447491
let runWaitForAngularScript: () => wdpromise.Promise<any> = () => {
448-
if (this.plugins_.skipAngularStability()) {
492+
if (this.plugins_.skipAngularStability() || this.bpClient) {
449493
return wdpromise.fulfilled();
450494
} else if (this.rootEl) {
451495
return this.executeAsyncScript_(
@@ -668,6 +712,12 @@ export class ProtractorBrowser extends Webdriver {
668712
return 'Protractor.get(' + destination + ') - ' + str;
669713
};
670714

715+
if (this.bpClient) {
716+
this.driver.controlFlow().execute(() => {
717+
return this.bpClient.setSynchronization(false);
718+
});
719+
}
720+
671721
if (this.ignoreSynchronization) {
672722
this.driver.get(destination);
673723
return this.driver.controlFlow().execute(() => this.plugins_.onPageLoad()).then(() => {});
@@ -768,6 +818,12 @@ export class ProtractorBrowser extends Webdriver {
768818
}
769819
}
770820

821+
if (this.bpClient) {
822+
this.driver.controlFlow().execute(() => {
823+
return this.bpClient.setSynchronization(!this.internalIgnoreSynchronization);
824+
});
825+
}
826+
771827
this.driver.controlFlow().execute(() => {
772828
return this.plugins_.onPageStable().then(() => {
773829
deferred.fulfill();

lib/config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ export interface Config {
9595
*/
9696
webDriverProxy?: string;
9797

98+
/**
99+
* If specified, connect to webdriver through a proxy that manages client-side
100+
* synchronization. Blocking Proxy is an experimental feature and may change
101+
* without notice.
102+
*/
103+
useBlockingProxy?: boolean;
104+
98105
// ---- 3. To use remote browsers via Sauce Labs -----------------------------
99106

100107
/**

lib/driverProviders/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Each file exports a function which takes in the configuration as a parameter and
1111
* @return {q.promise} A promise which will resolve when the environment is
1212
* ready to test.
1313
*/
14-
DriverProvider.prototype.setupEnv
14+
DriverProvider.prototype.setupDriverEnv
1515

1616
/**
1717
* @return {Array.<webdriver.WebDriver>} Array of existing webdriver instances.
@@ -47,9 +47,9 @@ DriverProvider.prototype.updateJob
4747
Requirements
4848
------------
4949

50-
- `setupEnv` will be called before the test framework is loaded, so any
50+
- `setupDriverEnv` will be called before the test framework is loaded, so any
5151
pre-work which might cause timeouts on the first test should be done there.
52-
`getNewDriver` will be called once right after `setupEnv` to generate the
52+
`getNewDriver` will be called once right after `setupDriverEnv` to generate the
5353
initial driver, and possibly during the middle of the test if users request
5454
additional browsers.
5555

lib/driverProviders/attachSession.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ export class AttachSession extends DriverProvider {
2222

2323
/**
2424
* Configure and launch (if applicable) the object's environment.
25-
* @public
2625
* @return {q.promise} A promise which will resolve when the environment is
2726
* ready to test.
2827
*/
29-
setupEnv(): q.Promise<any> {
28+
protected setupDriverEnv(): q.Promise<any> {
3029
logger.info('Using the selenium server at ' + this.config_.seleniumAddress);
3130
logger.info('Using session id - ' + this.config_.seleniumSessionId);
3231
return q(undefined);

lib/driverProviders/browserStack.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ export class BrowserStack extends DriverProvider {
8989

9090
/**
9191
* Configure and launch (if applicable) the object's environment.
92-
* @public
9392
* @return {q.promise} A promise which will resolve when the environment is
9493
* ready to test.
9594
*/
96-
setupEnv(): q.Promise<any> {
95+
protected setupDriverEnv(): q.Promise<any> {
9796
var deferred = q.defer();
9897
this.config_.capabilities['browserstack.user'] = this.config_.browserstackUser;
9998
this.config_.capabilities['browserstack.key'] = this.config_.browserstackKey;

lib/driverProviders/direct.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ export class Direct extends DriverProvider {
2626

2727
/**
2828
* Configure and launch (if applicable) the object's environment.
29-
* @public
3029
* @return {q.promise} A promise which will resolve when the environment is
3130
* ready to test.
3231
*/
33-
setupEnv(): q.Promise<any> {
32+
protected setupDriverEnv(): q.Promise<any> {
3433
switch (this.config_.capabilities.browserName) {
3534
case 'chrome':
3635
logger.info('Using ChromeDriver directly...');

lib/driverProviders/driverProvider.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
*/
66
import * as q from 'q';
77

8+
import {BlockingProxyRunner} from '../bpRunner';
89
import {Config} from '../config';
910

1011
let webdriver = require('selenium-webdriver');
1112

12-
export class DriverProvider {
13+
export abstract class DriverProvider {
1314
drivers_: webdriver.WebDriver[];
1415
config_: Config;
16+
private bpRunner: BlockingProxyRunner;
1517

1618
constructor(config: Config) {
1719
this.config_ = config;
1820
this.drivers_ = [];
21+
this.bpRunner = new BlockingProxyRunner(config);
1922
}
2023

2124
/**
@@ -28,17 +31,28 @@ export class DriverProvider {
2831
return this.drivers_.slice(); // Create a shallow copy
2932
}
3033

34+
getBPUrl() {
35+
return `http://localhost:${this.bpRunner.port}`;
36+
}
37+
3138
/**
3239
* Create a new driver.
3340
*
3441
* @public
3542
* @return webdriver instance
3643
*/
3744
getNewDriver() {
38-
let builder = new webdriver.Builder()
39-
.usingServer(this.config_.seleniumAddress)
40-
.usingWebDriverProxy(this.config_.webDriverProxy)
41-
.withCapabilities(this.config_.capabilities);
45+
let builder: webdriver.Builder;
46+
if (this.config_.useBlockingProxy) {
47+
builder = new webdriver.Builder()
48+
.usingServer(this.getBPUrl())
49+
.withCapabilities(this.config_.capabilities);
50+
} else {
51+
builder = new webdriver.Builder()
52+
.usingServer(this.config_.seleniumAddress)
53+
.usingWebDriverProxy(this.config_.webDriverProxy)
54+
.withCapabilities(this.config_.capabilities);
55+
}
4256
if (this.config_.disableEnvironmentOverrides === true) {
4357
builder.disableEnvironmentOverrides();
4458
}
@@ -89,13 +103,23 @@ export class DriverProvider {
89103
};
90104

91105
/**
92-
* Default setup environment method.
93-
* @return a promise
106+
* Default setup environment method, common to all driver providers.
94107
*/
95108
setupEnv(): q.Promise<any> {
96-
return q.fcall(function() {});
109+
let driverPromise = this.setupDriverEnv();
110+
if (this.config_.useBlockingProxy) {
111+
// TODO(heathkit): If set, pass the webDriverProxy to BP.
112+
return q.all([driverPromise, this.bpRunner.start()]);
113+
}
114+
return driverPromise;
97115
};
98116

117+
/**
118+
* Set up environment specific to a particular driver provider. Overridden
119+
* by each driver provider.
120+
*/
121+
protected abstract setupDriverEnv(): q.Promise<any>;
122+
99123
/**
100124
* Teardown and destroy the environment and do any associated cleanup.
101125
* Shuts down the drivers.

lib/driverProviders/hosted.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class Hosted extends DriverProvider {
2222
* @return {q.promise} A promise which will resolve when the environment is
2323
* ready to test.
2424
*/
25-
setupEnv(): q.Promise<any> {
25+
protected setupDriverEnv(): q.Promise<any> {
2626
logger.info('Using the selenium server at ' + this.config_.seleniumAddress);
2727
return q.fcall(function() {});
2828
}

lib/driverProviders/local.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class Local extends DriverProvider {
7575
* @return {q.promise} A promise which will resolve when the environment is
7676
* ready to test.
7777
*/
78-
setupEnv(): q.Promise<any> {
78+
protected setupDriverEnv(): q.Promise<any> {
7979
let deferred = q.defer();
8080

8181
this.addDefaultBinaryLocs_();

lib/driverProviders/mock.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class Mock extends DriverProvider {
2828
* @public
2929
* @return {q.promise} A promise which will resolve immediately.
3030
*/
31-
setupEnv(): q.Promise<any> {
31+
protected setupDriverEnv(): q.Promise<any> {
3232
return q.fcall(function() {});
3333
}
3434

lib/driverProviders/sauce.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class Sauce extends DriverProvider {
5151
* @return {q.promise} A promise which will resolve when the environment is
5252
* ready to test.
5353
*/
54-
setupEnv(): q.Promise<any> {
54+
protected setupDriverEnv(): q.Promise<any> {
5555
let deferred = q.defer();
5656
this.sauceServer_ = new SauceLabs({
5757
username: this.config_.sauceUser,

lib/runner.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,14 @@ export class Runner extends EventEmitter {
206206
var config = this.config_;
207207
var driver = this.driverprovider_.getNewDriver();
208208

209+
let blockingProxyUrl: string;
210+
if (config.useBlockingProxy) {
211+
blockingProxyUrl = this.driverprovider_.getBPUrl();
212+
}
213+
209214
var browser_ = new ProtractorBrowser(
210-
driver, config.baseUrl, config.rootElement, config.untrackOutstandingTimeouts);
215+
driver, config.baseUrl, config.rootElement, config.untrackOutstandingTimeouts,
216+
blockingProxyUrl);
211217

212218
browser_.params = config.params;
213219
if (plugins) {

0 commit comments

Comments
 (0)