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

Commit 0e26b21

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 681b54a commit 0e26b21

File tree

18 files changed

+209
-31
lines changed

18 files changed

+209
-31
lines changed

lib/bpRunner.ts

Lines changed: 62 additions & 0 deletions
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

Lines changed: 59 additions & 3 deletions
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

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

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

194218
/**
195219
* Timeout in milliseconds to wait for pages to load when calling `get`.
@@ -273,7 +297,7 @@ export class ProtractorBrowser extends Webdriver {
273297

274298
constructor(
275299
webdriverInstance: WebDriver, opt_baseUrl?: string, opt_rootElement?: string,
276-
opt_untrackOutstandingTimeouts?: boolean) {
300+
opt_untrackOutstandingTimeouts?: boolean, opt_blockingProxyUrl?: string) {
277301
super();
278302
// These functions should delegate to the webdriver instance, but should
279303
// wait for Angular to sync up before performing the action. This does not
@@ -292,6 +316,10 @@ export class ProtractorBrowser extends Webdriver {
292316
});
293317

294318
this.driver = webdriverInstance;
319+
if (opt_blockingProxyUrl) {
320+
logger.info('Starting BP client for ' + opt_blockingProxyUrl);
321+
this.bpClient = new BPClient(opt_blockingProxyUrl);
322+
}
295323
this.element = buildElementHelper(this);
296324
this.$ = build$(this.element, By);
297325
this.$$ = build$$(this.element, By);
@@ -342,6 +370,22 @@ export class ProtractorBrowser extends Webdriver {
342370
this.ExpectedConditions = new ProtractorExpectedConditions(this);
343371
}
344372

373+
/**
374+
* If set to false, Protractor will not wait for Angular $http and $timeout
375+
* tasks to complete before interacting with the browser. This can cause
376+
* flaky tests, but should be used if, for instance, your app continuously
377+
* polls an API with $timeout.
378+
*
379+
* Call waitForAngularEnabled() without passing a value to read the current
380+
* state without changing it.
381+
*/
382+
waitForAngularEnabled(enabled: boolean = null): boolean {
383+
if (enabled != null) {
384+
this.ignoreSynchronization = !enabled;
385+
}
386+
return !this.ignoreSynchronization;
387+
}
388+
345389
/**
346390
* Get the processed configuration object that is currently being run. This
347391
* will contain the specs and capabilities properties of the current runner
@@ -462,7 +506,7 @@ export class ProtractorBrowser extends Webdriver {
462506
}
463507

464508
let runWaitForAngularScript: () => wdpromise.Promise<any> = () => {
465-
if (this.plugins_.skipAngularStability()) {
509+
if (this.plugins_.skipAngularStability() || this.bpClient) {
466510
return wdpromise.fulfilled();
467511
} else if (this.rootEl) {
468512
return this.executeAsyncScript_(
@@ -690,6 +734,12 @@ export class ProtractorBrowser extends Webdriver {
690734
return 'Protractor.get(' + destination + ') - ' + str;
691735
};
692736

737+
if (this.bpClient) {
738+
this.driver.controlFlow().execute(() => {
739+
return this.bpClient.setSynchronization(false);
740+
});
741+
}
742+
693743
if (this.ignoreSynchronization) {
694744
this.driver.get(destination);
695745
return this.driver.controlFlow().execute(() => this.plugins_.onPageLoad()).then(() => {});
@@ -790,6 +840,12 @@ export class ProtractorBrowser extends Webdriver {
790840
}
791841
}
792842

843+
if (this.bpClient) {
844+
this.driver.controlFlow().execute(() => {
845+
return this.bpClient.setSynchronization(!this.internalIgnoreSynchronization);
846+
});
847+
}
848+
793849
this.driver.controlFlow().execute(() => {
794850
return this.plugins_.onPageStable().then(() => {
795851
deferred.fulfill();

lib/config.ts

Lines changed: 7 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,10 @@ export class BrowserStack extends DriverProvider {
9090

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

lib/driverProviders/direct.ts

Lines changed: 1 addition & 2 deletions
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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
import * as q from 'q';
77
import {Builder, Session, WebDriver} from 'selenium-webdriver';
88

9+
import {BlockingProxyRunner} from '../bpRunner';
910
import {Config} from '../config';
1011

11-
export class DriverProvider {
12+
export abstract class DriverProvider {
1213
drivers_: WebDriver[];
1314
config_: Config;
15+
private bpRunner: BlockingProxyRunner;
1416

1517
constructor(config: Config) {
1618
this.config_ = config;
1719
this.drivers_ = [];
20+
this.bpRunner = new BlockingProxyRunner(config);
1821
}
1922

2023
/**
@@ -27,17 +30,28 @@ export class DriverProvider {
2730
return this.drivers_.slice(); // Create a shallow copy
2831
}
2932

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

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

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

lib/driverProviders/hosted.ts

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class Local extends DriverProvider {
7777
* @return {q.promise} A promise which will resolve when the environment is
7878
* ready to test.
7979
*/
80-
setupEnv(): q.Promise<any> {
80+
protected setupDriverEnv(): q.Promise<any> {
8181
let deferred = q.defer();
8282

8383
this.addDefaultBinaryLocs_();

0 commit comments

Comments
 (0)