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

chore(refactor): Pull proxy logic out and add unit tests around it. #22

Merged
merged 12 commits into from
Jan 23, 2017
Prev Previous commit
Next Next commit
Trying out commands as event emitters.
  • Loading branch information
heathkit committed Jan 23, 2017
commit a22172eb2b5c911690d738493733d285cb4efa31
32 changes: 24 additions & 8 deletions lib/blockingproxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as url from 'url';

import {parseWebDriverCommand} from './webdriver_commands';
import {WebDriverLogger} from './webdriver_logger';
import {WebDriverBarrier, WebDriverProxy} from "./webdriver_proxy";
import {WebDriverCommand} from "./webdriver_commands";

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

// The ng-app root to use when waiting on the client.
rootSelector = '';
waitEnabled: boolean;
server: http.Server;
logger: WebDriverLogger;
private proxy: WebDriverProxy;

constructor(seleniumAddress) {
this.seleniumAddress = seleniumAddress;
this.rootSelector = '';
this.waitEnabled = true;
this.server = http.createServer(this.requestListener.bind(this));
this.proxy = new WebDriverProxy(seleniumAddress);
this.proxy.addBarrier(this);
}

waitForAngularData() {
Expand Down Expand Up @@ -194,11 +199,11 @@ export class BlockingProxy {
}
}

