From e9460ce8f1cb05250c5f4c73e6443ed8ccac155f Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 13:20:42 -0800 Subject: [PATCH 01/12] Fix naming. --- lib/blockingproxy.ts | 4 ++-- lib/{webdriverCommands.ts => webdriver_commands.ts} | 0 lib/{webdriverLogger.ts => webdriver_logger.ts} | 2 +- .../{webdriverLogger_spec.ts => webdriver_logger_spec.ts} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename lib/{webdriverCommands.ts => webdriver_commands.ts} (100%) rename lib/{webdriverLogger.ts => webdriver_logger.ts} (97%) rename spec/unit/{webdriverLogger_spec.ts => webdriver_logger_spec.ts} (97%) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 7060467..228b9d4 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -1,8 +1,8 @@ import * as http from 'http'; import * as url from 'url'; -import {parseWebDriverCommand} from './webdriverCommands'; -import {WebDriverLogger} from './webdriverLogger'; +import {parseWebDriverCommand} from './webdriver_commands'; +import {WebDriverLogger} from './webdriver_logger'; let angularWaits = require('./angular/wait.js'); export const BP_PREFIX = 'bpproxy'; diff --git a/lib/webdriverCommands.ts b/lib/webdriver_commands.ts similarity index 100% rename from lib/webdriverCommands.ts rename to lib/webdriver_commands.ts diff --git a/lib/webdriverLogger.ts b/lib/webdriver_logger.ts similarity index 97% rename from lib/webdriverLogger.ts rename to lib/webdriver_logger.ts index 9051b68..6795152 100644 --- a/lib/webdriverLogger.ts +++ b/lib/webdriver_logger.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as stream from 'stream'; -import {CommandName, WebDriverCommand} from './webdriverCommands'; +import {CommandName, WebDriverCommand} from './webdriver_commands'; // Generate a random 8 character ID to avoid collisions. function getLogId() { diff --git a/spec/unit/webdriverLogger_spec.ts b/spec/unit/webdriver_logger_spec.ts similarity index 97% rename from spec/unit/webdriverLogger_spec.ts rename to spec/unit/webdriver_logger_spec.ts index 20e26d9..a3cbc63 100644 --- a/spec/unit/webdriverLogger_spec.ts +++ b/spec/unit/webdriver_logger_spec.ts @@ -3,7 +3,7 @@ import * as webdriver from 'selenium-webdriver'; import * as stream from 'stream'; import {BlockingProxy} from '../../lib/blockingproxy'; -import {WebDriverLogger} from '../../lib/webdriverLogger'; +import {WebDriverLogger} from '../../lib/webdriver_logger'; import {getMockSelenium, Session} from '../helpers/mock_selenium'; const capabilities = webdriver.Capabilities.chrome(); From 26f7591659adf595687389fc2cf7d1ef8c07bfb1 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 13:30:01 -0800 Subject: [PATCH 02/12] Pulling the proxy part out into a separate class. --- lib/blockingproxy.ts | 44 +---------------- lib/webdriver_proxy.ts | 72 ++++++++++++++++++++++++++++ spec/e2e/logging_spec.ts | 6 +-- spec/unit/proxy_spec.ts | 5 ++ spec/unit/webdriver_commands_spec.ts | 58 ++++++++++++++++++++++ spec/unit/webdriver_logger_spec.ts | 44 +++++++++-------- spec/unit/webdriver_proxy_spec.ts | 31 ++++++++++++ 7 files changed, 194 insertions(+), 66 deletions(-) create mode 100644 lib/webdriver_proxy.ts create mode 100644 spec/unit/webdriver_commands_spec.ts create mode 100644 spec/unit/webdriver_proxy_spec.ts diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 228b9d4..5cc58fb 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -246,49 +246,7 @@ export class BlockingProxy { return; } - // If the command is not a proxy command, it's a regular webdriver command. - if (self.shouldStabilize(originalRequest.url)) { - stabilized = self.sendRequestToStabilize(originalRequest); - - // TODO: Log waiting for Angular. - } - - 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(); - }); + this.proxy.requestLister(originalRequest, response); } listen(port: number) { diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts new file mode 100644 index 0000000..d8a69b7 --- /dev/null +++ b/lib/webdriver_proxy.ts @@ -0,0 +1,72 @@ +import {parseWebDriverCommand} from './webdriverCommands'; + +/** + * 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 { + + constructor() { + + } + + addMiddleware(middleware: WebDriverMiddleware) { + + } + + requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { + + + let stabilized = Promise.resolve(null); + + // If the command is not a proxy command, it's a regular webdriver command. + if (self.shouldStabilize(originalRequest.url)) { + stabilized = self.sendRequestToStabilize(originalRequest); + + // TODO: Log waiting for Angular. + } + + 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(); + }); + } +} + +export class WebDriverMiddleware { + onRequest() { + } +} \ No newline at end of file diff --git a/spec/e2e/logging_spec.ts b/spec/e2e/logging_spec.ts index c0aa59b..32d6911 100644 --- a/spec/e2e/logging_spec.ts +++ b/spec/e2e/logging_spec.ts @@ -12,9 +12,9 @@ import {BP_URL, getTestEnv} from './environment'; Example log of a test session: [12:51:30] Getting new "chrome" session -[12:51:33] [abcdef] Navigating to 'http://localhost/stuff' -[12:51:35] [abcdef] Wait for Angular -[12:51:36] [abcdef] Click on css '.test_element' +[12:51:33] [abcdef] [0.5s] Navigating to 'http://localhost/stuff' +[12:51:35] [abcdef] [0.3s] Wait for Angular +[12:51:36] [abcdef] [0.01s] Click on css '.test_element' [12:51:36] [abcdef] Move mouse by (0,50) [12:51:37] [abcdef] Click on binding 'thinger' */ diff --git a/spec/unit/proxy_spec.ts b/spec/unit/proxy_spec.ts index e8ac5c5..3fe6ab6 100644 --- a/spec/unit/proxy_spec.ts +++ b/spec/unit/proxy_spec.ts @@ -5,4 +5,9 @@ describe('BlockingProxy', () => { let proxy = new BlockingProxy(8111); expect(proxy.waitEnabled).toBe(true); }); + + it('should provide hooks when relaying commands', () => { + + + }) }); diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts new file mode 100644 index 0000000..57917b8 --- /dev/null +++ b/spec/unit/webdriver_commands_spec.ts @@ -0,0 +1,58 @@ +import {Server} from 'selenium-mock'; +import * as webdriver from 'selenium-webdriver'; + +import {BlockingProxy} from '../../lib/blockingproxy'; +import {getMockSelenium, Session} from '../helpers/mock_selenium'; + +const capabilities = webdriver.Capabilities.chrome(); + +class TestBarrier implements WDBarrier { + +} + +describe('WebDriver logger', () => { + let mockServer: Server; + let driver: webdriver.WebDriver; + let proxy: BlockingProxy; + let bpPort: number; + + beforeAll(() => { + mockServer = getMockSelenium(); + mockServer.start(); + let mockPort = mockServer.handle.address().port; + + proxy = new BlockingProxy(`http://localhost:${mockPort}/wd/hub`); + bpPort = proxy.listen(0); + }); + + beforeEach(async() => { + driver = new webdriver.Builder() + .usingServer(`http://localhost:${bpPort}`) + .withCapabilities(capabilities) + .build(); + + // Ensure WebDriver client has created a session by waiting on a command. + await driver.get('http://example.com'); + }); + + afterEach(() => { + }); + + it('parses 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'); + }); + + afterAll(() => { + mockServer.stop(); + }); +}); diff --git a/spec/unit/webdriver_logger_spec.ts b/spec/unit/webdriver_logger_spec.ts index a3cbc63..8ed52bd 100644 --- a/spec/unit/webdriver_logger_spec.ts +++ b/spec/unit/webdriver_logger_spec.ts @@ -76,30 +76,34 @@ describe('WebDriver logger', () => { expect(logger.logName).not.toEqual(otherLogger.logName); }); - it('logs session commands', async() => { - let session = await driver.getSession(); - let shortSession = session.getId().slice(0, 6); - await driver.quit(); - - let log = logger.getLog(); - expect(log[0]).toContain('Getting new "chrome" session'); - expect(log[2]).toContain(`Deleting session ${shortSession}`); - }); - it('logs 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'); - }); + describe('logs', () => { + it('logs session commands', async() => { + let session = await driver.getSession(); + let shortSession = session.getId().slice(0, 6); + await driver.quit(); + + let log = logger.getLog(); + expect(log[0]).toContain('Getting new "chrome" session'); + expect(log[2]).toContain(`Deleting session ${shortSession}`); + }); - it('logs the session ID', async() => { - let session = await driver.getSession(); - let shortSession = session.getId().slice(0, 6); + it('logs url commands', async() => { + await driver.getCurrentUrl(); - let log = logger.getLog(); - expect(log[1]).toContain(shortSession); + let log = logger.getLog(); + expect(log[1]).toContain('Navigating to http://example.com'); + expect(log[2]).toContain('Getting current URL'); + }); + + it('logs the session ID', async() => { + let session = await driver.getSession(); + let shortSession = session.getId().slice(0, 6); + + let log = logger.getLog(); + expect(log[1]).toContain(shortSession); + }); }); afterAll(() => { diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts new file mode 100644 index 0000000..0996dbd --- /dev/null +++ b/spec/unit/webdriver_proxy_spec.ts @@ -0,0 +1,31 @@ +describe('WebDriver Proxy', () => { + it('forwards requests to WebDriver', () => { + + }); + + it('waits for filters', () => { + + }); + + it('filters can insert webdriver commands', () => { + + }); + + it('calls filters with webdriver responses', () => { + + }); + + it('propagates http errors', () => { + + }); + + it('propagates headers to webdriver', () => { + + }); + + describe('with WebDriver commands', () => { + it('parses clicks', () => { + + }); + }); +}); \ No newline at end of file From a22172eb2b5c911690d738493733d285cb4efa31 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 18:13:17 -0800 Subject: [PATCH 03/12] Trying out commands as event emitters. --- lib/blockingproxy.ts | 32 ++++++-- lib/webdriver_commands.ts | 34 +++++--- lib/webdriver_logger.ts | 1 + lib/webdriver_proxy.ts | 111 +++++++++++++++------------ spec/unit/webdriver_commands_spec.ts | 34 ++++---- spec/unit/webdriver_logger_spec.ts | 8 +- 6 files changed, 131 insertions(+), 89 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 5cc58fb..4cf2530 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -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'; @@ -12,7 +14,7 @@ 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. @@ -20,12 +22,15 @@ export class BlockingProxy { 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() { @@ -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 = ''; @@ -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 { + 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) { @@ -261,3 +276,4 @@ export class BlockingProxy { }); } } + diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index b96225c..f6e1b56 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -1,6 +1,8 @@ /** * Utilities for parsing WebDriver commands from HTTP Requests. */ +import * as http from 'http'; +import * as events from 'events'; type HttpMethod = 'GET'|'POST'|'DELETE'; export type paramKey = 'sessionId' | 'elementId' | 'name' | 'propertyName'; @@ -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'); + } } @@ -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'; diff --git a/lib/webdriver_logger.ts b/lib/webdriver_logger.ts index 6795152..926ad52 100644 --- a/lib/webdriver_logger.ts +++ b/lib/webdriver_logger.ts @@ -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); diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index d8a69b7..b26f559 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -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? + 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(); + }); } } -export class WebDriverMiddleware { - onRequest() { - } +export interface WebDriverBarrier { + onCommand(command: WebDriverCommand): Promise; } \ No newline at end of file diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index 57917b8..daa0ee1 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -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 { + return undefined; + } } -describe('WebDriver logger', () => { +describe('WebDriver command parser', () => { let mockServer: Server; 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) @@ -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(() => { diff --git a/spec/unit/webdriver_logger_spec.ts b/spec/unit/webdriver_logger_spec.ts index 8ed52bd..b7619c0 100644 --- a/spec/unit/webdriver_logger_spec.ts +++ b/spec/unit/webdriver_logger_spec.ts @@ -38,7 +38,7 @@ class InMemoryLogger extends WebDriverLogger { } } -describe('WebDriver logger', () => { +fdescribe('WebDriver logger', () => { let mockServer: Server; let driver: webdriver.WebDriver; let logger = new InMemoryLogger(); @@ -79,7 +79,7 @@ describe('WebDriver logger', () => { describe('logs', () => { - it('logs session commands', async() => { + it('session commands', async() => { let session = await driver.getSession(); let shortSession = session.getId().slice(0, 6); await driver.quit(); @@ -89,7 +89,7 @@ describe('WebDriver logger', () => { expect(log[2]).toContain(`Deleting session ${shortSession}`); }); - it('logs url commands', async() => { + it('url commands', async() => { await driver.getCurrentUrl(); let log = logger.getLog(); @@ -97,7 +97,7 @@ describe('WebDriver logger', () => { expect(log[2]).toContain('Getting current URL'); }); - it('logs the session ID', async() => { + it('the session ID', async() => { let session = await driver.getSession(); let shortSession = session.getId().slice(0, 6); From 49bc92c6fb9bcdf5f1732ff9217767104c61d5f2 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 18:25:26 -0800 Subject: [PATCH 04/12] Fix tests and formatting. --- lib/blockingproxy.ts | 29 ++++---- lib/webdriver_commands.ts | 2 +- lib/webdriver_logger.ts | 3 +- lib/webdriver_proxy.ts | 101 ++++++++++++++------------- spec/unit/proxy_spec.ts | 5 +- spec/unit/webdriver_commands_spec.ts | 21 +++--- spec/unit/webdriver_logger_spec.ts | 44 ++++++------ spec/unit/webdriver_proxy_spec.ts | 35 ++++++---- 8 files changed, 118 insertions(+), 122 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 4cf2530..e4172e9 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -1,10 +1,9 @@ import * as http from 'http'; import * as url from 'url'; -import {parseWebDriverCommand} from './webdriver_commands'; +import {WebDriverCommand} from './webdriver_commands'; import {WebDriverLogger} from './webdriver_logger'; -import {WebDriverBarrier, WebDriverProxy} from "./webdriver_proxy"; -import {WebDriverCommand} from "./webdriver_commands"; +import {WebDriverBarrier, WebDriverProxy} from './webdriver_proxy'; let angularWaits = require('./angular/wait.js'); export const BP_PREFIX = 'bpproxy'; @@ -199,10 +198,9 @@ export class BlockingProxy implements WebDriverBarrier { } } - sendRequestToStabilize(url: string) { - let self = this; - let deferred = new Promise((resolve, reject) => { - let stabilityRequest = self.createSeleniumRequest( + sendRequestToStabilize(url: string): Promise { + return new Promise((resolve, reject) => { + let stabilityRequest = this.createSeleniumRequest( '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? @@ -232,8 +230,6 @@ export class BlockingProxy implements WebDriverBarrier { stabilityRequest.write(this.waitForAngularData()); stabilityRequest.end(); }); - - return deferred; } requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { @@ -252,16 +248,16 @@ export class BlockingProxy implements WebDriverBarrier { } onCommand(command: WebDriverCommand): Promise { - let stabilized = Promise.resolve(null); + if (this.logger) { + command.on('data', () => { + this.logger.logWebDriverCommand(command); + }); + } if (this.shouldStabilize(command.url)) { - stabilized = this.sendRequestToStabilize(command.url); + return this.sendRequestToStabilize(command.url); } - command.on('data', () => { - console.log('Got data', command); - this.logger.logWebDriverCommand(command); - }); - return stabilized; + return Promise.resolve(null); } listen(port: number) { @@ -276,4 +272,3 @@ export class BlockingProxy implements WebDriverBarrier { }); } } - diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index f6e1b56..bf41fb5 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -1,8 +1,8 @@ /** * Utilities for parsing WebDriver commands from HTTP Requests. */ -import * as http from 'http'; import * as events from 'events'; +import * as http from 'http'; type HttpMethod = 'GET'|'POST'|'DELETE'; export type paramKey = 'sessionId' | 'elementId' | 'name' | 'propertyName'; diff --git a/lib/webdriver_logger.ts b/lib/webdriver_logger.ts index 926ad52..7321cc9 100644 --- a/lib/webdriver_logger.ts +++ b/lib/webdriver_logger.ts @@ -56,7 +56,6 @@ 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); @@ -66,7 +65,7 @@ export class WebDriverLogger { case CommandName.GetCurrentURL: return `Getting current URL`; default: - return `Unknown command ${command.data['url']}`; + return `Unknown command ${command.url}`; } } diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index b26f559..f8b7c90 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -1,7 +1,8 @@ -import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands'; import * as http from 'http'; import * as url from 'url'; +import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands'; + /** * A proxy that understands WebDriver commands. Users can add middleware (similar to middleware in * express) that will be called before @@ -20,62 +21,64 @@ export class WebDriverProxy { this.barriers.push(barrier); } - async requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { - + requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { let command = parseWebDriverCommand(originalRequest.url, originalRequest.method); - let reqData = ''; - originalRequest.on('data', (d) => { - reqData += d; - }); - originalRequest.on('end', () => { - command.handleData(reqData); - }); + let replyWithError = (err) => { + response.writeHead(500); + response.end(err); + }; // TODO: What happens when barriers error? return a client error? - for (let barrier of this.barriers) { - await barrier.onCommand(command); - } + let barrierPromises = this.barriers.map((b) => b.onCommand(command)); - 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; + Promise.all(barrierPromises).then(() => { + 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.headers; - originalRequest.on('error', (err) => { - response.writeHead(500); - response.end(err); - }); + let forwardedRequest = http.request(options); + forwardedRequest + .on('response', + (seleniumResponse) => { + response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); + + let respData = ''; + seleniumResponse + .on('data', + (d) => { + respData += d; + response.write(d); + }) + .on('end', + () => { + command.handleResponse(seleniumResponse.statusCode, respData); + response.end(); + }) + .on('error', replyWithError); - 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); - }); - }); + }) + .on('error', replyWithError); - originalRequest.on('data', (d) => { - forwardedRequest.write(d); - }); - originalRequest.on('end', () => { - forwardedRequest.end(); - }); + let reqData = ''; + originalRequest + .on('data', + (d) => { + reqData += d; + forwardedRequest.write(d); + }) + .on('end', + () => { + command.handleData(reqData); + forwardedRequest.end(); + }) + .on('error', replyWithError); + }, replyWithError); } } -export interface WebDriverBarrier { - onCommand(command: WebDriverCommand): Promise; -} \ No newline at end of file +export interface WebDriverBarrier { onCommand(command: WebDriverCommand): Promise; } \ No newline at end of file diff --git a/spec/unit/proxy_spec.ts b/spec/unit/proxy_spec.ts index 3fe6ab6..a848dd6 100644 --- a/spec/unit/proxy_spec.ts +++ b/spec/unit/proxy_spec.ts @@ -6,8 +6,9 @@ describe('BlockingProxy', () => { expect(proxy.waitEnabled).toBe(true); }); - it('should provide hooks when relaying commands', () => { + it('should provide hooks when relaying commands', + () => { - }) + }); }); diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index daa0ee1..3baebf1 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -1,10 +1,10 @@ +import * as http from 'http'; import {Server} from 'selenium-mock'; import * as webdriver from 'selenium-webdriver'; +import {WebDriverCommand} from '../../lib/webdriver_commands'; +import {WebDriverBarrier, WebDriverProxy} from '../../lib/webdriver_proxy'; 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(); @@ -13,7 +13,6 @@ class TestBarrier implements WebDriverBarrier { commands: WebDriverCommand[] = []; onCommand(command: WebDriverCommand): Promise { - return undefined; } } @@ -34,25 +33,21 @@ describe('WebDriver command parser', () => { server = http.createServer(proxy.requestListener.bind(proxy)); driver = new webdriver.Builder() - .usingServer(`http://localhost:${bpPort}`) - .withCapabilities(capabilities) - .build(); + .usingServer(`http://localhost:${bpPort}`) + .withCapabilities(capabilities) + .build(); // Ensure WebDriver client has created a session by waiting on a command. await driver.get('http://example.com'); }); - afterEach(() => { - }); + afterEach(() => {}); xit('handles session commands', async() => { let session = await driver.getSession(); - - }); - xit('handles url commands', async() => { - }); + xit('handles url commands', async() => {}); afterAll(() => { mockServer.stop(); diff --git a/spec/unit/webdriver_logger_spec.ts b/spec/unit/webdriver_logger_spec.ts index b7619c0..a3cbc63 100644 --- a/spec/unit/webdriver_logger_spec.ts +++ b/spec/unit/webdriver_logger_spec.ts @@ -38,7 +38,7 @@ class InMemoryLogger extends WebDriverLogger { } } -fdescribe('WebDriver logger', () => { +describe('WebDriver logger', () => { let mockServer: Server; let driver: webdriver.WebDriver; let logger = new InMemoryLogger(); @@ -76,34 +76,30 @@ fdescribe('WebDriver logger', () => { expect(logger.logName).not.toEqual(otherLogger.logName); }); + it('logs session commands', async() => { + let session = await driver.getSession(); + let shortSession = session.getId().slice(0, 6); + await driver.quit(); + let log = logger.getLog(); + expect(log[0]).toContain('Getting new "chrome" session'); + expect(log[2]).toContain(`Deleting session ${shortSession}`); + }); - describe('logs', () => { - it('session commands', async() => { - let session = await driver.getSession(); - let shortSession = session.getId().slice(0, 6); - await driver.quit(); - - let log = logger.getLog(); - expect(log[0]).toContain('Getting new "chrome" session'); - expect(log[2]).toContain(`Deleting session ${shortSession}`); - }); - - it('url commands', async() => { - await driver.getCurrentUrl(); + it('logs 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'); - }); + let log = logger.getLog(); + expect(log[1]).toContain('Navigating to http://example.com'); + expect(log[2]).toContain('Getting current URL'); + }); - it('the session ID', async() => { - let session = await driver.getSession(); - let shortSession = session.getId().slice(0, 6); + it('logs the session ID', async() => { + let session = await driver.getSession(); + let shortSession = session.getId().slice(0, 6); - let log = logger.getLog(); - expect(log[1]).toContain(shortSession); - }); + let log = logger.getLog(); + expect(log[1]).toContain(shortSession); }); afterAll(() => { diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index 0996dbd..75001cd 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,31 +1,38 @@ describe('WebDriver Proxy', () => { - it('forwards requests to WebDriver', () => { + it('forwards requests to WebDriver', + () => { - }); + }); - it('waits for filters', () => { + it('waits for filters', + () => { - }); + }); - it('filters can insert webdriver commands', () => { + it('filters can insert webdriver commands', + () => { - }); + }); - it('calls filters with webdriver responses', () => { + it('calls filters with webdriver responses', + () => { - }); + }); - it('propagates http errors', () => { + it('propagates http errors', + () => { - }); + }); - it('propagates headers to webdriver', () => { + it('propagates headers to webdriver', + () => { - }); + }); describe('with WebDriver commands', () => { - it('parses clicks', () => { + it('parses clicks', + () => { - }); + }); }); }); \ No newline at end of file From b1db171b4250299e6fb87748c197ccdaf9970142 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 20:22:24 -0800 Subject: [PATCH 05/12] Try out manual formatting. --- lib/webdriver_proxy.ts | 55 ++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index f8b7c90..2f29cf7 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -42,41 +42,32 @@ export class WebDriverProxy { options.headers = originalRequest.headers; let forwardedRequest = http.request(options); - forwardedRequest - .on('response', - (seleniumResponse) => { - response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); - let respData = ''; - seleniumResponse - .on('data', - (d) => { - respData += d; - response.write(d); - }) - .on('end', - () => { - command.handleResponse(seleniumResponse.statusCode, respData); - response.end(); - }) - .on('error', replyWithError); + // clang-format off + let reqData = ''; + originalRequest.on('data', (d) => { + reqData += d; + forwardedRequest.write(d); + }).on('end', () => { + command.handleData(reqData); + forwardedRequest.end(); + }).on('error', replyWithError); - }) - .on('error', replyWithError); + forwardedRequest.on('response', (seleniumResponse) => { + response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); + + let respData = ''; + seleniumResponse.on('data', (d) => { + respData += d; + response.write(d); + }).on('end', () => { + command.handleResponse(seleniumResponse.statusCode, respData); + response.end(); + }).on('error', replyWithError); + + }).on('error', replyWithError); + // clang-format on - let reqData = ''; - originalRequest - .on('data', - (d) => { - reqData += d; - forwardedRequest.write(d); - }) - .on('end', - () => { - command.handleData(reqData); - forwardedRequest.end(); - }) - .on('error', replyWithError); }, replyWithError); } } From f20f04be3f7a9069a0763f549e39f66b749c22d5 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sun, 22 Jan 2017 14:15:00 -0800 Subject: [PATCH 06/12] Add unit tests for command parsing using barriers. --- lib/blockingproxy.ts | 2 +- lib/webdriver_commands.ts | 7 ++++ spec/unit/webdriver_commands_spec.ts | 50 ++++++++++++++++++++++------ spec/unit/webdriver_proxy_spec.ts | 42 ++++++++++++++++++----- 4 files changed, 80 insertions(+), 21 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index e4172e9..5a1b5c4 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -204,7 +204,7 @@ export class BlockingProxy implements WebDriverBarrier { '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 = ''; + let stabilityData = '' stabilityResponse.on('data', (data) => { stabilityData += data; }); diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index bf41fb5..21c0da2 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -94,6 +94,13 @@ export class WebDriverCommand extends events.EventEmitter { responseStatus: number; responseData: number; + // All WebDriver commands have a session Id, except for two. + // NewSession will have a session Id in the data + // Status just doesn't + get sessionId(): string { + return this.getParam('sessionId'); + } + constructor(public commandName: CommandName, public url: string, params?) { super(); this.params = params; diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index 3baebf1..009fc42 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -2,18 +2,22 @@ import * as http from 'http'; import {Server} from 'selenium-mock'; import * as webdriver from 'selenium-webdriver'; -import {WebDriverCommand} from '../../lib/webdriver_commands'; +import {WebDriverCommand, CommandName} from '../../lib/webdriver_commands'; import {WebDriverBarrier, WebDriverProxy} from '../../lib/webdriver_proxy'; import {getMockSelenium, Session} from '../helpers/mock_selenium'; - const capabilities = webdriver.Capabilities.chrome(); class TestBarrier implements WebDriverBarrier { commands: WebDriverCommand[] = []; onCommand(command: WebDriverCommand): Promise { - return undefined; + this.commands.push(command); + return null; + } + + getCommandNames() { + return this.commands.map((c) => c.commandName); } } @@ -21,19 +25,23 @@ describe('WebDriver command parser', () => { let mockServer: Server; let driver: webdriver.WebDriver; let proxy: WebDriverProxy; - let bpPort: number; let server: http.Server; + let testBarrier: TestBarrier; - beforeAll(async() => { + beforeEach(async() => { mockServer = getMockSelenium(); mockServer.start(); let mockPort = mockServer.handle.address().port; proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`); + testBarrier = new TestBarrier; + proxy.addBarrier(testBarrier); server = http.createServer(proxy.requestListener.bind(proxy)); + server.listen(0); + let port = server.address().port; driver = new webdriver.Builder() - .usingServer(`http://localhost:${bpPort}`) + .usingServer(`http://localhost:${port}`) .withCapabilities(capabilities) .build(); @@ -41,15 +49,35 @@ describe('WebDriver command parser', () => { await driver.get('http://example.com'); }); - afterEach(() => {}); - - xit('handles session commands', async() => { + it('parses session commands', async() => { let session = await driver.getSession(); + let sessionId = session.getId(); + await driver.quit(); + + let recentCommands = testBarrier.getCommandNames(); + expect(recentCommands.length).toBe(3); + expect(recentCommands).toEqual([ + CommandName.NewSession, + CommandName.Go, + CommandName.DeleteSession + ]); + expect(testBarrier.commands[1].sessionId).toEqual(sessionId); }); - xit('handles url commands', async() => {}); + it('parses url commands', async() => { + await driver.getCurrentUrl(); + + let recentCommands = testBarrier.getCommandNames(); + expect(recentCommands.length).toBe(3); + expect(recentCommands).toEqual([ + CommandName.NewSession, + CommandName.Go, + CommandName.GetCurrentURL + ]); + }); - afterAll(() => { + afterEach(() => { + server.close(); mockServer.stop(); }); }); diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index 75001cd..839a023 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,7 +1,38 @@ +import * as http from 'http'; +import {Server} from 'selenium-mock'; +import * as webdriver from 'selenium-webdriver'; + +import {WebDriverCommand} from '../../lib/webdriver_commands'; +import {WebDriverBarrier, WebDriverProxy} from '../../lib/webdriver_proxy'; +import {getMockSelenium, Session} from '../helpers/mock_selenium'; + +class TestBarrier implements WebDriverBarrier { + commands: WebDriverCommand[] = []; + + onCommand(command: WebDriverCommand): Promise { + this.commands.push(command); + return undefined; + } +} + describe('WebDriver Proxy', () => { + let mockServer: Server; + let driver: webdriver.WebDriver; + let proxy: WebDriverProxy; + let server: http.Server; + + beforeEach(() => { + mockServer = getMockSelenium(); + mockServer.start(); + let mockPort = mockServer.handle.address().port; + + proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`); + proxy.addBarrier(new TestBarrier()); + server = http.createServer(proxy.requestListener.bind(proxy)); + }); + it('forwards requests to WebDriver', () => { - }); it('waits for filters', @@ -24,15 +55,8 @@ describe('WebDriver Proxy', () => { }); - it('propagates headers to webdriver', + it('propagates headers to selenium', () => { }); - - describe('with WebDriver commands', () => { - it('parses clicks', - () => { - - }); - }); }); \ No newline at end of file From ba0784096029915f4cbf6b80bab9c3630a0e8468 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sun, 22 Jan 2017 17:17:01 -0800 Subject: [PATCH 07/12] Testing with nock and fake streams. --- lib/webdriver_proxy.ts | 3 + package.json | 2 + spec/unit/util.ts | 20 +++++ spec/unit/webdriver_commands_spec.ts | 22 +----- spec/unit/webdriver_proxy_spec.ts | 105 +++++++++++++++++++-------- 5 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 spec/unit/util.ts diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index 2f29cf7..9d068e9 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -42,6 +42,7 @@ export class WebDriverProxy { options.headers = originalRequest.headers; let forwardedRequest = http.request(options); + console.log('Forwarding req'); // clang-format off let reqData = ''; @@ -54,6 +55,7 @@ export class WebDriverProxy { }).on('error', replyWithError); forwardedRequest.on('response', (seleniumResponse) => { + console.log('Upstream response'); response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); let respData = ''; @@ -62,6 +64,7 @@ export class WebDriverProxy { response.write(d); }).on('end', () => { command.handleResponse(seleniumResponse.statusCode, respData); + console.log('Response done'); response.end(); }).on('error', replyWithError); diff --git a/package.json b/package.json index f130fd1..9e4f3c9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@types/jasmine": "^2.2.33", "@types/minimist": "^1.1.29", + "@types/nock": "^8.2.0", "@types/node": "^6.0.45", "@types/rimraf": "0.0.28", "@types/selenium-webdriver": "^2.53.39", @@ -31,6 +32,7 @@ "jasmine-co": "^1.2.2", "jasmine-ts": "0.0.3", "jshint": "2.9.1", + "nock": "^9.0.2", "rimraf": "^2.5.4", "run-sequence": "^1.2.2", "selenium-mock": "^0.1.5", diff --git a/spec/unit/util.ts b/spec/unit/util.ts new file mode 100644 index 0000000..1ccf028 --- /dev/null +++ b/spec/unit/util.ts @@ -0,0 +1,20 @@ +import {WebDriverBarrier} from "../../lib/webdriver_proxy"; +import {WebDriverCommand, CommandName} from "../../lib/webdriver_commands"; + +/** + * Fakes and helpers for testing. + */ +export class TestBarrier implements WebDriverBarrier { + commands: WebDriverCommand[] = []; + + onCommand(command: WebDriverCommand): Promise { + this.commands.push(command); + return null; + } + + getCommandNames(): CommandName[] { + return this.commands.map((c) => c.commandName); + } +} + + diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index 009fc42..f2f3998 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -2,24 +2,10 @@ import * as http from 'http'; import {Server} from 'selenium-mock'; import * as webdriver from 'selenium-webdriver'; -import {WebDriverCommand, CommandName} from '../../lib/webdriver_commands'; -import {WebDriverBarrier, WebDriverProxy} from '../../lib/webdriver_proxy'; +import {CommandName} from '../../lib/webdriver_commands'; +import {WebDriverProxy} from '../../lib/webdriver_proxy'; import {getMockSelenium, Session} from '../helpers/mock_selenium'; - -const capabilities = webdriver.Capabilities.chrome(); - -class TestBarrier implements WebDriverBarrier { - commands: WebDriverCommand[] = []; - - onCommand(command: WebDriverCommand): Promise { - this.commands.push(command); - return null; - } - - getCommandNames() { - return this.commands.map((c) => c.commandName); - } -} +import {TestBarrier} from "./util"; describe('WebDriver command parser', () => { let mockServer: Server; @@ -42,7 +28,7 @@ describe('WebDriver command parser', () => { driver = new webdriver.Builder() .usingServer(`http://localhost:${port}`) - .withCapabilities(capabilities) + .withCapabilities(webdriver.Capabilities.chrome()) .build(); // Ensure WebDriver client has created a session by waiting on a command. diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index 839a023..4f7c307 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,62 +1,105 @@ import * as http from 'http'; -import {Server} from 'selenium-mock'; -import * as webdriver from 'selenium-webdriver'; +import * as stream from 'stream'; +import * as nock from 'nock'; -import {WebDriverCommand} from '../../lib/webdriver_commands'; -import {WebDriverBarrier, WebDriverProxy} from '../../lib/webdriver_proxy'; -import {getMockSelenium, Session} from '../helpers/mock_selenium'; +import {WebDriverProxy} from '../../lib/webdriver_proxy'; -class TestBarrier implements WebDriverBarrier { - commands: WebDriverCommand[] = []; +class InMemoryWriter extends stream.Writable { + content: string[]; + doneCb: Function; + done; - onCommand(command: WebDriverCommand): Promise { - this.commands.push(command); - return undefined; + constructor() { + super({decodeStrings: true}); + this.content = []; + } + + _write(chunk: Buffer, encoding?, callback?) { + let data = chunk.toString(); + this.content.push(data); + callback(); + } + + onEnd(cb: Function) { + this.doneCb = cb; + } + + end() { + super.end(); + this.doneCb(this.content); + } +} + +class InMemoryReader extends stream.Readable { + content: string[]; + idx: number; + + constructor() { + super(); + this.content = [] + this.idx = 0; + } + + _read() { + if (this.idx < this.content.length) { + this.push(this.content[this.idx++]); + } else { + this.push(null); + } } } -describe('WebDriver Proxy', () => { - let mockServer: Server; - let driver: webdriver.WebDriver; +fdescribe('WebDriver Proxy', () => { let proxy: WebDriverProxy; - let server: http.Server; beforeEach(() => { - mockServer = getMockSelenium(); - mockServer.start(); - let mockPort = mockServer.handle.address().port; - - proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`); - proxy.addBarrier(new TestBarrier()); - server = http.createServer(proxy.requestListener.bind(proxy)); + proxy = new WebDriverProxy(`http://localhost:4444/wd/hub`); }); - it('forwards requests to WebDriver', - () => { - }); + fit('proxies to WebDriver', (done) => { + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + req.url = '/session/sessionId/get'; + req.method = 'GET'; - it('waits for filters', - () => { + resp.writeHead = jasmine.createSpy('spy'); - }); + let scope = nock(proxy.seleniumAddress) + .get('/session/sessionId/get') + .reply(500, 'test'); + + proxy.requestListener(req, resp); - it('filters can insert webdriver commands', + resp.onEnd((content) => { + console.log(content); + console.log(resp.writeHead.calls.first()); + scope.done(); + done(); + }); + }); + + xit('waits for filters', () => { + + }); + + xit('filters can insert webdriver commands', () => { }); - it('calls filters with webdriver responses', + xit('calls filters with webdriver responses', () => { }); - it('propagates http errors', + xit('propagates http errors', () => { }); - it('propagates headers to selenium', + xit('propagates headers to selenium', () => { }); + }); \ No newline at end of file From 16a7cb3a32550671b1b8d2f63475468a7f857ca9 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sun, 22 Jan 2017 19:08:07 -0800 Subject: [PATCH 08/12] Add tests for proxy. --- lib/webdriver_commands.ts | 6 +- lib/webdriver_proxy.ts | 6 +- spec/unit/util.ts | 48 +++++++++- spec/unit/webdriver_proxy_spec.ts | 147 ++++++++++++++++-------------- 4 files changed, 131 insertions(+), 76 deletions(-) diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index 21c0da2..3360b83 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -119,7 +119,9 @@ export class WebDriverCommand extends events.EventEmitter { public handleResponse(statusCode: number, data?: any) { this.responseStatus = statusCode; - this.responseData = data; + if(data) { + this.responseData = JSON.parse(data); + } this.emit('response'); } } @@ -151,7 +153,7 @@ export function parseWebDriverCommand(url, method) { let sessionPrefix = '/session/:sessionId'; addWebDriverCommand(CommandName.NewSession, 'POST', '/session'); addWebDriverCommand(CommandName.DeleteSession, 'DELETE', '/session/:sessionId'); -addWebDriverCommand(CommandName.Status, 'GET', sessionPrefix + '/status'); +addWebDriverCommand(CommandName.Status, 'GET', '/status'); addWebDriverCommand(CommandName.GetTimeouts, 'GET', sessionPrefix + '/timeouts'); addWebDriverCommand(CommandName.SetTimeouts, 'POST', sessionPrefix + '/timeouts'); addWebDriverCommand(CommandName.Go, 'POST', sessionPrefix + '/url'); diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index 9d068e9..1760607 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -26,7 +26,8 @@ export class WebDriverProxy { let replyWithError = (err) => { response.writeHead(500); - response.end(err); + response.write(err.toString()); + response.end(); }; // TODO: What happens when barriers error? return a client error? @@ -42,7 +43,6 @@ export class WebDriverProxy { options.headers = originalRequest.headers; let forwardedRequest = http.request(options); - console.log('Forwarding req'); // clang-format off let reqData = ''; @@ -55,7 +55,6 @@ export class WebDriverProxy { }).on('error', replyWithError); forwardedRequest.on('response', (seleniumResponse) => { - console.log('Upstream response'); response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); let respData = ''; @@ -64,7 +63,6 @@ export class WebDriverProxy { response.write(d); }).on('end', () => { command.handleResponse(seleniumResponse.statusCode, respData); - console.log('Response done'); response.end(); }).on('error', replyWithError); diff --git a/spec/unit/util.ts b/spec/unit/util.ts index 1ccf028..759bf17 100644 --- a/spec/unit/util.ts +++ b/spec/unit/util.ts @@ -1,3 +1,5 @@ +import * as stream from 'stream'; + import {WebDriverBarrier} from "../../lib/webdriver_proxy"; import {WebDriverCommand, CommandName} from "../../lib/webdriver_commands"; @@ -9,7 +11,7 @@ export class TestBarrier implements WebDriverBarrier { onCommand(command: WebDriverCommand): Promise { this.commands.push(command); - return null; + return; } getCommandNames(): CommandName[] { @@ -17,4 +19,48 @@ export class TestBarrier implements WebDriverBarrier { } } +export class InMemoryWriter extends stream.Writable { + content: string; + doneCb: Function; + + constructor() { + super({decodeStrings: true}); + this.content = ''; + } + + _write(chunk: Buffer, encoding?, callback?) { + let data = chunk.toString(); + this.content += data; + callback(); + } + + onEnd(cb: Function) { + this.doneCb = cb; + } + end() { + super.end(); + if (this.doneCb) { + this.doneCb(this.content); + } + } +} + +export class InMemoryReader extends stream.Readable { + content: string[]; + idx: number; + + constructor() { + super(); + this.content = [] + this.idx = 0; + } + + _read() { + if (this.idx < this.content.length) { + this.push(this.content[this.idx++]); + } else { + this.push(null); + } + } +} diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index 4f7c307..73c8ea8 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,105 +1,114 @@ -import * as http from 'http'; -import * as stream from 'stream'; import * as nock from 'nock'; import {WebDriverProxy} from '../../lib/webdriver_proxy'; +import {InMemoryReader, InMemoryWriter, TestBarrier} from "./util"; +import {WebDriverCommand, CommandName} from "../../lib/webdriver_commands"; -class InMemoryWriter extends stream.Writable { - content: string[]; - doneCb: Function; - done; - - constructor() { - super({decodeStrings: true}); - this.content = []; - } - - _write(chunk: Buffer, encoding?, callback?) { - let data = chunk.toString(); - this.content.push(data); - callback(); - } - - onEnd(cb: Function) { - this.doneCb = cb; - } - - end() { - super.end(); - this.doneCb(this.content); - } -} - -class InMemoryReader extends stream.Readable { - content: string[]; - idx: number; - - constructor() { - super(); - this.content = [] - this.idx = 0; - } - - _read() { - if (this.idx < this.content.length) { - this.push(this.content[this.idx++]); - } else { - this.push(null); - } - } -} - -fdescribe('WebDriver Proxy', () => { +describe('WebDriver Proxy', () => { let proxy: WebDriverProxy; beforeEach(() => { proxy = new WebDriverProxy(`http://localhost:4444/wd/hub`); }); - fit('proxies to WebDriver', (done) => { + it('proxies to WebDriver', (done) => { let req = new InMemoryReader() as any; let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); req.url = '/session/sessionId/get'; req.method = 'GET'; - - resp.writeHead = jasmine.createSpy('spy'); + const responseData = {value: 'selenium response'}; let scope = nock(proxy.seleniumAddress) .get('/session/sessionId/get') - .reply(500, 'test'); + .reply(200, responseData); proxy.requestListener(req, resp); - resp.onEnd((content) => { - console.log(content); - console.log(resp.writeHead.calls.first()); + resp.onEnd((data) => { + // Verify that all nock endpoints were called. + expect(resp.writeHead.calls.first().args[0]).toBe(200); + expect(data).toEqual(JSON.stringify(responseData)); scope.done(); done(); }); }); - xit('waits for filters', () => { + it('waits for barriers', (done) => { + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); + req.url = '/session/sessionId/get'; + req.method = 'GET'; - }); + let barrier = new TestBarrier(); + let barrierDone = false; + barrier.onCommand = (): Promise => { + return new Promise((res) => { + setTimeout(() => { + barrierDone = true; + res(); + }, 250); + }); + }; + + proxy.addBarrier(barrier); + proxy.requestListener(req, resp); - xit('filters can insert webdriver commands', - () => { + resp.onEnd(() => { + expect(barrierDone).toBeTruthy(); + done(); + }); + }); - }); + it('barriers get selenium responses', (done) => { + const WD_URL = '/session/sessionId/url'; + const RESPONSE = {url: 'http://example.com'}; - xit('calls filters with webdriver responses', - () => { + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); + req.url = WD_URL; + req.method = 'GET'; - }); + let scope = nock(proxy.seleniumAddress) + .get(WD_URL) + .reply(200, RESPONSE); + + let barrier = new TestBarrier(); + barrier.onCommand = (command: WebDriverCommand): Promise => { + command.on('response', () => { + expect(command.responseData['url']).toEqual(RESPONSE.url); + scope.done(); + done(); + }); + return undefined; + }; + proxy.addBarrier(barrier); + proxy.requestListener(req, resp); + }); - xit('propagates http errors', - () => { + it('propagates http errors', (done) => { + const WD_URL = '/session/'; + const ERR = new Error('HTTP error'); - }); + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); + req.url = WD_URL; + req.method = 'POST'; - xit('propagates headers to selenium', - () => { + let scope = nock(proxy.seleniumAddress) + .post(WD_URL) + .replyWithError(ERR); - }); + proxy.requestListener(req, resp); + resp.onEnd((data) => { + expect(resp.writeHead.calls.first().args[0]).toBe(500); + expect(data).toEqual(ERR.toString()); + scope.done(); + done(); + }); + }); }); \ No newline at end of file From 99a9e0ad6b32fa5a66a1ed3d36fa38cb425b1e34 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sun, 22 Jan 2017 19:08:42 -0800 Subject: [PATCH 09/12] Clang format --- lib/blockingproxy.ts | 2 +- lib/webdriver_commands.ts | 2 +- spec/unit/util.ts | 6 +++--- spec/unit/webdriver_commands_spec.ts | 10 +++------- spec/unit/webdriver_proxy_spec.ts | 17 ++++++----------- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 5a1b5c4..e4172e9 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -204,7 +204,7 @@ export class BlockingProxy implements WebDriverBarrier { '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 = '' + let stabilityData = ''; stabilityResponse.on('data', (data) => { stabilityData += data; }); diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index 3360b83..8110204 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -119,7 +119,7 @@ export class WebDriverCommand extends events.EventEmitter { public handleResponse(statusCode: number, data?: any) { this.responseStatus = statusCode; - if(data) { + if (data) { this.responseData = JSON.parse(data); } this.emit('response'); diff --git a/spec/unit/util.ts b/spec/unit/util.ts index 759bf17..3bb96be 100644 --- a/spec/unit/util.ts +++ b/spec/unit/util.ts @@ -1,7 +1,7 @@ import * as stream from 'stream'; -import {WebDriverBarrier} from "../../lib/webdriver_proxy"; -import {WebDriverCommand, CommandName} from "../../lib/webdriver_commands"; +import {CommandName, WebDriverCommand} from '../../lib/webdriver_commands'; +import {WebDriverBarrier} from '../../lib/webdriver_proxy'; /** * Fakes and helpers for testing. @@ -52,7 +52,7 @@ export class InMemoryReader extends stream.Readable { constructor() { super(); - this.content = [] + this.content = []; this.idx = 0; } diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index f2f3998..f531e4a 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -5,7 +5,7 @@ import * as webdriver from 'selenium-webdriver'; import {CommandName} from '../../lib/webdriver_commands'; import {WebDriverProxy} from '../../lib/webdriver_proxy'; import {getMockSelenium, Session} from '../helpers/mock_selenium'; -import {TestBarrier} from "./util"; +import {TestBarrier} from './util'; describe('WebDriver command parser', () => { let mockServer: Server; @@ -43,9 +43,7 @@ describe('WebDriver command parser', () => { let recentCommands = testBarrier.getCommandNames(); expect(recentCommands.length).toBe(3); expect(recentCommands).toEqual([ - CommandName.NewSession, - CommandName.Go, - CommandName.DeleteSession + CommandName.NewSession, CommandName.Go, CommandName.DeleteSession ]); expect(testBarrier.commands[1].sessionId).toEqual(sessionId); }); @@ -56,9 +54,7 @@ describe('WebDriver command parser', () => { let recentCommands = testBarrier.getCommandNames(); expect(recentCommands.length).toBe(3); expect(recentCommands).toEqual([ - CommandName.NewSession, - CommandName.Go, - CommandName.GetCurrentURL + CommandName.NewSession, CommandName.Go, CommandName.GetCurrentURL ]); }); diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index 73c8ea8..fea4086 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,8 +1,9 @@ import * as nock from 'nock'; +import {CommandName, WebDriverCommand} from '../../lib/webdriver_commands'; import {WebDriverProxy} from '../../lib/webdriver_proxy'; -import {InMemoryReader, InMemoryWriter, TestBarrier} from "./util"; -import {WebDriverCommand, CommandName} from "../../lib/webdriver_commands"; + +import {InMemoryReader, InMemoryWriter, TestBarrier} from './util'; describe('WebDriver Proxy', () => { let proxy: WebDriverProxy; @@ -19,9 +20,7 @@ describe('WebDriver Proxy', () => { req.method = 'GET'; const responseData = {value: 'selenium response'}; - let scope = nock(proxy.seleniumAddress) - .get('/session/sessionId/get') - .reply(200, responseData); + let scope = nock(proxy.seleniumAddress).get('/session/sessionId/get').reply(200, responseData); proxy.requestListener(req, resp); @@ -71,9 +70,7 @@ describe('WebDriver Proxy', () => { req.url = WD_URL; req.method = 'GET'; - let scope = nock(proxy.seleniumAddress) - .get(WD_URL) - .reply(200, RESPONSE); + let scope = nock(proxy.seleniumAddress).get(WD_URL).reply(200, RESPONSE); let barrier = new TestBarrier(); barrier.onCommand = (command: WebDriverCommand): Promise => { @@ -98,9 +95,7 @@ describe('WebDriver Proxy', () => { req.url = WD_URL; req.method = 'POST'; - let scope = nock(proxy.seleniumAddress) - .post(WD_URL) - .replyWithError(ERR); + let scope = nock(proxy.seleniumAddress).post(WD_URL).replyWithError(ERR); proxy.requestListener(req, resp); From d0124f33ee4f761f197ac7797155df67a11b18cf Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sun, 22 Jan 2017 19:24:57 -0800 Subject: [PATCH 10/12] Improve barrier test. --- spec/unit/webdriver_proxy_spec.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index fea4086..bf36de8 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -28,17 +28,19 @@ describe('WebDriver Proxy', () => { // Verify that all nock endpoints were called. expect(resp.writeHead.calls.first().args[0]).toBe(200); expect(data).toEqual(JSON.stringify(responseData)); - scope.done(); + scope.isDone(); done(); }); }); it('waits for barriers', (done) => { + const WD_URL = '/session/sessionId/url'; + let req = new InMemoryReader() as any; let resp = new InMemoryWriter() as any; resp.writeHead = jasmine.createSpy('spy'); - req.url = '/session/sessionId/get'; - req.method = 'GET'; + req.url = WD_URL; + req.method = 'POST'; let barrier = new TestBarrier(); let barrierDone = false; @@ -51,11 +53,18 @@ describe('WebDriver Proxy', () => { }); }; + let scope = nock(proxy.seleniumAddress).post(WD_URL).reply(() => { + // Shouldn't see the command until the barrier is done. + expect(barrierDone).toBeTruthy(); + return [200]; + }); + proxy.addBarrier(barrier); proxy.requestListener(req, resp); resp.onEnd(() => { expect(barrierDone).toBeTruthy(); + scope.isDone(); done(); }); }); @@ -76,7 +85,7 @@ describe('WebDriver Proxy', () => { barrier.onCommand = (command: WebDriverCommand): Promise => { command.on('response', () => { expect(command.responseData['url']).toEqual(RESPONSE.url); - scope.done(); + scope.isDone(); done(); }); return undefined; @@ -102,7 +111,7 @@ describe('WebDriver Proxy', () => { resp.onEnd((data) => { expect(resp.writeHead.calls.first().args[0]).toBe(500); expect(data).toEqual(ERR.toString()); - scope.done(); + scope.isDone(); done(); }); }); From f3f81dff737c1159a46f8effa90877506236d90d Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Mon, 23 Jan 2017 13:30:41 -0800 Subject: [PATCH 11/12] Call barriers one at a time, implement other feedback. --- lib/blockingproxy.ts | 2 +- lib/webdriver_commands.ts | 1 - lib/webdriver_proxy.ts | 87 +++++++++++++----------- package.json | 2 +- spec/unit/proxy_spec.ts | 6 -- spec/unit/webdriver_commands_spec.ts | 2 +- spec/unit/webdriver_proxy_spec.ts | 99 ++++++++++++++++++++++++---- tsconfig.json | 1 + 8 files changed, 141 insertions(+), 59 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index e4172e9..762ffa7 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -244,7 +244,7 @@ export class BlockingProxy implements WebDriverBarrier { return; } - this.proxy.requestListener(originalRequest, response); + this.proxy.handleRequest(originalRequest, response); } onCommand(command: WebDriverCommand): Promise { diff --git a/lib/webdriver_commands.ts b/lib/webdriver_commands.ts index 8110204..be7b098 100644 --- a/lib/webdriver_commands.ts +++ b/lib/webdriver_commands.ts @@ -2,7 +2,6 @@ * Utilities for parsing WebDriver commands from HTTP Requests. */ import * as events from 'events'; -import * as http from 'http'; type HttpMethod = 'GET'|'POST'|'DELETE'; export type paramKey = 'sessionId' | 'elementId' | 'name' | 'propertyName'; diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index 1760607..b8b9e91 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -4,9 +4,9 @@ import * as url from 'url'; import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands'; /** - * 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. + * A proxy that understands WebDriver commands. Users can add barriers * (similar to middleware in + * express) that will be called before forwarding the request to WebDriver. The proxy will wait for + * each barrier to finish, calling them in the order in which they were added. */ export class WebDriverProxy { barriers: WebDriverBarrier[]; @@ -21,7 +21,7 @@ export class WebDriverProxy { this.barriers.push(barrier); } - requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { + async handleRequest(originalRequest: http.IncomingMessage, response: http.ServerResponse) { let command = parseWebDriverCommand(originalRequest.url, originalRequest.method); let replyWithError = (err) => { @@ -30,47 +30,58 @@ export class WebDriverProxy { response.end(); }; - // TODO: What happens when barriers error? return a client error? - let barrierPromises = this.barriers.map((b) => b.onCommand(command)); + // Process barriers in order, one at a time. + try { + for (let barrier of this.barriers) { + await barrier.onCommand(command); + } + } catch (err) { + replyWithError(err); + // Don't call through if a barrier fails. + return; + } - Promise.all(barrierPromises).then(() => { - 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.headers; + 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.headers; - let forwardedRequest = http.request(options); + let forwardedRequest = http.request(options); - // clang-format off - let reqData = ''; - originalRequest.on('data', (d) => { - reqData += d; - forwardedRequest.write(d); - }).on('end', () => { - command.handleData(reqData); - forwardedRequest.end(); - }).on('error', replyWithError); - - forwardedRequest.on('response', (seleniumResponse) => { - response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); + // clang-format off + let reqData = ''; + originalRequest.on('data', (d) => { + reqData += d; + forwardedRequest.write(d); + }).on('end', () => { + command.handleData(reqData); + forwardedRequest.end(); + }).on('error', replyWithError); - let respData = ''; - seleniumResponse.on('data', (d) => { - respData += d; - response.write(d); - }).on('end', () => { - command.handleResponse(seleniumResponse.statusCode, respData); - response.end(); - }).on('error', replyWithError); + forwardedRequest.on('response', (seleniumResponse) => { + response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); + let respData = ''; + seleniumResponse.on('data', (d) => { + respData += d; + response.write(d); + }).on('end', () => { + command.handleResponse(seleniumResponse.statusCode, respData); + response.end(); }).on('error', replyWithError); - // clang-format on - }, replyWithError); + }).on('error', replyWithError); + // clang-format on } } -export interface WebDriverBarrier { onCommand(command: WebDriverCommand): Promise; } \ No newline at end of file +/** + * When the proxy receives a WebDriver command, it will call onCommand() for each of it's barriers. + * Barriers may return a promise for the proxy to wait for before proceeding. If the promise is + * rejected, the proxy will reply with an error code and the result of the promise and the command + * will not be forwarded to Selenium. + */ +export interface WebDriverBarrier { onCommand(command: WebDriverCommand): Promise; } diff --git a/package.json b/package.json index 9e4f3c9..0d84af0 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "selenium-mock": "^0.1.5", "selenium-webdriver": "2.53.3", "ts-node": "^2.0.0", - "tslint": "^4.0.2", + "tslint": "^4.3.1", "tslint-eslint-rules": "^3.1.0", "typescript": "^2.0.3", "vrsource-tslint-rules": "^0.14.1", diff --git a/spec/unit/proxy_spec.ts b/spec/unit/proxy_spec.ts index a848dd6..e8ac5c5 100644 --- a/spec/unit/proxy_spec.ts +++ b/spec/unit/proxy_spec.ts @@ -5,10 +5,4 @@ describe('BlockingProxy', () => { let proxy = new BlockingProxy(8111); expect(proxy.waitEnabled).toBe(true); }); - - it('should provide hooks when relaying commands', - () => { - - - }); }); diff --git a/spec/unit/webdriver_commands_spec.ts b/spec/unit/webdriver_commands_spec.ts index f531e4a..a0e2ffc 100644 --- a/spec/unit/webdriver_commands_spec.ts +++ b/spec/unit/webdriver_commands_spec.ts @@ -22,7 +22,7 @@ describe('WebDriver command parser', () => { proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`); testBarrier = new TestBarrier; proxy.addBarrier(testBarrier); - server = http.createServer(proxy.requestListener.bind(proxy)); + server = http.createServer(proxy.handleRequest.bind(proxy)); server.listen(0); let port = server.address().port; diff --git a/spec/unit/webdriver_proxy_spec.ts b/spec/unit/webdriver_proxy_spec.ts index bf36de8..6b9d2a5 100644 --- a/spec/unit/webdriver_proxy_spec.ts +++ b/spec/unit/webdriver_proxy_spec.ts @@ -1,6 +1,6 @@ import * as nock from 'nock'; -import {CommandName, WebDriverCommand} from '../../lib/webdriver_commands'; +import {WebDriverCommand} from '../../lib/webdriver_commands'; import {WebDriverProxy} from '../../lib/webdriver_proxy'; import {InMemoryReader, InMemoryWriter, TestBarrier} from './util'; @@ -9,7 +9,7 @@ describe('WebDriver Proxy', () => { let proxy: WebDriverProxy; beforeEach(() => { - proxy = new WebDriverProxy(`http://localhost:4444/wd/hub`); + proxy = new WebDriverProxy('http://test_webdriver_url/wd/hub'); }); it('proxies to WebDriver', (done) => { @@ -22,13 +22,13 @@ describe('WebDriver Proxy', () => { let scope = nock(proxy.seleniumAddress).get('/session/sessionId/get').reply(200, responseData); - proxy.requestListener(req, resp); + proxy.handleRequest(req, resp); resp.onEnd((data) => { // Verify that all nock endpoints were called. expect(resp.writeHead.calls.first().args[0]).toBe(200); expect(data).toEqual(JSON.stringify(responseData)); - scope.isDone(); + scope.done(); done(); }); }); @@ -60,11 +60,88 @@ describe('WebDriver Proxy', () => { }); proxy.addBarrier(barrier); - proxy.requestListener(req, resp); + proxy.handleRequest(req, resp); resp.onEnd(() => { expect(barrierDone).toBeTruthy(); - scope.isDone(); + scope.done(); + done(); + }); + }); + + it('waits for multiple barriers in order', (done) => { + const WD_URL = '/session/sessionId/url'; + + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); + req.url = WD_URL; + req.method = 'POST'; + + let barrier1 = new TestBarrier(); + let barrier1Done = false; + barrier1.onCommand = (): Promise => { + return new Promise((res) => { + setTimeout(() => { + expect(barrier2Done).toBeFalsy(); + barrier1Done = true; + res(); + }, 150); + }); + }; + let barrier2 = new TestBarrier(); + let barrier2Done = false; + barrier2.onCommand = (): Promise => { + return new Promise((res) => { + setTimeout(() => { + expect(barrier1Done).toBeTruthy(); + barrier2Done = true; + res(); + }, 50); + }); + }; + + let scope = nock(proxy.seleniumAddress).post(WD_URL).reply(200); + + proxy.addBarrier(barrier1); + proxy.addBarrier(barrier2); + proxy.handleRequest(req, resp); + + resp.onEnd(() => { + expect(barrier2Done).toBeTruthy(); + scope.done(); + done(); + }); + }); + + it('returns an error if a barrier fails', (done) => { + const WD_URL = '/session/sessionId/url'; + + let req = new InMemoryReader() as any; + let resp = new InMemoryWriter() as any; + resp.writeHead = jasmine.createSpy('spy'); + req.url = WD_URL; + req.method = 'GET'; + + let barrier = new TestBarrier(); + barrier.onCommand = (): Promise => { + return new Promise((res, rej) => { + rej('Barrier failed'); + }); + }; + + let scope = nock(proxy.seleniumAddress).get(WD_URL).reply(200); + + proxy.addBarrier(barrier); + proxy.handleRequest(req, resp); + + resp.onEnd((respData) => { + expect(resp.writeHead.calls.first().args[0]).toBe(500); + expect(respData).toEqual('Barrier failed'); + + // Should not call the selenium server. + expect(scope.isDone()).toBeFalsy(); + nock.cleanAll(); done(); }); }); @@ -85,13 +162,13 @@ describe('WebDriver Proxy', () => { barrier.onCommand = (command: WebDriverCommand): Promise => { command.on('response', () => { expect(command.responseData['url']).toEqual(RESPONSE.url); - scope.isDone(); + scope.done(); done(); }); return undefined; }; proxy.addBarrier(barrier); - proxy.requestListener(req, resp); + proxy.handleRequest(req, resp); }); it('propagates http errors', (done) => { @@ -106,13 +183,13 @@ describe('WebDriver Proxy', () => { let scope = nock(proxy.seleniumAddress).post(WD_URL).replyWithError(ERR); - proxy.requestListener(req, resp); + proxy.handleRequest(req, resp); resp.onEnd((data) => { expect(resp.writeHead.calls.first().args[0]).toBe(500); expect(data).toEqual(ERR.toString()); - scope.isDone(); + scope.done(); done(); }); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 65d98e3..c23dc49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "es6", "module": "commonjs", "moduleResolution": "node", + "noUnusedLocals": true, "sourceMap": true, "declaration": true, "removeComments": false, From 81f6b068542f1b4b8d9897aa66b14f5eed60ece6 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Mon, 23 Jan 2017 14:27:10 -0800 Subject: [PATCH 12/12] Comment fixes. --- lib/blockingproxy.ts | 1 + lib/webdriver_proxy.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 762ffa7..c697a83 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -244,6 +244,7 @@ export class BlockingProxy implements WebDriverBarrier { return; } + // OK to ignore the promise returned by this. this.proxy.handleRequest(originalRequest, response); } diff --git a/lib/webdriver_proxy.ts b/lib/webdriver_proxy.ts index b8b9e91..9fd763e 100644 --- a/lib/webdriver_proxy.ts +++ b/lib/webdriver_proxy.ts @@ -4,7 +4,7 @@ import * as url from 'url'; import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands'; /** - * A proxy that understands WebDriver commands. Users can add barriers * (similar to middleware in + * A proxy that understands WebDriver commands. Users can add barriers (similar to middleware in * express) that will be called before forwarding the request to WebDriver. The proxy will wait for * each barrier to finish, calling them in the order in which they were added. */