-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Difficulty overriding browser.waitForAngular #1358
Description
Hello,
My team is in a somewhat unusual position, and so we have a somewhat odd use-case. Instead of a single angular app on the page, we have one page-loader
app that dynamically determines which other apps to show to the current user, loads those apps, and bootstraps them. (I can speak in more detail about why this design decision makes sense for us if you're interested but for the purposes of this issue let's just assume that it's reasonable.)
Additionally, we don't even have a rootElement
for the page-loader
- our entry point is a script tag which bootstraps the page-loader
on a div that's not in the DOM. So because of our lack of a root element and because we have multiple angular apps, we need to implement custom logic for determining if our page is ready for protractor's tests to proceed.
We have this code that sets up a waitForStateTracker
method on browser
, which we adapted from protractor. This method is used by tests to specify which app / load phase they need to wait for to run:
if (typeof browser !== 'undefined') {
browser.waitForStateTracker = function(module, phase) {
// NB: this code is adapted from protractor.waitForAngular.
return browser.driver.executeAsyncScript(
waitForStateTracker,
module,
phase
).then(function(browserErr) {
if (browserErr) {
throw 'Error while waiting for Protractor to ' +
'sync with the page: ' + JSON.stringify(browserErr);
}
}).then(null, function(err) {
var timeout;
if (/asynchronous script timeout/.test(err.message)) {
// Timeout on Chrome
timeout = /-?[\d\.]*\ seconds/.exec(err.message);
} else if (/Timed out waiting for async script/.test(err.message)) {
// Timeout on Firefox
timeout = /-?[\d\.]*ms/.exec(err.message);
} else if (/Timed out waiting for an asynchronous script/.test(err.message)) {
// Timeout on Safari
timeout = /-?[\d\.]*\ ms/.exec(err.message);
}
if (timeout) {
throw 'Timed out waiting for Protractor to synchronize with ' +
'the page after ' + timeout + '. Please see ' +
'https://github.com/angular/protractor/blob/master/docs/faq.md';
} else {
throw err;
}
});
};
}
(The details of how stateTracker
works is unimportant here, but again, I can share if you're curious.)
Then, we use this method to override browser.waitForAngular
:
function waitForAngularWithStateTracker() {
if (browser.ignoreSynchronization) {
return originalWaitForAngular.call(browser);
}
return browser.waitForStateTracker(widgetName, xWebLookup.constants.INITIAL_READY_STATE);
}
if (typeof browser !== 'undefined') {
originalWaitForAngular = browser.waitForAngular;
browser.waitForAngular = waitForAngularWithStateTracker;
browser.enableStateTracker = function() {
browser.waitForAngular = waitForAngularWithStateTracker;
};
browser.disableStateTracker = function() {
browser.waitForAngular = originalWaitForAngular;
};
}
This all works fine, and our tests are able to wait for the proper events before running. However, if a test includes a call to browser.getTitle()
, browser.getPageSource()
, or browser.getCurrentUrl()
, we fail with the following error:
Error: done() invoked with non-Error: Error while waiting for Protractor to sync with the page: {}
at /Users/nick.heiner/opower/x-web-taskmaster/node_modules/mocha/lib/runnable.js:222:25
at /Users/nick.heiner/opower/x-web-taskmaster/node_modules/grunt-x-web-protractor-runner/node_modules/protractor/node_modules/selenium-webdriver/lib/goog/base.js:1243:15
at webdriver.promise.ControlFlow.runInNewFrame_ (/Users/nick.heiner/opower/x-web-taskmaster/node_modules/grunt-x-web-protractor-runner/node_modules/protractor/node_modules/selenium-webdriver/lib/webdriver/promise.js:1539:20)
With some console logging, I can see that if a test contains those methods, then we use the original waitForAngular
instead of our waitForAngular
:
functions.waitForAngular = function(selector, callback) {
var el = document.querySelector(selector);
try {
if (angular.getTestability) {
angular.getTestability(el).whenStable(callback);
} else {
angular.element(el).injector().get('$browser').
notifyWhenNoOutstandingRequests(callback);
}
} catch (e) {
callback(e);
}
};
We don't have a rootElement
, so this will fail.
It appears that the reason for this is the following in protractor.js:
// These functions should delegate to the webdriver instance, but should
// wait for Angular to sync up before performing the action. This does not
// include functions which are overridden by protractor below.
var methodsToSync = ['getCurrentUrl', 'getPageSource', 'getTitle'];
// Mix all other driver functionality into Protractor.
for (var method in webdriverInstance) {
if(!this[method] && typeof webdriverInstance[method] == 'function') {
if (methodsToSync.indexOf(method) !== -1) {
mixin(this, webdriverInstance, method, this.waitForAngular.bind(this));
} else {
mixin(this, webdriverInstance, method);
}
}
}
If I make the methodsToSync
list empty, everything works as expected.
My questions are as follows:
- Is this issue a symptom of our overall approach to overriding the waitForAngular logic being flawed?
- Is overriding
waitForAngular
even an officially supported use-case? - Would you consider making this
methodsToSync
behavior optional? Or is there another workaround you would suggest?
Thanks!