Note this is not specific to Protractor. The issue is with Angular 2's built-in Testability service which Protractor happens to use. Protractor invokes Testability.whenStable
via a call to waitForAngular
. I've hit some code where this fails.
My test code looks something like this:
await renderFooView();
await interactWithFooView();
The second lines fails:
Failed: No element found using locator: By(css selector, foo-view)
Angular is not done rendering 'foo-view' when Protractor continues with code that tries to interact with it.
If I add a sleep in between, it works:
await renderFooView();
await browser.sleep(1000);
await interactWithFooView();
Obviously I don't want to do that. Most of the value of Protractor for me is the "wait for angular" mechanism which eliminates the "wait for X" noise from my scripts. What I want is this:
await renderFooView();
await browser.waitForAngular();
await interactWithFooView();
And in fact, I should never have to manually execute that middle line. Protractor does it automatically any time I make a call that interacts with browser.
After doing some digging, I've found that Protractor is making the call, it is working correctly, but the underlying Testability mechanism in Angular 2 appears broken.
Under Angular 2, Protractor's "waitForAngular" does something like the following:
let rootElement = window.getAllAngularRootElements()[0];
let testability = window.getAngularTestability(rootElement);
testability.whenStable(callbackThatResumesScriptExecution);
In other words, it invokes Angular's testability.whenStable
and only resumes execution when Angular reports that it is stable. If I add some logging in the callback:
testability.whenStable(() => {
console.log("isStable:", testability.isStable());
callback();
});
isStable()
is always true inside of the whenStable
callback, so Angular is definitely calling at what appears to be the right time.
However, if immediately after this callback returns, I poll isStable()
again, it evaluates to false.
let pollAngularIsStable = `{
let rootElement = window.getAllAngularRootElements()[0];
let testability = window.getAngularTestability(rootElement);
return testability.isStable();
}`;
await renderFooView();
await browser.waitForAngular();
console.log(await browser.executeScript(pollAngularIsStable)); // => false
await interactWithFooView();
I've written my own version of browser.waitForAngular
, and I see the exact same results:
let waitForAngularStable = `
let callback = arguments[arguments.length - 1];
let rootElement = window.getAllAngularRootElements()[0];
let testability = window.getAngularTestability(rootElement);
testability.whenStable(callback);
`;
await renderFooView();
await browser.executeAsyncScript(waitForAngularStable);
console.log(await browser.executeScript(pollAngularIsStable)); // => false
await interactWithFooView();
If I write a routine that manually polls isStable()
until it returns true, that fixes my script:
async function waitForAngular() {
let ms;
for (ms=0; ms<10000; ++ms) {
await browser.sleep(1);
if (await browser.executeScript(pollAngularIsStable)) {
break;
}
}
console.log(`Waited ${ms}ms for Angular to be stable.`);
}
await renderFooView();
await waitForAngular(); // usually waits < 50ms
console.log(await browser.executeScript(pollAngularIsStable)); // => true
await interactWithFooView(); // always works now
So... why does polling isStable()
work, but waiting for the whenStable()
callback not work? More important, why does whenStable()
report that the app is stable, when the very next call (no intervening code) show that isStable()
is false?
await browser.sleep(...)
is still required in some places to make specs more stable. I wish we could solely rely on the built-in mechanisms too. – Entremetsbrowser.sleep
in your code. It will work as protractor should have. – Cadmar