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

Commit a648290

Browse files
authored
fix(waitForAngular): Allow setting the angular root selector dynamically. (#21)
* Update to Protractor 5 wait functions, make naming more consistent. * Allow setting rootSelector dynamically. Also make naming more consistent. * Add polling spec from Protractor.
1 parent f6345e3 commit a648290

File tree

10 files changed

+250
-72
lines changed

10 files changed

+250
-72
lines changed

lib/angular/wait.js

+107-38
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,136 @@
11
/**
22
* Wait until Angular has finished rendering and has
33
* no outstanding $http calls before continuing. The specific Angular app
4-
* is determined by the rootSelector.
4+
* is determined by the rootSelector. Copied from Protractor 5.
55
*
66
* Asynchronous.
77
*
88
* @param {string} rootSelector The selector housing an ng-app
9-
* @param {boolean} ng12Hybrid Flag set if app is a hybrid of angular 1 and 2
109
* @param {function(string)} callback callback. If a failure occurs, it will
1110
* be passed as a parameter.
1211
*/
13-
exports.NG_WAIT_FN = function(rootSelector, ng12Hybrid, callback) {
14-
var el = document.querySelector(rootSelector);
15-
12+
function waitForAngular(rootSelector, callback) {
1613
try {
17-
if (!ng12Hybrid && window.getAngularTestability) {
14+
if (window.angular && !(window.angular.version &&
15+
window.angular.version.major > 1)) {
16+
/* ng1 */
17+
var hooks = getNg1Hooks(rootSelector);
18+
if (hooks.$$testability) {
19+
hooks.$$testability.whenStable(callback);
20+
} else if (hooks.$injector) {
21+
hooks.$injector.get('$browser').
22+
notifyWhenNoOutstandingRequests(callback);
23+
} else if (!!rootSelector) {
24+
throw new Error('Could not automatically find injector on page: "' +
25+
window.location.toString() + '". Consider using config.rootEl');
26+
} else {
27+
throw new Error('root element (' + rootSelector + ') has no injector.' +
28+
' this may mean it is not inside ng-app.');
29+
}
30+
} else if (rootSelector && window.getAngularTestability) {
31+
var el = document.querySelector(rootSelector);
1832
window.getAngularTestability(el).whenStable(callback);
19-
return;
20-
}
21-
if (!window.angular) {
33+
} else if (window.getAllAngularTestabilities) {
34+
var testabilities = window.getAllAngularTestabilities();
35+
var count = testabilities.length;
36+
var decrement = function() {
37+
count--;
38+
if (count === 0) {
39+
callback();
40+
}
41+
};
42+
testabilities.forEach(function(testability) {
43+
testability.whenStable(decrement);
44+
});
45+
} else if (!window.angular) {
2246
throw new Error('window.angular is undefined. This could be either ' +
2347
'because this is a non-angular page or because your test involves ' +
2448
'client-side navigation, which can interfere with Protractor\'s ' +
2549
'bootstrapping. See http://git.io/v4gXM for details');
26-
}
27-
if (angular.getTestability) {
28-
angular.getTestability(el).whenStable(callback);
50+
} else if (window.angular.version >= 2) {
51+
throw new Error('You appear to be using angular, but window.' +
52+
'getAngularTestability was never set. This may be due to bad ' +
53+
'obfuscation.');
2954
} else {
30-
if (!angular.element(el).injector()) {
31-
throw new Error('root element (' + rootSelector + ') has no injector.' +
32-
' this may mean it is not inside ng-app.');
33-
}
34-
angular.element(el).injector().get('$browser').
35-
notifyWhenNoOutstandingRequests(callback);
55+
throw new Error('Cannot get testability API for unknown angular ' +
56+
'version "' + window.angular.version + '"');
3657
}
3758
} catch (err) {
3859
callback(err.message);
3960
}
4061
};
4162

42-
/**
43-
* Wait until all Angular2 applications on the page have become stable.
63+
/* Tries to find $$testability and possibly $injector for an ng1 app
4464
*
45-
* Asynchronous.
65+
* By default, doesn't care about $injector if it finds $$testability. However,
66+
* these priorities can be reversed.
4667
*
47-
* @param {function(string)} callback callback. If a failure occurs, it will
48-
* be passed as a parameter.
68+
* @param {string=} selector The selector for the element with the injector. If
69+
* falsy, tries a variety of methods to find an injector
70+
* @param {boolean=} injectorPlease Prioritize finding an injector
71+
* @return {$$testability?: Testability, $injector?: Injector} Returns whatever
72+
* ng1 app hooks it finds
4973
*/
50-
exports.NG2_WAIT_FN = function(callback) {
51-
try {
52-
var testabilities = window.getAllAngularTestabilities();
53-
var count = testabilities.length;
54-
var decrement = function() {
55-
count--;
56-
if (count === 0) {
57-
callback();
74+
function getNg1Hooks(selector, injectorPlease) {
75+
function tryEl(el) {
76+
try {
77+
if (!injectorPlease && angular.getTestability) {
78+
var $$testability = angular.getTestability(el);
79+
if ($$testability) {
80+
return {$$testability: $$testability};
81+
}
82+
} else {
83+
var $injector = angular.element(el).injector();
84+
if ($injector) {
85+
return {$injector: $injector};
86+
}
5887
}
59-
};
60-
testabilities.forEach(function(testability) {
61-
testability.whenStable(decrement);
62-
});
63-
} catch (err) {
64-
callback(err.message);
88+
} catch(err) {}
6589
}
66-
};
90+
function trySelector(selector) {
91+
var els = document.querySelectorAll(selector);
92+
for (var i = 0; i < els.length; i++) {
93+
var elHooks = tryEl(els[i]);
94+
if (elHooks) {
95+
return elHooks;
96+
}
97+
}
98+
}
99+
100+
if (selector) {
101+
return trySelector(selector);
102+
} else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) {
103+
var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__;
104+
var $$testability = null;
105+
try {
106+
$$testability = $injector.get('$$testability');
107+
} catch (e) {}
108+
return {$injector: $injector, $$testability: $$testability};
109+
} else {
110+
return tryEl(document.body) ||
111+
trySelector('[ng-app]') || trySelector('[ng:app]') ||
112+
trySelector('[ng-controller]') || trySelector('[ng:controller]');
113+
}
114+
}
115+
116+
/* Wraps a function up into a string with its helper functions so that it can
117+
* call those helper functions client side
118+
*
119+
* @param {function} fun The function to wrap up with its helpers
120+
* @param {...function} The helper functions. Each function must be named
121+
*
122+
* @return {string} The string which, when executed, will invoke fun in such a
123+
* way that it has access to its helper functions
124+
*/
125+
function wrapWithHelpers(fun) {
126+
var helpers = Array.prototype.slice.call(arguments, 1);
127+
if (!helpers.length) {
128+
return fun;
129+
}
130+
var FunClass = Function; // Get the linter to allow this eval
131+
return new FunClass(
132+
helpers.join(';') + String.fromCharCode(59) +
133+
' return (' + fun.toString() + ').apply(this, arguments);');
134+
}
67135

136+
exports.NG_WAIT_FN = wrapWithHelpers(waitForAngular, getNg1Hooks);

lib/bin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ if (argv.help) {
1515
process.exit(0);
1616
}
1717

18-
const proxy = new BlockingProxy(argv.seleniumAddress, argv.rootElement);
18+
const proxy = new BlockingProxy(argv.seleniumAddress);
1919
if (argv.logDir) {
2020
proxy.enableLogging(argv.logDir);
2121
}

lib/blockingproxy.ts

+34-12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {parseWebDriverCommand} from './webdriverCommands';
55
import {WebDriverLogger} from './webdriverLogger';
66

77
let angularWaits = require('./angular/wait.js');
8+
export const BP_PREFIX = 'bpproxy';
89

910
/**
1011
* The stability proxy is an http server responsible for intercepting
@@ -15,31 +16,30 @@ export class BlockingProxy {
1516
seleniumAddress: string;
1617

1718
// The ng-app root to use when waiting on the client.
18-
rootElement = '';
19-
ng12hybrid = false;
20-
stabilityEnabled: boolean;
19+
rootSelector = '';
20+
waitEnabled: boolean;
2121
server: http.Server;
2222
logger: WebDriverLogger;
2323

24-
constructor(seleniumAddress, rootElement?) {
24+
constructor(seleniumAddress) {
2525
this.seleniumAddress = seleniumAddress;
26-
this.rootElement = rootElement || 'body';
27-
this.stabilityEnabled = true;
26+
this.rootSelector = '';
27+
this.waitEnabled = true;
2828
this.server = http.createServer(this.requestListener.bind(this));
2929
}
3030

3131
waitForAngularData() {
3232
return JSON.stringify({
3333
script: 'return (' + angularWaits.NG_WAIT_FN + ').apply(null, arguments);',
34-
args: [this.rootElement, this.ng12hybrid]
34+
args: [this.rootSelector]
3535
});
3636
}
3737

3838
/**
3939
* This command is for the proxy server, not to be forwarded to Selenium.
4040
*/
4141
static isProxyCommand(commandPath: string) {
42-
return (commandPath.split('/')[1] === 'stabilize_proxy');
42+
return (commandPath.split('/')[1] === BP_PREFIX);
4343
}
4444

4545
/**
@@ -71,13 +71,20 @@ export class BlockingProxy {
7171
this.logger = logger;
7272
}
7373

74+
/**
75+
* Change the parameters used by the wait function.
76+
*/
77+
setWaitParams(rootEl) {
78+
this.rootSelector = rootEl;
79+
}
80+
7481
/**
7582
* Return true if the requested method should trigger a stabilize first.
7683
*
7784
* @param {string} commandPath Original request url.
7885
*/
7986
shouldStabilize(commandPath) {
80-
if (!this.stabilityEnabled) {
87+
if (!this.waitEnabled) {
8188
return false;
8289
}
8390

@@ -135,14 +142,29 @@ export class BlockingProxy {
135142
handleProxyCommand(message, data, response) {
136143
let command = message.url.split('/')[2];
137144
switch (command) {
138-
case 'enabled':
145+
case 'waitEnabled':
146+
if (message.method === 'GET') {
147+
response.writeHead(200);
148+
response.write(JSON.stringify({value: this.waitEnabled}));
149+
response.end();
150+
} else if (message.method === 'POST') {
151+
response.writeHead(200);
152+
this.waitEnabled = JSON.parse(data).value;
153+
response.end();
154+
} else {
155+
response.writeHead(405);
156+
response.write('Invalid method');
157+
response.end();
158+
}
159+
break;
160+
case 'waitParams':
139161
if (message.method === 'GET') {
140162
response.writeHead(200);
141-
response.write(JSON.stringify({value: this.stabilityEnabled}));
163+
response.write(JSON.stringify({rootSelector: this.rootSelector}));
142164
response.end();
143165
} else if (message.method === 'POST') {
144166
response.writeHead(200);
145-
this.stabilityEnabled = JSON.parse(data).value;
167+
this.rootSelector = JSON.parse(data).rootSelector;
146168
response.end();
147169
} else {
148170
response.writeHead(405);

lib/client.ts

+43-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as http from 'http';
22
import * as url from 'url';
3+
import {BP_PREFIX} from './blockingproxy';
34

45
export class BPClient {
56
hostname: string;
@@ -11,10 +12,16 @@ export class BPClient {
1112
this.port = parseInt(bpUrl.port);
1213
}
1314

14-
setSynchronization(enabled: boolean) {
15+
/**
16+
* Toggle whether waiting for Angular is enabled.
17+
*
18+
* @param enabled Whether or not to enable waiting for angular.
19+
* @returns {Promise<T>}
20+
*/
21+
setWaitEnabled(enabled: boolean): Promise<any> {
1522
return new Promise((resolve, reject) => {
1623
let options =
17-
{host: this.hostname, port: this.port, method: 'POST', path: '/stabilize_proxy/enabled'};
24+
{host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitEnabled`};
1825

1926
let request = http.request(options, (response) => {
2027
response.on('data', () => {});
@@ -28,9 +35,41 @@ export class BPClient {
2835
});
2936
}
3037

31-
isSyncEnabled() {
38+
/**
39+
* A CSS Selector for a DOM element within your Angular application.
40+
* BlockingProxy will attempt to automatically find your application, but it is
41+
* necessary to set rootElement in certain cases.
42+
*
43+
* In Angular 1, BlockingProxy will use the element your app bootstrapped to by
44+
* default. If that doesn't work, it will then search for hooks in `body` or
45+
* `ng-app` elements (details here: https://git.io/v1b2r).
46+
*
47+
* In later versions of Angular, BlockingProxy will try to hook into all angular
48+
* apps on the page. Use rootElement to limit the scope of which apps
49+
* BlockingProxy waits for and searches within.
50+
*
51+
* @param rootSelector A selector for the root element of the Angular app.
52+
*/
53+
setWaitParams(rootSelector: string): Promise<any> {
54+
return new Promise((resolve, reject) => {
55+
let options =
56+
{host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitParams`};
57+
58+
let request = http.request(options, (response) => {
59+
response.on('data', () => {});
60+
response.on('error', (err) => reject(err));
61+
response.on('end', () => {
62+
resolve();
63+
});
64+
});
65+
request.write(JSON.stringify({rootSelector: rootSelector}));
66+
request.end();
67+
});
68+
}
69+
70+
isWaitEnabled() {
3271
return new Promise((res) => {
33-
let options = {host: this.hostname, port: this.port, path: '/stabilize_proxy/enabled'};
72+
let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/waitEnabled`};
3473

3574
http.get(options, (response) => {
3675
let body = '';

lib/config.ts

-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface Config {
88
seleniumAddress?: string;
99
logDir?: string;
1010
port?: number;
11-
rootElement?: string;
1211
}
1312

1413
const opts: minimist.Opts = {
@@ -22,7 +21,6 @@ const opts: minimist.Opts = {
2221
default: {
2322
port: process.env.BP_PORT || 0,
2423
seleniumAddress: process.env.BP_SELENIUM_ADDRESS || 'http://localhost:4444/wd/hub',
25-
rootElement: 'body'
2624
}
2725
};
2826

0 commit comments

Comments
 (0)