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

fix(waitForAngular): Allow setting the angular root selector dynamically. #21

Merged
merged 4 commits into from
Jan 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 107 additions & 38 deletions lib/angular/wait.js
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 1 addition & 1 deletion lib/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
46 changes: 34 additions & 12 deletions lib/blockingproxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,31 +16,30 @@ 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]
});
}

/**
* 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);
}

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -135,14 +142,29 @@ 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}));
response.end();
} else if (message.method === 'POST') {
response.writeHead(200);
this.waitEnabled = JSON.parse(data).value;
response.end();
} else {
response.writeHead(405);
response.write('Invalid method');
response.end();
}
break;
case 'waitParams':
if (message.method === 'GET') {
response.writeHead(200);
response.write(JSON.stringify({value: this.stabilityEnabled}));
response.write(JSON.stringify({rootSelector: this.rootSelector}));
response.end();
} else if (message.method === 'POST') {
response.writeHead(200);
this.stabilityEnabled = JSON.parse(data).value;
this.rootSelector = JSON.parse(data).rootSelector;
response.end();
} else {
response.writeHead(405);
Expand Down
47 changes: 43 additions & 4 deletions lib/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as http from 'http';
import * as url from 'url';
import {BP_PREFIX} from './blockingproxy';

export class BPClient {
hostname: string;
Expand All @@ -11,10 +12,16 @@ export class BPClient {
this.port = parseInt(bpUrl.port);
}

setSynchronization(enabled: boolean) {
/**
* Toggle whether waiting for Angular is enabled.
*
* @param enabled Whether or not to enable waiting for angular.
* @returns {Promise<T>}
*/
setWaitEnabled(enabled: boolean): Promise<any> {
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}/waitEnabled`};

let request = http.request(options, (response) => {
response.on('data', () => {});
Expand All @@ -28,9 +35,41 @@ export class BPClient {
});
}

isSyncEnabled() {
/**
* 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<any> {
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: '/stabilize_proxy/enabled'};
let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/waitEnabled`};

http.get(options, (response) => {
let body = '';
Expand Down
2 changes: 0 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export interface Config {
seleniumAddress?: string;
logDir?: string;
port?: number;
rootElement?: string;
}

const opts: minimist.Opts = {
Expand All @@ -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'
}
};

Expand Down
Loading