From 9774d2cc6af5819363c0c3f4755b91123859be85 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 11:44:26 -0800 Subject: [PATCH 1/4] Update to Protractor 5 wait functions, make naming more consistent. --- lib/angular/wait.js | 145 +++++++++++++++++++++++++++++---------- lib/bin.ts | 2 +- lib/blockingproxy.ts | 29 +++++--- lib/client.ts | 9 +-- lib/config.ts | 2 - spec/unit/client_spec.ts | 24 ++++--- spec/unit/proxy_spec.ts | 2 +- 7 files changed, 147 insertions(+), 66 deletions(-) diff --git a/lib/angular/wait.js b/lib/angular/wait.js index 2c6d255..0b6052a 100644 --- a/lib/angular/wait.js +++ b/lib/angular/wait.js @@ -1,67 +1,136 @@ /** * Wait until Angular has finished rendering and has * no outstanding $http calls before continuing. The specific Angular app - * is determined by the rootSelector. + * is determined by the rootSelector. Copied from Protractor 5. * * Asynchronous. * * @param {string} rootSelector The selector housing an ng-app - * @param {boolean} ng12Hybrid Flag set if app is a hybrid of angular 1 and 2 * @param {function(string)} callback callback. If a failure occurs, it will * be passed as a parameter. */ -exports.NG_WAIT_FN = function(rootSelector, ng12Hybrid, callback) { - var el = document.querySelector(rootSelector); - +function waitForAngular(rootSelector, callback) { try { - if (!ng12Hybrid && window.getAngularTestability) { + if (window.angular && !(window.angular.version && + window.angular.version.major > 1)) { + /* ng1 */ + var hooks = getNg1Hooks(rootSelector); + if (hooks.$$testability) { + hooks.$$testability.whenStable(callback); + } else if (hooks.$injector) { + hooks.$injector.get('$browser'). + notifyWhenNoOutstandingRequests(callback); + } else if (!!rootSelector) { + throw new Error('Could not automatically find injector on page: "' + + window.location.toString() + '". Consider using config.rootEl'); + } else { + throw new Error('root element (' + rootSelector + ') has no injector.' + + ' this may mean it is not inside ng-app.'); + } + } else if (rootSelector && window.getAngularTestability) { + var el = document.querySelector(rootSelector); window.getAngularTestability(el).whenStable(callback); - return; - } - if (!window.angular) { + } else if (window.getAllAngularTestabilities) { + var testabilities = window.getAllAngularTestabilities(); + var count = testabilities.length; + var decrement = function() { + count--; + if (count === 0) { + callback(); + } + }; + testabilities.forEach(function(testability) { + testability.whenStable(decrement); + }); + } else if (!window.angular) { throw new Error('window.angular is undefined. This could be either ' + 'because this is a non-angular page or because your test involves ' + 'client-side navigation, which can interfere with Protractor\'s ' + 'bootstrapping. See http://git.io/v4gXM for details'); - } - if (angular.getTestability) { - angular.getTestability(el).whenStable(callback); + } else if (window.angular.version >= 2) { + throw new Error('You appear to be using angular, but window.' + + 'getAngularTestability was never set. This may be due to bad ' + + 'obfuscation.'); } else { - if (!angular.element(el).injector()) { - throw new Error('root element (' + rootSelector + ') has no injector.' + - ' this may mean it is not inside ng-app.'); - } - angular.element(el).injector().get('$browser'). - notifyWhenNoOutstandingRequests(callback); + throw new Error('Cannot get testability API for unknown angular ' + + 'version "' + window.angular.version + '"'); } } catch (err) { callback(err.message); } }; -/** - * Wait until all Angular2 applications on the page have become stable. +/* Tries to find $$testability and possibly $injector for an ng1 app * - * Asynchronous. + * By default, doesn't care about $injector if it finds $$testability. However, + * these priorities can be reversed. * - * @param {function(string)} callback callback. If a failure occurs, it will - * be passed as a parameter. + * @param {string=} selector The selector for the element with the injector. If + * falsy, tries a variety of methods to find an injector + * @param {boolean=} injectorPlease Prioritize finding an injector + * @return {$$testability?: Testability, $injector?: Injector} Returns whatever + * ng1 app hooks it finds */ -exports.NG2_WAIT_FN = function(callback) { - try { - var testabilities = window.getAllAngularTestabilities(); - var count = testabilities.length; - var decrement = function() { - count--; - if (count === 0) { - callback(); +function getNg1Hooks(selector, injectorPlease) { + function tryEl(el) { + try { + if (!injectorPlease && angular.getTestability) { + var $$testability = angular.getTestability(el); + if ($$testability) { + return {$$testability: $$testability}; + } + } else { + var $injector = angular.element(el).injector(); + if ($injector) { + return {$injector: $injector}; + } } - }; - testabilities.forEach(function(testability) { - testability.whenStable(decrement); - }); - } catch (err) { - callback(err.message); + } catch(err) {} } -}; + function trySelector(selector) { + var els = document.querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + var elHooks = tryEl(els[i]); + if (elHooks) { + return elHooks; + } + } + } + + if (selector) { + return trySelector(selector); + } else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) { + var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__; + var $$testability = null; + try { + $$testability = $injector.get('$$testability'); + } catch (e) {} + return {$injector: $injector, $$testability: $$testability}; + } else { + return tryEl(document.body) || + trySelector('[ng-app]') || trySelector('[ng:app]') || + trySelector('[ng-controller]') || trySelector('[ng:controller]'); + } +} + +/* Wraps a function up into a string with its helper functions so that it can + * call those helper functions client side + * + * @param {function} fun The function to wrap up with its helpers + * @param {...function} The helper functions. Each function must be named + * + * @return {string} The string which, when executed, will invoke fun in such a + * way that it has access to its helper functions + */ +function wrapWithHelpers(fun) { + var helpers = Array.prototype.slice.call(arguments, 1); + if (!helpers.length) { + return fun; + } + var FunClass = Function; // Get the linter to allow this eval + return new FunClass( + helpers.join(';') + String.fromCharCode(59) + + ' return (' + fun.toString() + ').apply(this, arguments);'); +} +exports.NG_WAIT_FN = wrapWithHelpers(waitForAngular, getNg1Hooks); diff --git a/lib/bin.ts b/lib/bin.ts index e70edb3..7505d00 100644 --- a/lib/bin.ts +++ b/lib/bin.ts @@ -15,7 +15,7 @@ if (argv.help) { process.exit(0); } -const proxy = new BlockingProxy(argv.seleniumAddress, argv.rootElement); +const proxy = new BlockingProxy(argv.seleniumAddress); if (argv.logDir) { proxy.enableLogging(argv.logDir); } diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 290d1d7..2370f0b 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -5,6 +5,7 @@ import {parseWebDriverCommand} from './webdriverCommands'; import {WebDriverLogger} from './webdriverLogger'; let angularWaits = require('./angular/wait.js'); +export const BP_PREFIX = 'bpproxy'; /** * The stability proxy is an http server responsible for intercepting @@ -15,23 +16,22 @@ export class BlockingProxy { seleniumAddress: string; // The ng-app root to use when waiting on the client. - rootElement = ''; - ng12hybrid = false; - stabilityEnabled: boolean; + rootSelector = ''; + waitEnabled: boolean; server: http.Server; logger: WebDriverLogger; - constructor(seleniumAddress, rootElement?) { + constructor(seleniumAddress) { this.seleniumAddress = seleniumAddress; - this.rootElement = rootElement || 'body'; - this.stabilityEnabled = true; + this.rootSelector = ''; + this.waitEnabled = true; this.server = http.createServer(this.requestListener.bind(this)); } waitForAngularData() { return JSON.stringify({ script: 'return (' + angularWaits.NG_WAIT_FN + ').apply(null, arguments);', - args: [this.rootElement, this.ng12hybrid] + args: [this.rootSelector] }); } @@ -39,7 +39,7 @@ export class BlockingProxy { * This command is for the proxy server, not to be forwarded to Selenium. */ static isProxyCommand(commandPath: string) { - return (commandPath.split('/')[1] === 'stabilize_proxy'); + return (commandPath.split('/')[1] === BP_PREFIX); } /** @@ -71,13 +71,20 @@ export class BlockingProxy { this.logger = logger; } + /** + * Change the parameters used by the wait function. + */ + setWaitParams(rootEl) { + this.rootSelector = rootEl; + } + /** * Return true if the requested method should trigger a stabilize first. * * @param {string} commandPath Original request url. */ shouldStabilize(commandPath) { - if (!this.stabilityEnabled) { + if (!this.waitEnabled) { return false; } @@ -138,11 +145,11 @@ export class BlockingProxy { case 'enabled': if (message.method === 'GET') { response.writeHead(200); - response.write(JSON.stringify({value: this.stabilityEnabled})); + response.write(JSON.stringify({value: this.waitEnabled})); response.end(); } else if (message.method === 'POST') { response.writeHead(200); - this.stabilityEnabled = JSON.parse(data).value; + this.waitEnabled = JSON.parse(data).value; response.end(); } else { response.writeHead(405); diff --git a/lib/client.ts b/lib/client.ts index 681a483..275c7bb 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,5 +1,6 @@ import * as http from 'http'; import * as url from 'url'; +import {BP_PREFIX} from './blockingproxy'; export class BPClient { hostname: string; @@ -11,10 +12,10 @@ export class BPClient { this.port = parseInt(bpUrl.port); } - setSynchronization(enabled: boolean) { + setWaitEnabled(enabled: boolean) { return new Promise((resolve, reject) => { let options = - {host: this.hostname, port: this.port, method: 'POST', path: '/stabilize_proxy/enabled'}; + {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/enabled`}; let request = http.request(options, (response) => { response.on('data', () => {}); @@ -28,9 +29,9 @@ export class BPClient { }); } - isSyncEnabled() { + isWaitEnabled() { return new Promise((res) => { - let options = {host: this.hostname, port: this.port, path: '/stabilize_proxy/enabled'}; + let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/enabled`}; http.get(options, (response) => { let body = ''; diff --git a/lib/config.ts b/lib/config.ts index 41402a9..1499e0a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -8,7 +8,6 @@ export interface Config { seleniumAddress?: string; logDir?: string; port?: number; - rootElement?: string; } const opts: minimist.Opts = { @@ -22,7 +21,6 @@ const opts: minimist.Opts = { default: { port: process.env.BP_PORT || 0, seleniumAddress: process.env.BP_SELENIUM_ADDRESS || 'http://localhost:4444/wd/hub', - rootElement: 'body' } }; diff --git a/spec/unit/client_spec.ts b/spec/unit/client_spec.ts index 58e4887..952c592 100644 --- a/spec/unit/client_spec.ts +++ b/spec/unit/client_spec.ts @@ -4,21 +4,27 @@ describe('BlockingProxy Client', () => { let bp: BlockingProxy; let client: BPClient; - // TODO dynamically find an open port const BP_PORT = 4111; - beforeEach(() => { + beforeAll(() => { bp = new BlockingProxy('http://localhost:3111'); bp.listen(BP_PORT); client = new BPClient(`http://localhost:${BP_PORT}`); }); - it('should set synchronization', (done) => { - expect(bp.stabilityEnabled).toBe(true); + it('should toggle waiting', async() => { + expect(bp.waitEnabled).toBe(true); - client.setSynchronization(false).then(() => { - expect(bp.stabilityEnabled).toBe(false); - done(); - }); + await client.setWaitEnabled(false); + expect(bp.waitEnabled).toBe(false); }); -}); \ No newline at end of file + + it('allows changing the root selector', () => { + bp.rootSelector= ''; + const newRoot = 'div#app'; + + //await client.setWaitParams(newRoot); + //expect(bp.rootSelector).toBe(newRoot); + }); +}); + diff --git a/spec/unit/proxy_spec.ts b/spec/unit/proxy_spec.ts index 0e26cbe..e8ac5c5 100644 --- a/spec/unit/proxy_spec.ts +++ b/spec/unit/proxy_spec.ts @@ -3,6 +3,6 @@ import {BlockingProxy} from '../../lib/blockingproxy'; describe('BlockingProxy', () => { it('should be able to be created', () => { let proxy = new BlockingProxy(8111); - expect(proxy.stabilityEnabled).toBe(true); + expect(proxy.waitEnabled).toBe(true); }); }); From d026f6ebc29bad34d8799090fc9b13be10e649b2 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 12:03:53 -0800 Subject: [PATCH 2/4] Allow setting rootSelector dynamically. Also make naming more consistent. --- lib/blockingproxy.ts | 17 +++++++++++++++- lib/client.ts | 44 +++++++++++++++++++++++++++++++++++++--- spec/unit/client_spec.ts | 22 ++++++++++++-------- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/lib/blockingproxy.ts b/lib/blockingproxy.ts index 2370f0b..7060467 100644 --- a/lib/blockingproxy.ts +++ b/lib/blockingproxy.ts @@ -142,7 +142,7 @@ export class BlockingProxy { handleProxyCommand(message, data, response) { let command = message.url.split('/')[2]; switch (command) { - case 'enabled': + case 'waitEnabled': if (message.method === 'GET') { response.writeHead(200); response.write(JSON.stringify({value: this.waitEnabled})); @@ -157,6 +157,21 @@ export class BlockingProxy { response.end(); } break; + case 'waitParams': + if (message.method === 'GET') { + response.writeHead(200); + response.write(JSON.stringify({rootSelector: this.rootSelector})); + response.end(); + } else if (message.method === 'POST') { + response.writeHead(200); + this.rootSelector = JSON.parse(data).rootSelector; + response.end(); + } else { + response.writeHead(405); + response.write('Invalid method'); + response.end(); + } + break; case 'selenium_address': if (message.method === 'GET') { response.writeHead(200); diff --git a/lib/client.ts b/lib/client.ts index 275c7bb..71a6284 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -12,10 +12,16 @@ export class BPClient { this.port = parseInt(bpUrl.port); } - setWaitEnabled(enabled: boolean) { + /** + * Toggle whether waiting for Angular is enabled. + * + * @param enabled Whether or not to enable waiting for angular. + * @returns {Promise} + */ + setWaitEnabled(enabled: boolean): Promise { return new Promise((resolve, reject) => { let options = - {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/enabled`}; + {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitEnabled`}; let request = http.request(options, (response) => { response.on('data', () => {}); @@ -29,9 +35,41 @@ export class BPClient { }); } + /** + * A CSS Selector for a DOM element within your Angular application. + * BlockingProxy will attempt to automatically find your application, but it is + * necessary to set rootElement in certain cases. + * + * In Angular 1, BlockingProxy will use the element your app bootstrapped to by + * default. If that doesn't work, it will then search for hooks in `body` or + * `ng-app` elements (details here: https://git.io/v1b2r). + * + * In later versions of Angular, BlockingProxy will try to hook into all angular + * apps on the page. Use rootElement to limit the scope of which apps + * BlockingProxy waits for and searches within. + * + * @param rootSelector A selector for the root element of the Angular app. + */ + setWaitParams(rootSelector: string): Promise { + return new Promise((resolve, reject) => { + let options = + {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitParams`}; + + let request = http.request(options, (response) => { + response.on('data', () => {}); + response.on('error', (err) => reject(err)); + response.on('end', () => { + resolve(); + }); + }); + request.write(JSON.stringify({rootSelector: rootSelector})); + request.end(); + }); + } + isWaitEnabled() { return new Promise((res) => { - let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/enabled`}; + let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/waitEnabled`}; http.get(options, (response) => { let body = ''; diff --git a/spec/unit/client_spec.ts b/spec/unit/client_spec.ts index 952c592..4db8384 100644 --- a/spec/unit/client_spec.ts +++ b/spec/unit/client_spec.ts @@ -4,12 +4,10 @@ describe('BlockingProxy Client', () => { let bp: BlockingProxy; let client: BPClient; - const BP_PORT = 4111; - beforeAll(() => { bp = new BlockingProxy('http://localhost:3111'); - bp.listen(BP_PORT); - client = new BPClient(`http://localhost:${BP_PORT}`); + let bpPort = bp.listen(0); + client = new BPClient(`http://localhost:${bpPort}`); }); it('should toggle waiting', async() => { @@ -19,12 +17,18 @@ describe('BlockingProxy Client', () => { expect(bp.waitEnabled).toBe(false); }); - it('allows changing the root selector', () => { - bp.rootSelector= ''; + it('can get whether wait is enabled', async() => { + bp.waitEnabled = true; + expect(await client.isWaitEnabled()).toBeTruthy(); + bp.waitEnabled = false; + expect(await client.isWaitEnabled()).toBeFalsy(); + }); + + it('allows changing the root selector', async() => { + bp.rootSelector = ''; const newRoot = 'div#app'; - //await client.setWaitParams(newRoot); - //expect(bp.rootSelector).toBe(newRoot); + await client.setWaitParams(newRoot); + expect(bp.rootSelector).toBe(newRoot); }); }); - From 265bd82aa88496c0b8302ce750d7c8d6cd9b9848 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 12:27:45 -0800 Subject: [PATCH 3/4] Add polling spec from Protractor. --- spec/e2e/ng1_polling_spec.ts | 37 ++++++++++++++++++++++++++++++++ testapp/ng1/app.js | 1 + testapp/ng1/polling/polling.html | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 spec/e2e/ng1_polling_spec.ts diff --git a/spec/e2e/ng1_polling_spec.ts b/spec/e2e/ng1_polling_spec.ts new file mode 100644 index 0000000..c09cdda --- /dev/null +++ b/spec/e2e/ng1_polling_spec.ts @@ -0,0 +1,37 @@ +import * as webdriver from 'selenium-webdriver'; +import {getTestEnv} from './environment'; +import {BlockingProxy} from "../../lib/blockingproxy"; + +const By = webdriver.By; + +describe('disabling waiting as needed', function() { + let driver: webdriver.WebDriver; + let bp: BlockingProxy; + + beforeAll(() => { + ({driver, bp} = getTestEnv()); + }); + + beforeEach(async() => { + await driver.get('http://localhost:8081/ng1/#/polling'); + }); + + it('avoids timeouts', async() => { + bp.waitEnabled = true; + + let startButton = await driver.findElement(By.id('pollstarter')); + + let count = await driver.findElement(By.id('count')); + expect(await count.getText()).toEqual('0'); + + await startButton.click(); + + bp.waitEnabled = false; + + expect(await count.getText()).toBeGreaterThan(-1); + + await driver.sleep(2000); + + expect(await count.getText()).toBeGreaterThan(1); + }); +}); diff --git a/testapp/ng1/app.js b/testapp/ng1/app.js index d462e4c..53d89a7 100644 --- a/testapp/ng1/app.js +++ b/testapp/ng1/app.js @@ -5,6 +5,7 @@ angular.module('myApp', ['ngRoute', 'myApp.appVersion']). config(['$routeProvider', function($routeProvider) { $routeProvider.when('/async', {templateUrl: 'async/async.html', controller: AsyncCtrl}); + $routeProvider.when('/polling', {templateUrl: 'polling/polling.html', controller: PollingCtrl}); $routeProvider.when('/interaction', {templateUrl: 'interaction/interaction.html', controller: InteractionCtrl}); $routeProvider.when('/slowloader', { diff --git a/testapp/ng1/polling/polling.html b/testapp/ng1/polling/polling.html index 789da57..0d50de6 100644 --- a/testapp/ng1/polling/polling.html +++ b/testapp/ng1/polling/polling.html @@ -1,4 +1,4 @@
This view shows a controller which uses a polling mechanism to contact the server. It is constantly using angular's $timeout.
-
{{count}}
+
{{count}}
From 4b4eec233aa09ee265131ae338e9fba0cf0fcbf2 Mon Sep 17 00:00:00 2001 From: Michael Giambalvo Date: Sat, 21 Jan 2017 12:35:52 -0800 Subject: [PATCH 4/4] clang format --- spec/e2e/ng1_polling_spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/e2e/ng1_polling_spec.ts b/spec/e2e/ng1_polling_spec.ts index c09cdda..b3cae6a 100644 --- a/spec/e2e/ng1_polling_spec.ts +++ b/spec/e2e/ng1_polling_spec.ts @@ -1,6 +1,8 @@ import * as webdriver from 'selenium-webdriver'; + +import {BlockingProxy} from '../../lib/blockingproxy'; + import {getTestEnv} from './environment'; -import {BlockingProxy} from "../../lib/blockingproxy"; const By = webdriver.By;