Reducing Test Maintenance in Protractor with the Page Object Model
Learn how to implement the Page Object Model (POM) in Protractor to decouple test logic from UI selectors, reducing maintenance overhead in Angular E2E suites.
19 Apr 2026, 00:02 UTC

The Problem: Brittle Selectors in E2E Tests
End-to-end (E2E) tests often fail not because the application is broken, but because the UI changed. When you hardcode selectors like by.css('.btn-primary') across twenty different test files, a single CSS class change requires twenty manual updates. This creates a maintenance bottleneck that slows down deployment cycles.
The solution is the Page Object Model (POM). POM decouples your test logic (the \"what\") from the UI structure (the \"how\"). By encapsulating selectors and interactions within dedicated classes, you create a single source of truth for every page in your application.
Implementing a Page Object Class
In Protractor, a Page Object is a standard JavaScript class. It should contain two primary components: element finders (properties) and interaction methods (behaviors). This ensures that the test script reads like a user story rather than a list of DOM queries.
Below is a configuration for a login page. This example assumes you are using Protractor with the Jasmine framework.
// login.page.js
class LoginPage {
constructor() {
// Element finders are defined as properties
this.usernameField = element(by.id('user-name'));
this.passwordField = element(by.id('password'));
this.loginButton = element(by.id('login-button'));
this.errorMessage = element(by.css('.error-text'));
}
async login(username, password) {
await this.usernameField.sendKeys(username);
await this.passwordField.sendKeys(password);
await this.loginButton.click();
}
async getErrorText() {
return await this.errorMessage.getText();
}
}
module.exports = new LoginPage();
Integrating the Page Object into Tests
To use the Page Object, import the instance into your spec file. This keeps your it blocks clean and focused on assertions rather than selector logic.
// login.spec.js
const loginPage = require('./login.page');
describe('Login Flow', () => {
it('should show an error with invalid credentials', async () => {
await browser.get('https://example.com/login');
// The test calls a behavior, not a selector
await loginPage.login('wrong_user', 'wrong_pass');
const error = await loginPage.getErrorText();
expect(error).toContain('Invalid credentials');
});
});
Execution Requirements
- Environment: Node.js environment with
protractorandwebdriverjsinstalled. - Permissions: The user running the tests must have permissions to execute the WebDriver binary (e.g., chromedriver) and access the target URL.
- Placeholders: Replace
'https://example.com/login'with your application's actual environment URL.
Limitations and Common Pitfalls
The Deprecation Warning
It is critical to note that Protractor is deprecated. While POM is a universal design pattern applicable to any framework, new projects should prioritize tools like Playwright or Cypress, which offer better synchronization and faster execution than the WebDriver-based architecture used by Protractor.
Deep Inheritance Chains
A common mistake is creating complex inheritance hierarchies (e.g., BasePage < HeaderPage < UserProfilePage). While this seems DRY (Don't Repeat Yourself), it makes debugging difficult because the actual selector might be defined three levels up in a parent class. Prefer composition—instantiating a HeaderPage object inside other page objects—over deep inheritance.
Synchronization Flakiness
Protractor attempts to synchronize with Angular, but this fails for non-Angular elements or complex async operations. Avoid putting browser.sleep() inside your Page Objects. Instead, use ExpectedConditions (EC) to wait for a specific state.
// Better approach for stability
const EC = protractor.ExpectedConditions;
async waitForLoginButton() {
await browser.wait(EC.elementToBeClickable(this.loginButton), 5000);
}
Verifying the Implementation
To verify that your POM is functioning correctly and providing maintenance value, perform the following test:
- Run your test suite and confirm it passes.
- Change the
idof the username field in your application's HTML (e.g., changeuser-nametousername_input). - Run the tests again; they should fail.
- Update the selector only in the
LoginPageclass. - Run the tests again. If they pass without any changes to the
.spec.jsfile, the decoupling is successful.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.