0

I'm writing a few E2E tests in an Angular application. A problem I'm having is that whenever I redirect to a new route, I'd like to verify that the correct component is being displayed. For some reason this is always resulting a failure with the following error:

- Failed: script timeout

I've been searching all over the web to try to find a solution but all the solutions I've found haven't worked for me.

Example

  • Wait for Angular

browser.waitForAngular();

Reference

  • Check the browser current URL and compare with the expected URL:
browser.driver.wait(() => {
    browser.driver.getCurrentUrl().then((url) => {
        return /expected-url/.test(url);
    });
}, 10000);

Reference


Code Example

The following is a sample of the code that is testing the login functionality.

Login Page Object Class

import { browser, element, by } from 'protractor';

export class LoginPage {
    private credentials: { username: string, password: string } = {
        username: 'admin',
        password: 'admin'
    };

    navigateTo() {
        return browser.get(browser.baseUrl) as Promise<any>;
    }

    getLoginComponent() {
        return element(by.css('ra-login'));
    }

    getUsernameTextbox() {
        return element(by.css('ra-login [data-test-id="username"]'));
    }

    getPasswordTextbox() {
        return element(by.css('ra-login [data-test-id="password"]'));
    }

    getLoginButton() {
        return element(by.css('ra-login [data-test-id="login-btn"]'));
    }

    getSecurePage() {
        return element(by.css('ra-secure-content'));
    }

    login(credentials: { username: string, password: string } = this.credentials) {
        this.getUsernameTextbox().sendKeys(credentials.username);
        this.getPasswordTextbox().sendKeys(credentials.password);
        this.getLoginButton().click();
    }

    getErrorMessage() {
        return element(by.css('ra-login [data-test-id="login-error"]'));
    }
}

Login E2E Spec File

describe('Login Functionality', () => {
    let loginPage: LoginPage;

    beforeEach(() => {
        loginPage = new LoginPage();
    });

    // Test runs successfully
    it('should display login', () => {
        loginPage.navigateTo();
        expect(loginPage.getLoginComponent().isPresent()).toBeTruthy();
    });

    // Test runs successfully
    it('should have the login button disabled', () => {
        loginPage.navigateTo();
        expect(loginPage.getLoginButton().isEnabled()).toBe(false);
    });

    // Test runs fails
    it('should redirect to a secure page', () => {
        loginPage.navigateTo();
        loginPage.login();

        expect(loginPage.getSecurePage().isPresent()).toBeTruthy();
    });

    afterEach(async () => {
        // Assert that there are no errors emitted from the browser
        const logs = await browser.manage().logs().get(logging.Type.BROWSER);
        expect(logs).not.toContain(jasmine.objectContaining({
            level: logging.Level.SEVERE,
        } as logging.Entry));
    });
});

Using the above code the test 'should redirect to a secure page' is failing. This test is trying to verify that after a successful login, the "secure content" component is displayed. This component is a wrapper component to host the pages that can only be displayed if a user is logged in.

The page redirect is always successful, and the correct page is being displayed. My suspicion is that somehow the test is trying to get the element being before actually redirecting?

I'm new to E2E so I'm not entirely sure if this is the issue.


Just in case here's the version number of the important packages:

  • Angular: 8.2.14
  • Angular CLI: 8.2.2
  • Protractor: 5.4.2
  • Jasmine: 2.8.0
Daniel Grima
  • 2,765
  • 7
  • 34
  • 58

3 Answers3

0

I would suggest you to use async/await statement structure. Your suspicion seems to correct and the issue that you are facing is because of promise mishandling.

You can try writing spec as:

it('should redirect to a secure page', async() => {
        await loginPage.navigateTo();
        await loginPage.login();

        expect(await(await loginPage.getSecurePage()).isPresent()).toBeTruthy();
    });

And also change navigate and login functions as:

async navigateTo() {
        return await browser.get(browser.baseUrl);
    }

You can add await whenever there is a browser interaction and make the function async if there is any await statement.

refer: [1]: https://hackernoon.com/should-i-use-promises-or-async-await-126ab5c98789

  • Thanks for your answer, unfortunately it still didn't work. I'll continue investigating.. to be honest I'm still not sure if async/await is needed, because in some articles I've read mention that Protractor should automatically wait for each Promise in between steps. I might be wrong of course. – Daniel Grima Feb 27 '20 at 15:48
  • When I started working with protractor, I was resolving promises using then(). Read a few articles after that and understood how await/ async works. It did made execution a little bit easy and smooth, removing dependency on manual promise resolution. Its a choice though, to use it or not. Thanks. – Aishwarya Vatsa Mar 02 '20 at 10:35
  • Yes agreed, I prefer the async/await pattern as it makes the code much more readable. I find using the `then()` with multiple nested callbacks difficult to read at times. Of course this is a personal preference both approaches result in the same thing :) – Daniel Grima Mar 02 '20 at 10:49
0

u have to use promise with operator then this solution was worked for me if u know the time of response u can use

browser.driver.sleep(1000)

but it is a bad practice

NB: if the application is very slow it is time to report performance problem.

0

In my case, apart from waiting for certain Promise objects, I also had something else causing the error. Long story short I have an interval that's responsible for showing the current time to the user. Since an interval is an asynchronous function, Protractor will assume that Angular is still running some function, and thus keeps on waiting for Angular to be "responsive".

In Angular 2+, to cater for such a scenario, the setInterval() function needs to be run outside the Angular zone.

Example

ngOnInit() {
    this.now = new Date();

    setInterval(() => {
        this.now = new Date();
    }, 1000);
}

The above code will result in Protractor giving timeouts whenever the component with the interval exists on the page.

To fix this issue, we can make use of NgZone.

Example

import { NgZone } from '@angular/core';

constructor(private ngZone: NgZone) { }

ngOnInit() {
    this.now = new Date();

    // running setInterval() function outside angular
    // this sets the asynchronous call to not run within the angular zone
    this.ngZone.runOutsideAngular(() => {
        setInterval(() => {
            // update the date within angular, so that change detection is triggered
            this.ngZone.runTask(() => {
                this.now = new Date();
            });
        }, 1000);
    });
}

References


In my tests I've also added a utility function which helps with waiting for an element to be shown on screen.

import { ElementFinder, ExpectedConditions, browser } from 'protractor';

const until = ExpectedConditions;

export const waitForElement = (elementToFind: ElementFinder, timeout = 10000) => {
    return browser.wait(until.presenceOf(elementToFind), timeout, 'Element not found');
};

Which is then used in the tests like so:

it('should redirect to a secure page', async () => {
    loginPage.navigateTo();
    loginPage.login();

    const secureContent = loginPage.getSecureContentComponent();
    waitForElement(secureContent);

    expect(secureContent.isPresent()).toBe(true);
});

References

Daniel Grima
  • 2,765
  • 7
  • 34
  • 58