sendRequestToStabilize(originalRequest) {
sendRequestToStabilize(url: string) {
let self = this;
let deferred = new Promise((resolve, reject) => {
let stabilityRequest = self.createSeleniumRequest(
'POST', BlockingProxy.executeAsyncUrl(originalRequest.url), function(stabilityResponse) {
'POST', BlockingProxy.executeAsyncUrl(url), function(stabilityResponse) {
// TODO - If the response is that angular is not available on the
// page, should we just go ahead and continue?
let stabilityData = '';
Expand Down Expand Up @@ -232,21 +237,31 @@ export class BlockingProxy {
}

requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) {
let self = this;
let stabilized = Promise.resolve(null);

if (BlockingProxy.isProxyCommand(originalRequest.url)) {
let commandData = '';
originalRequest.on('data', (d) => {
commandData += d;
});
originalRequest.on('end', () => {
self.handleProxyCommand(originalRequest, commandData, response);
this.handleProxyCommand(originalRequest, commandData, response);
});
return;
}

this.proxy.requestLister(originalRequest, response);
this.proxy.requestListener(originalRequest, response);
}

onCommand(command: WebDriverCommand): Promise<void> {
let stabilized = Promise.resolve(null);

if (this.shouldStabilize(command.url)) {
stabilized = this.sendRequestToStabilize(command.url);
}
command.on('data', () => {
console.log('Got data', command);
this.logger.logWebDriverCommand(command);
});
return stabilized;
}

listen(port: number) {
Expand All @@ -261,3 +276,4 @@ export class BlockingProxy {
});
}
}

34 changes: 24 additions & 10 deletions lib/webdriver_commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Utilities for parsing WebDriver commands from HTTP Requests.
*/
import * as http from 'http';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think http is unused here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I'll turn on the no-unused-variable lint check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, rather, TypeScript 2.1 now has --noUnusedLocals

import * as events from 'events';

type HttpMethod = 'GET'|'POST'|'DELETE';
export type paramKey = 'sessionId' | 'elementId' | 'name' | 'propertyName';
Expand Down Expand Up @@ -86,16 +88,33 @@ class Endpoint {
* @param params Parameters for the command taken from the request's url.
* @param data Optional data included with the command, taken from the body of the request.
*/
export class WebDriverCommand {
export class WebDriverCommand extends events.EventEmitter {
private params: {[key: string]: string};
data: any;
responseStatus: number;
responseData: number;

constructor(public commandName: CommandName, params?, public data?: any) {
constructor(public commandName: CommandName, public url: string, params?) {
super();
this.params = params;
}

public getParam(key: paramKey) {
return this.params[key];
}

public handleData(data?: any) {
if (data) {
this.data = JSON.parse(data);
}
this.emit('data');
}

public handleResponse(statusCode: number, data?: any) {
this.responseStatus = statusCode;
this.responseData = data;
this.emit('response');
}
}


Expand All @@ -111,20 +130,15 @@ function addWebDriverCommand(command: CommandName, method: HttpMethod, pattern:
/**
* Returns a new WebdriverCommand object for the resource at the given URL.
*/
export function parseWebDriverCommand(url, method, data: string) {
let parsedData = {};
if (data) {
parsedData = JSON.parse(data);
}

export function parseWebDriverCommand(url, method) {
for (let endpoint of endpoints) {
if (endpoint.matches(url, method)) {
let params = endpoint.getParams(url);
return new WebDriverCommand(endpoint.name, params, parsedData);
return new WebDriverCommand(endpoint.name, url, params);
}
}

return new WebDriverCommand(CommandName.UNKNOWN, {}, {'url': url});
return new WebDriverCommand(CommandName.UNKNOWN, url, {});
}

let sessionPrefix = '/session/:sessionId';
Expand Down
1 change: 1 addition & 0 deletions lib/webdriver_logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class WebDriverLogger {
switch (command.commandName) {
case CommandName.NewSession:
let desired = command.data['desiredCapabilities'];
console.log(desired);
return `Getting new "${desired['browserName']}" session`;
case CommandName.DeleteSession:
let sessionId = command.getParam('sessionId').slice(0, 6);
Expand Down
111 changes: 60 additions & 51 deletions lib/webdriver_proxy.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,81 @@
import {parseWebDriverCommand} from './webdriverCommands';
import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands';
import * as http from 'http';
import * as url from 'url';

/**
* A proxy that understands WebDriver commands. Users can add middleware (similar to middleware in
* express) that will be called before
* forwarding the request to WebDriver or forwarding the response to the client.
*/
export class WebdriverProxy {
export class WebDriverProxy {
barriers: WebDriverBarrier[];
seleniumAddress: string;

constructor() {
constructor(seleniumAddress: string) {
this.barriers = [];
this.seleniumAddress = seleniumAddress;
}

addBarrier(barrier: WebDriverBarrier) {
this.barriers.push(barrier);
}

addMiddleware(middleware: WebDriverMiddleware) {
async requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) {

}
let command = parseWebDriverCommand(originalRequest.url, originalRequest.method);

requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) {
let reqData = '';
originalRequest.on('data', (d) => {
reqData += d;
});
originalRequest.on('end', () => {
command.handleData(reqData);
});

// TODO: What happens when barriers error? return a client error?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should reply to the initial response with a 500 and an error message chosen by the barrier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, if the barrier rejects its promise, we use that value as the error.

for (let barrier of this.barriers) {
await barrier.onCommand(command);
}

let stabilized = Promise.resolve(null);
let parsedUrl = url.parse(this.seleniumAddress);
let options: http.RequestOptions = {};
options.method = originalRequest.method;
options.path = parsedUrl.path + originalRequest.url;
options.hostname = parsedUrl.hostname;
options.port = parseInt(parsedUrl.port);
options.headers = originalRequest.rawHeaders;

// If the command is not a proxy command, it's a regular webdriver command.
if (self.shouldStabilize(originalRequest.url)) {
stabilized = self.sendRequestToStabilize(originalRequest);
originalRequest.on('error', (err) => {
response.writeHead(500);
response.end(err);
});

// TODO: Log waiting for Angular.
}
let forwardedRequest = http.request(options, (seleniumResponse) => {
response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers);
let respData = '';
seleniumResponse.on('data', (d) => {
respData += d;
response.write(d);
});
seleniumResponse.on('end', () => {
command.handleResponse(seleniumResponse.statusCode, respData);
response.end();
});
seleniumResponse.on('error', (err) => {
response.writeHead(500);
response.end(err);
});
});

stabilized.then(
() => {
let seleniumRequest = self.createSeleniumRequest(
originalRequest.method, originalRequest.url, function(seleniumResponse) {
response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers);
seleniumResponse.pipe(response);
seleniumResponse.on('error', (err) => {
response.writeHead(500);
response.write(err);
response.end();
});
});
let reqData = '';
originalRequest.on('error', (err) => {
response.writeHead(500);
response.write(err);
response.end();
});
originalRequest.on('data', (d) => {
reqData += d;
seleniumRequest.write(d);
});
originalRequest.on('end', () => {
let command =
parseWebDriverCommand(originalRequest.url, originalRequest.method, reqData);
if (this.logger) {
this.logger.logWebDriverCommand(command);
}
seleniumRequest.end();
});
},
(err) => {
response.writeHead(500);
response.write(err);
response.end();
});
originalRequest.on('data', (d) => {
forwardedRequest.write(d);
});
originalRequest.on('end', () => {
forwardedRequest.end();
});
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class-level comment for WebDriverBarrier would be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

export class WebDriverMiddleware {
onRequest() {
}
export interface WebDriverBarrier {
onCommand(command: WebDriverCommand): Promise<void>;
}
34 changes: 18 additions & 16 deletions spec/unit/webdriver_commands_spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import {Server} from 'selenium-mock';
import * as webdriver from 'selenium-webdriver';

import {BlockingProxy} from '../../lib/blockingproxy';
import {getMockSelenium, Session} from '../helpers/mock_selenium';
import {WebDriverBarrier, WebDriverProxy} from "../../lib/webdriver_proxy";
import * as http from 'http';
import {WebDriverCommand} from "../../lib/webdriver_commands";


const capabilities = webdriver.Capabilities.chrome();

class TestBarrier implements WDBarrier {
class TestBarrier implements WebDriverBarrier {
commands: WebDriverCommand[] = [];

onCommand(command: WebDriverCommand): Promise<void> {

return undefined;
}
}

describe('WebDriver logger', () => {
describe('WebDriver command parser', () => {
let mockServer: Server<Session>;
let driver: webdriver.WebDriver;
let proxy: BlockingProxy;
let proxy: WebDriverProxy;
let bpPort: number;
let server: http.Server;

beforeAll(() => {
beforeAll(async() => {
mockServer = getMockSelenium();
mockServer.start();
let mockPort = mockServer.handle.address().port;

proxy = new BlockingProxy(`http://localhost:${mockPort}/wd/hub`);
bpPort = proxy.listen(0);
});
proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`);
server = http.createServer(proxy.requestListener.bind(proxy));

beforeEach(async() => {
driver = new webdriver.Builder()
.usingServer(`http://localhost:${bpPort}`)
.withCapabilities(capabilities)
Expand All @@ -38,18 +45,13 @@ describe('WebDriver logger', () => {
afterEach(() => {
});

it('parses session commands', async() => {
xit('handles session commands', async() => {
let session = await driver.getSession();


});

it('parses url commands', async() => {
await driver.getCurrentUrl();

let log = logger.getLog();
expect(log[1]).toContain('Navigating to http://example.com');
expect(log[2]).toContain('Getting current URL');
xit('handles url commands', async() => {
});

afterAll(() => {
Expand Down
Loading