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();
browser.driver.wait(() => {
browser.driver.getCurrentUrl().then((url) => {
return /expected-url/.test(url);
});
}, 10000);
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:
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
zone.js
and its role in Angular)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