Using Protractor's Element Object Model to Keep Angular E2E Tests Maintainable
Learn how to apply Protractor’s Element Object Model to keep Angular end‑to‑end tests readable and maintainable, with a concrete login‑page example and practical verification steps.
21 May 2026, 18:11 UTC

The problem: brittle selectors scattered across spec files
When you first write end‑to‑end tests for an Angular app with Protractor, it’s tempting to locate elements directly in each it block using CSS or XPath. A login test might look like this:
it('should log in with valid credentials', function () {
browser.get('http://example.com/login');
element(by.css('input[name="username"]')).sendKeys('alice');
element(by.css('input[name="password"]')).sendKeys('secret');
element(by.buttonText('Login')).click();
expect(element(by.binding('user.name')).getText()).toEqual('Alice');
});
If the markup changes—say the username field gets a new id or the button text is updated—you must hunt down every spec that touches those elements. Over a growing test suite this becomes a maintenance burden.
Thesis: encapsulate selectors in an Element Object Model (EOM) class
The Element Object Model pattern moves all UI‑specific locating logic into a small set of classes, one per page or component. Test specs then call methods on those classes, keeping the test intent clear and isolating UI changes to a single place.
Building a simple EOM class
Consider a login page. Create a file login.page.ts (or .js) that exports a class with methods for each interaction:
export class LoginPage {
// Locators are kept private to the class
private usernameInput = element(by.css('input[name="username"]'));
private passwordInput = element(by.css('input[name="password"]'));
private loginButton = element(by.buttonText('Login'));
private greeting = element(by.binding('user.name'));
navigate() {
return browser.get('http://example.com/login');
}
setUsername(name: string) {
return this.usernameInput.sendKeys(name);
}
setPassword(pwd: string) {
return this.passwordInput.sendKeys(pwd);
}
submit() {
return this.loginButton.click();
}
getGreetingText() {
return this.greeting.getText();
}
}
All selectors live inside LoginPage. If the username field changes to input#user, you edit only this file.
Using the EOM class in a spec
The spec now reads like a narrative:
const { LoginPage } = require('./login.page');
describe('Login feature', function () {
const login = new LoginPage();
beforeEach(function () {
login.navigate();
});
it('should greet the user after valid login', function () {
login.setUsername('alice');
login.setPassword('secret');
login.submit();
// Protractor’s built‑in synchronization waits for Angular to settle
expect(login.getGreetingText()).toEqual('Alice');
});
});
When you run protractor conf.js, Protractor will automatically wait for Angular’s $http and $timeout tasks to finish before checking the greeting, thanks to its built‑in synchronization.
Trade‑off and limitation
While EOM improves maintainability, it can introduce indirection. If you create a separate class for every tiny widget, you may end up with a deep hierarchy that mirrors the DOM more than the test intent. The key discipline is to group locators by logical screen or reusable component, not by individual HTML tags.
Another practical limitation concerns Protractor’s deprecation. The Angular team officially marked Protractor as deprecated; new projects should evaluate alternatives like Playwright or Cypress. If you are maintaining an existing Protractor suite, keep an eye on the compatibility matrix with your Angular version, as future Angular releases may drop the zone‑based synchronization Protractor relies on.
Actionable closing
To verify that your EOM implementation is working:
- Run the spec suite with
protractor conf.jsand confirm that tests pass without anybrowser.sleepcalls. - Introduce a deliberate UI change (e.g., rename the username field) and observe that only the
LoginPageclass needs editing. - Add a slow backend endpoint that delays the login response by a few seconds; the test should still pass because Protractor’s synchronization waits for Angular to stabilize.
If the test fails after the UI change but passes after you update the EOM class, you have confirmed the pattern’s benefit: UI edits are localized, and test readability remains high.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.