Implementing the Page Object Model in Protractor for Maintainable UI Tests
Learn how to implement the Page Object Model (POM) in Protractor to decouple UI locators from test logic, reducing maintenance and preventing brittle test suites.
03 Jul 2026, 22:43 UTC

The Problem: Brittle Selectors and Redundant Test Logic
When writing end-to-end tests in Protractor, developers often embed element locators (like by.css or by.id) directly within the test specifications. This creates a maintenance burden: if a developer changes a single ID or class name in the HTML, every test file referencing that element fails, requiring a manual search-and-replace across the entire codebase.
The Page Object Model (POM) solves this by decoupling the test logic from the UI structure. Instead of the test knowing how to find a button, it asks a Page Object to perform an action. The takeaway is simple: encapsulate locators and interactions in dedicated classes so that UI changes only require updates in one file, not across dozens of test specs.
Mechanism: Encapsulating UI Logic
In a POM architecture, each page of your application is represented by a JavaScript or TypeScript class. This class serves two purposes: it defines the elements on that page and provides public methods that simulate user behavior.
Worked Example: Login Flow
Below is a configuration for a login page and its corresponding test spec. This example assumes you are using Protractor with the Jasmine framework.
1. Define the Page Object (login.page.js)
const { browser } = require('protractor');
class LoginPage {
constructor() {
// Define locators as properties to centralize UI references
this.usernameField = element(by.id('user-name'));
this.passwordField = element(by.id('password'));
this.loginButton = element(by.css('.btn-submit'));
}
async login(user, pass) {
await this.usernameField.sendKeys(user);
await this.passwordField.sendKeys(pass);
await this.loginButton.click();
// Return the next page object to allow for method chaining
return new DashboardPage();
}
}
module.exports = new LoginPage();
2. Implement the Test Spec (login.spec.js)
const loginPage = require('./pages/login.page');
describe('Login Functionality', () => {
it('should navigate to dashboard on valid credentials', async () => {
await browser.get('https://example.com/login');
// The test spec focuses on the 'what', not the 'how'
const dashboard = await loginPage.login('standard_user', 'secret_sauce');
// Assertions stay in the spec file, not the page object
expect(await dashboard.header.getText()).toEqual('Welcome to Dashboard');
});
});
Critical Engineering Decisions
Separation of Concerns: Assertions vs. Actions
A common mistake is placing expect() statements inside the Page Object methods. Page Objects should be service providers; they provide the state of the UI or perform actions. They should not decide if a test passes or fails. Keeping assertions in the spec file ensures that your Page Objects remain reusable across different test scenarios (e.g., a login() method might be used for both a successful login test and a "wrong password" error test).
Avoiding the "God Object"
As applications grow, there is a temptation to create a single AppPage class that contains every locator in the system. This leads to a "God Object" that is difficult to navigate and maintain. Instead, split objects by logical components:
- Page-level objects: For unique pages (e.g.,
SettingsPage). - Component-level objects: For shared UI elements like a
NavigationMenuorFooterthat appear on multiple pages.
Limitations and Risks
While POM improves maintainability, it introduces a layer of abstraction that can hide performance issues. Because you are wrapping Protractor's element() calls, it is easy to forget that every interaction is an asynchronous network request to the WebDriver.
Warning on Tooling: Protractor is currently deprecated. While POM is a universal pattern applicable to modern tools, new projects should evaluate Playwright or Cypress, which offer more robust built-in waiting mechanisms than Protractor's browser.wait().
Verification Checklist
To verify your POM implementation is working correctly, perform these checks:
- Locator Isolation: Change a CSS selector in the Page Object class. Run all associated tests. If you have to touch any
.spec.jsfile to fix the failure, your abstraction is leaking. - State Validation: Ensure that Page Object methods return the next expected Page Object. This allows you to chain actions (e.g.,
loginPage.login().goToProfile().updateEmail()). - Execution Check: Run the tests using the Protractor CLI:
protractor conf.js. Ensure thatbeforeEachblocks are used to instantiate fresh page objects to prevent state bleed between tests.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.