angularprotractorangular-e2e

Angular Protractor - Verifying page redirect


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

browser.waitForAngular();

Reference

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:


Solution

  • 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