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

Commit e9e13ff

Browse files
authored
chore(refactor): Pull proxy logic out and add unit tests around it. (#22)
This pulls out the the proxy/wait behavior into a separate module so we can unit test it. The idea is that the proxy waits for promises from barriers to resolve before proceeding.
1 parent d7ac321 commit e9e13ff

11 files changed

+480
-72
lines changed

lib/blockingproxy.ts

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as http from 'http';
22
import * as url from 'url';
33

4-
import {parseWebDriverCommand} from './webdriverCommands';
5-
import {WebDriverLogger} from './webdriverLogger';
4+
import {WebDriverCommand} from './webdriver_commands';
5+
import {WebDriverLogger} from './webdriver_logger';
6+
import {WebDriverBarrier, WebDriverProxy} from './webdriver_proxy';
67

78
let angularWaits = require('./angular/wait.js');
89
export const BP_PREFIX = 'bpproxy';
@@ -12,20 +13,23 @@ export const BP_PREFIX = 'bpproxy';
1213
* JSON webdriver commands. It keeps track of whether the page under test
1314
* needs to wait for page stability, and initiates a wait if so.
1415
*/
15-
export class BlockingProxy {
16+
export class BlockingProxy implements WebDriverBarrier {
1617
seleniumAddress: string;
1718

1819
// The ng-app root to use when waiting on the client.
1920
rootSelector = '';
2021
waitEnabled: boolean;
2122
server: http.Server;
2223
logger: WebDriverLogger;
24+
private proxy: WebDriverProxy;
2325

2426
constructor(seleniumAddress) {
2527
this.seleniumAddress = seleniumAddress;
2628
this.rootSelector = '';
2729
this.waitEnabled = true;
2830
this.server = http.createServer(this.requestListener.bind(this));
31+
this.proxy = new WebDriverProxy(seleniumAddress);
32+
this.proxy.addBarrier(this);
2933
}
3034

3135
waitForAngularData() {
@@ -194,11 +198,10 @@ export class BlockingProxy {
194198
}
195199
}
196200

197-
sendRequestToStabilize(originalRequest) {
198-
let self = this;
199-
let deferred = new Promise((resolve, reject) => {
200-
let stabilityRequest = self.createSeleniumRequest(
201-
'POST', BlockingProxy.executeAsyncUrl(originalRequest.url), function(stabilityResponse) {
201+
sendRequestToStabilize(url: string): Promise<void> {
202+
return new Promise<void>((resolve, reject) => {
203+
let stabilityRequest = this.createSeleniumRequest(
204+
'POST', BlockingProxy.executeAsyncUrl(url), function(stabilityResponse) {
202205
// TODO - If the response is that angular is not available on the
203206
// page, should we just go ahead and continue?
204207
let stabilityData = '';
@@ -227,68 +230,35 @@ export class BlockingProxy {
227230
stabilityRequest.write(this.waitForAngularData());
228231
stabilityRequest.end();
229232
});
230-
231-
return deferred;
232233
}
233234

234235
requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) {
235-
let self = this;
236-
let stabilized = Promise.resolve(null);
237-
238236
if (BlockingProxy.isProxyCommand(originalRequest.url)) {
239237
let commandData = '';
240238
originalRequest.on('data', (d) => {
241239
commandData += d;
242240
});
243241
originalRequest.on('end', () => {
244-
self.handleProxyCommand(originalRequest, commandData, response);
242+
this.handleProxyCommand(originalRequest, commandData, response);
245243
});
246244
return;
247245
}
248246

249-
// If the command is not a proxy command, it's a regular webdriver command.
250-
if (self.shouldStabilize(originalRequest.url)) {
251-
stabilized = self.sendRequestToStabilize(originalRequest);
247+
// OK to ignore the promise returned by this.
248+
this.proxy.handleRequest(originalRequest, response);
249+
}
252250

253-
// TODO: Log waiting for Angular.
251+
onCommand(command: WebDriverCommand): Promise<void> {
252+
if (this.logger) {
253+
command.on('data', () => {
254+
this.logger.logWebDriverCommand(command);
255+
});
254256
}
255257

256-
stabilized.then(
257-
() => {
258-
let seleniumRequest = self.createSeleniumRequest(
259-
originalRequest.method, originalRequest.url, function(seleniumResponse) {
260-
response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers);
261-
seleniumResponse.pipe(response);
262-
seleniumResponse.on('error', (err) => {
263-
response.writeHead(500);
264-
response.write(err);
265-
response.end();
266-
});
267-
});
268-
let reqData = '';
269-
originalRequest.on('error', (err) => {
270-
response.writeHead(500);
271-
response.write(err);
272-
response.end();
273-
});
274-
originalRequest.on('data', (d) => {
275-
reqData += d;
276-
seleniumRequest.write(d);
277-
});
278-
originalRequest.on('end', () => {
279-
let command =
280-
parseWebDriverCommand(originalRequest.url, originalRequest.method, reqData);
281-
if (this.logger) {
282-
this.logger.logWebDriverCommand(command);
283-
}
284-
seleniumRequest.end();
285-
});
286-
},
287-
(err) => {
288-
response.writeHead(500);
289-
response.write(err);
290-
response.end();
291-
});
258+
if (this.shouldStabilize(command.url)) {
259+
return this.sendRequestToStabilize(command.url);
260+
}
261+
return Promise.resolve(null);
292262
}
293263

294264
listen(port: number) {

lib/webdriverCommands.ts renamed to lib/webdriver_commands.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Utilities for parsing WebDriver commands from HTTP Requests.
33
*/
4+
import * as events from 'events';
45

56
type HttpMethod = 'GET'|'POST'|'DELETE';
67
export type paramKey = 'sessionId' | 'elementId' | 'name' | 'propertyName';
@@ -86,16 +87,42 @@ class Endpoint {
8687
* @param params Parameters for the command taken from the request's url.
8788
* @param data Optional data included with the command, taken from the body of the request.
8889
*/
89-
export class WebDriverCommand {
90+
export class WebDriverCommand extends events.EventEmitter {
9091
private params: {[key: string]: string};
92+
data: any;
93+
responseStatus: number;
94+
responseData: number;
95+
96+
// All WebDriver commands have a session Id, except for two.
97+
// NewSession will have a session Id in the data
98+
// Status just doesn't
99+
get sessionId(): string {
100+
return this.getParam('sessionId');
101+
}
91102

92-
constructor(public commandName: CommandName, params?, public data?: any) {
103+
constructor(public commandName: CommandName, public url: string, params?) {
104+
super();
93105
this.params = params;
94106
}
95107

96108
public getParam(key: paramKey) {
97109
return this.params[key];
98110
}
111+
112+
public handleData(data?: any) {
113+
if (data) {
114+
this.data = JSON.parse(data);
115+
}
116+
this.emit('data');
117+
}
118+
119+
public handleResponse(statusCode: number, data?: any) {
120+
this.responseStatus = statusCode;
121+
if (data) {
122+
this.responseData = JSON.parse(data);
123+
}
124+
this.emit('response');
125+
}
99126
}
100127

101128

@@ -111,26 +138,21 @@ function addWebDriverCommand(command: CommandName, method: HttpMethod, pattern:
111138
/**
112139
* Returns a new WebdriverCommand object for the resource at the given URL.
113140
*/
114-
export function parseWebDriverCommand(url, method, data: string) {
115-
let parsedData = {};
116-
if (data) {
117-
parsedData = JSON.parse(data);
118-
}
119-
141+
export function parseWebDriverCommand(url, method) {
120142
for (let endpoint of endpoints) {
121143
if (endpoint.matches(url, method)) {
122144
let params = endpoint.getParams(url);
123-
return new WebDriverCommand(endpoint.name, params, parsedData);
145+
return new WebDriverCommand(endpoint.name, url, params);
124146
}
125147
}
126148

127-
return new WebDriverCommand(CommandName.UNKNOWN, {}, {'url': url});
149+
return new WebDriverCommand(CommandName.UNKNOWN, url, {});
128150
}
129151

130152
let sessionPrefix = '/session/:sessionId';
131153
addWebDriverCommand(CommandName.NewSession, 'POST', '/session');
132154
addWebDriverCommand(CommandName.DeleteSession, 'DELETE', '/session/:sessionId');
133-
addWebDriverCommand(CommandName.Status, 'GET', sessionPrefix + '/status');
155+
addWebDriverCommand(CommandName.Status, 'GET', '/status');
134156
addWebDriverCommand(CommandName.GetTimeouts, 'GET', sessionPrefix + '/timeouts');
135157
addWebDriverCommand(CommandName.SetTimeouts, 'POST', sessionPrefix + '/timeouts');
136158
addWebDriverCommand(CommandName.Go, 'POST', sessionPrefix + '/url');

lib/webdriverLogger.ts renamed to lib/webdriver_logger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import * as stream from 'stream';
44

5-
import {CommandName, WebDriverCommand} from './webdriverCommands';
5+
import {CommandName, WebDriverCommand} from './webdriver_commands';
66

77
// Generate a random 8 character ID to avoid collisions.
88
function getLogId() {
@@ -65,7 +65,7 @@ export class WebDriverLogger {
6565
case CommandName.GetCurrentURL:
6666
return `Getting current URL`;
6767
default:
68-
return `Unknown command ${command.data['url']}`;
68+
return `Unknown command ${command.url}`;
6969
}
7070
}
7171

lib/webdriver_proxy.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as http from 'http';
2+
import * as url from 'url';
3+
4+
import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands';
5+
6+
/**
7+
* A proxy that understands WebDriver commands. Users can add barriers (similar to middleware in
8+
* express) that will be called before forwarding the request to WebDriver. The proxy will wait for
9+
* each barrier to finish, calling them in the order in which they were added.
10+
*/
11+
export class WebDriverProxy {
12+
barriers: WebDriverBarrier[];
13+
seleniumAddress: string;
14+
15+
constructor(seleniumAddress: string) {
16+
this.barriers = [];
17+
this.seleniumAddress = seleniumAddress;
18+
}
19+
20+
addBarrier(barrier: WebDriverBarrier) {
21+
this.barriers.push(barrier);
22+
}
23+
24+
async handleRequest(originalRequest: http.IncomingMessage, response: http.ServerResponse) {
25+
let command = parseWebDriverCommand(originalRequest.url, originalRequest.method);
26+
27+
let replyWithError = (err) => {
28+
response.writeHead(500);
29+
response.write(err.toString());
30+
response.end();
31+
};
32+
33+
// Process barriers in order, one at a time.
34+
try {
35+
for (let barrier of this.barriers) {
36+
await barrier.onCommand(command);
37+
}
38+
} catch (err) {
39+
replyWithError(err);
40+
// Don't call through if a barrier fails.
41+
return;
42+
}
43+
44+
let parsedUrl = url.parse(this.seleniumAddress);
45+
let options: http.RequestOptions = {};
46+
options.method = originalRequest.method;
47+
options.path = parsedUrl.path + originalRequest.url;
48+
options.hostname = parsedUrl.hostname;
49+
options.port = parseInt(parsedUrl.port);
50+
options.headers = originalRequest.headers;
51+
52+
let forwardedRequest = http.request(options);
53+
54+
// clang-format off
55+
let reqData = '';
56+
originalRequest.on('data', (d) => {
57+
reqData += d;
58+
forwardedRequest.write(d);
59+
}).on('end', () => {
60+
command.handleData(reqData);
61+
forwardedRequest.end();
62+
}).on('error', replyWithError);
63+
64+
forwardedRequest.on('response', (seleniumResponse) => {
65+
response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers);
66+
67+
let respData = '';
68+
seleniumResponse.on('data', (d) => {
69+
respData += d;
70+
response.write(d);
71+
}).on('end', () => {
72+
command.handleResponse(seleniumResponse.statusCode, respData);
73+
response.end();
74+
}).on('error', replyWithError);
75+
76+
}).on('error', replyWithError);
77+
// clang-format on
78+
}
79+
}
80+
81+
/**
82+
* When the proxy receives a WebDriver command, it will call onCommand() for each of it's barriers.
83+
* Barriers may return a promise for the proxy to wait for before proceeding. If the promise is
84+
* rejected, the proxy will reply with an error code and the result of the promise and the command
85+
* will not be forwarded to Selenium.
86+
*/
87+
export interface WebDriverBarrier { onCommand(command: WebDriverCommand): Promise<void>; }

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"devDependencies": {
2020
"@types/jasmine": "^2.2.33",
2121
"@types/minimist": "^1.1.29",
22+
"@types/nock": "^8.2.0",
2223
"@types/node": "^6.0.45",
2324
"@types/rimraf": "0.0.28",
2425
"@types/selenium-webdriver": "^2.53.39",
@@ -31,12 +32,13 @@
3132
"jasmine-co": "^1.2.2",
3233
"jasmine-ts": "0.0.3",
3334
"jshint": "2.9.1",
35+
"nock": "^9.0.2",
3436
"rimraf": "^2.5.4",
3537
"run-sequence": "^1.2.2",
3638
"selenium-mock": "^0.1.5",
3739
"selenium-webdriver": "2.53.3",
3840
"ts-node": "^2.0.0",
39-
"tslint": "^4.0.2",
41+
"tslint": "^4.3.1",
4042
"tslint-eslint-rules": "^3.1.0",
4143
"typescript": "^2.0.3",
4244
"vrsource-tslint-rules": "^0.14.1",

spec/e2e/logging_spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {BP_URL, getTestEnv} from './environment';
1212
Example log of a test session:
1313
1414
[12:51:30] Getting new "chrome" session
15-
[12:51:33] [abcdef] Navigating to 'http://localhost/stuff'
16-
[12:51:35] [abcdef] Wait for Angular
17-
[12:51:36] [abcdef] Click on css '.test_element'
15+
[12:51:33] [abcdef] [0.5s] Navigating to 'http://localhost/stuff'
16+
[12:51:35] [abcdef] [0.3s] Wait for Angular
17+
[12:51:36] [abcdef] [0.01s] Click on css '.test_element'
1818
[12:51:36] [abcdef] Move mouse by (0,50)
1919
[12:51:37] [abcdef] Click on binding 'thinger'
2020
*/

0 commit comments

Comments
 (0)