Search code examples
angulartypescriptprotractorend-to-end

How to wait for URL to load after button click in protractor?


I'm doing an end-to-end testing using protractor on an angular 2 project and i'm required to test a login operation. The problem here is that when login button is clicked the expect statement still gets the login page's URL as supposed to home page. If I wait, then the test succeeds. I have read somewhere in the documentation that protractor waits for processes to complete automatically, so,

  1. What is causing this error?
  2. How can I perform the operation correctly without explicitly using wait() method?
  3. Does protractor have something similar to fakeAsync, async or atleast whenStable like in unit test?

Here's my test case

it("User successfully login",()=>{
    browser.get(browser.baseUrl + '/account/login').then(() => {
        return expect(browser.getCurrentUrl()).toBe(browser.baseUrl +  "/account/login")
    }).then(()=>{
        element(by.tagName('input[type=text]')).sendKeys('username');
        element(by.tagName('input[type=password]')).sendKeys('password');
        element(by.css('button[type=submit]')).click().then(()=>{
            return expect(browser.getCurrentUrl()).toBe(browser.baseUrl + "/home");
        });

    });
});

Note:

  1. I have instructions to avoid using browser.wait() method.
  2. Login button sends an http request to the backend via API.

Workaround solution [Based on Ernst's solution]:

I sort of solved this problem. I don't know if it works for other scenarios or if it's the correct approach. I added

browser.wait(EC.urlContains(browser.baseUrl + "/account/login"), 10000);
browser.waitForAngularEnabled(false);
browser.wait(EC.urlContains(browser.baseUrl + "/home");

just after click() event and it seems to work for now.


Solution

  • What causes the error:

    click() returns a promise, which protractor waits to be resolved. It is probably resolved the moment, when the login-request got sent to the server and not when the homepage is loaded. Therefore the execution of the next step starts before the page is loaded.

    Why not wait():

    I'm not sure, on what base you got the instruction not to use browser.wait(). Most probably you shouldn't use browser.sleep() (because that command is always obsolete), but if you don't want to use browser.wait() your developers may not once leave the initially opened Angular Page.

    fakeAsync, whenStable and more:

    Protractor uses whenStable by default within browser.waitForAngular(), which is always called after a promise and as long as you didn't switch off browser.waitForAngularEnabled(false)... which you only need to switch off for non-angular pages or if your developers use i.e. long lasting Macrotasks, that would lead to timeouts.

    Further because the Selenium Control Flow will get deprecated in probably November 2018 (read about here), there is async/await available. Read this important guide here and also some further infos from protractor about it here

    Now in your case:

    You don't need to use then()s in your case, as you don't need to resolve the promise immediately. expect() will automatically resolve your promise, so there is no need for a then() there.

    In fact Protractor executes all commands line by line as if they were all wrapped within then()s, so it automatically keeps your execution synchronous (as long as you don't start an async task by using then() in the code.

    So here my suggestion for your code:

    it("User successfully login",()=>{
        var EC = protractor.ExpectedConditions;  //or within onPrepare() of conf.js put global.EC = protractor.ExpectedConditions ... to make it everywhere available.
        browser.get(browser.baseUrl + '/account/login');
        //next line lets protractor wait max 5 seconds for your url to become, what you want.
        browser.wait(EC.urlContains(browser.baseUrl +  "/account/login"), 5000);
        //additionally after the URL appeared, use this command to wait until the page is fully loaded.
        browser.waitForAngular();
        expect(browser.getCurrentUrl()).toBe(browser.baseUrl +  "/account/login");
        element(by.tagName('input[type=text]')).sendKeys('username');
        element(by.tagName('input[type=password]')).sendKeys('password');
        element(by.css('button[type=submit]')).click();
        //next line lets protractor wait max 5 seconds for your url to become, what you want.
        //as here a new page is loaded and the click()-promise is resolved before, use this:
        browser.wait(EC.urlContains(browser.baseUrl +  "/account/login"), 5000);
        //additionally after the URL appeared, use this command to wait until the page is fully loaded.
        browser.waitForAngular();
        expect(browser.getCurrentUrl()).toBe(browser.baseUrl + "/home");
    });
    

    If you get now timeouts, then that is a separate issue to debug. Check this Protractor site for infos about timeouts and check this SO-Answer here for how to debug timeouts.