testability.whenStable() returns, testability.isStable() returns false
Asked Answered
C

1

6

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?

Cadmar answered 4/2, 2019 at 2:51 Comment(2)
Unfortunately, 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.Entremets
If you use the final version of waitForAngular that I showed above, you won't need arbitrarily long browser.sleep in your code. It will work as protractor should have.Cadmar
T
0

I had faced a similar issue in the past when working with Protractor and Angular application. In my case, there was a third party plugin that was running for a longer time than expected. Because of that the isStable flag would show false until it loads completely. We dug deep and figured there were two ways to fix this:

1) Remove the third party integration in end to end testing suite and run the tests (this solved the issue and we did not require to use browser.sleep

2) Write a helper function that will continuously ping the testability api and monitor the isStable flag. It will wait for the flag to become true and then move on with the test. This is like adding a wait for angular wrapper on top of wait for angular.

We went for the first solution because that helped.

On a side note: You could also be missing an await somewhere in your code because of which a promise is not resolved and the test is not waiting for the isStable flag before searching for the element. Can you switch off the selenium promise manager in your tests and try to see if any promise is being rejected? You can do this by adding a flag in your protractor configuration file: SELENIUM_PROMISE_MANAGER: false

Thumbprint answered 23/12, 2019 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.