Linked Questions

Popular Questions

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?

Related Questions