If your test automation project has grown past a handful of tests and you find yourself copy-pasting the same login steps, the same navigation clicks, the same form fills across dozens of spec files, you have a problem. Your tests are fragile. One small UI change breaks everything.
So what is Page Object Model in test automation? It is a design pattern that solves exactly this mess. You organize your test code into reusable classes, stop repeating yourself, and write tests that actually scale.
Let's dive in.
What Is Page Object Model?
Page Object Model is a design pattern used in test automation to organize your source code. The goal is to improve maintainability and reusability of your tests.
The core idea is not complicated. You take repetitive parts of your test code, put them into methods inside a class, and then call that method instead of writing the same steps over and over again. That's it. No magic.
Why does this matter? You write the steps once and call them many times. No more copy-paste across test files. When something changes in the UI, you update it in one place instead of hunting through every test.
There is also one fact: there is no single industry-standard for implementing page objects. None. There are hundreds of flavors out there.
Some teams keep locators in the constructor. Some store them as class constants. Some put them in JSON files. I've even seen teams use CSV files for locator management. Some call them page objects, others page functions, and some skip the concept entirely and use "app actions."
The general concept goes like this: every page of your web application gets its own class, and that class contains methods responsible for the operations you perform on that page. A LoginPage class handles login. A DashboardPage class handles dashboard interactions. A SettingsPage class handles settings.
From this concept, you can go sideways in a hundred different directions, deciding how to build your architecture. There are no absolute rights or wrongs. It comes down to preference, team agreement, and what makes sense for your project.
And that flexibility is a feature, not a bug. You can start simple and evolve as your project grows. You don't need to get it perfect on day one.
Why Do You Need Page Objects?
Let me show you a real example. Say you have two tests that both require logging in before doing anything else.
Without page objects, your tests look like this:
// Test 1await page.locator('#email').fill('[email protected]')await page.locator('#password').fill('password123')await page.locator('button:has-text("Login")').click()// ... rest of test 1// Test 2await page.locator('#email').fill('[email protected]')await page.locator('#password').fill('password123')await page.locator('button:has-text("Login")').click()// ... rest of test 2
Three lines. Repeated in every single test that requires a logged-in user. Two tests, that's six lines. Ten tests, thirty lines. Fifty tests? You get the picture.
Now imagine the dev team renames the login button from "Login" to "Sign In." You have to go through every test file, find every instance of that button text, and update it. Miss one? That test fails.
The tests are also harder to read. You open a test file, see a wall of locators and actions, and have to mentally parse what each block of code is doing. Is this the login? Is this the form submission? You spend brain power on figuring out what the code does rather than focusing on the actual test logic.
How?
You create a page object class that owns the login behavior:
class LoginPage { readonly page: Page constructor(page: Page) { this.page = page } async performLogin(email: string, password: string) { await this.page.locator('#email').fill(email) await this.page.locator('#password').fill(password) await this.page.locator('button:has-text("Login")').click() }}
Now your tests become clean:
// Test 1const loginPage = new LoginPage(page)await loginPage.performLogin('[email protected]', 'password123')// ... rest of test 1// Test 2const loginPage = new LoginPage(page)await loginPage.performLogin('[email protected]', 'password123')// ... rest of test 2
Three lines reduced to one. Look how descriptive it is. "On the login page, perform login." Anyone reading this test immediately understands what's happening.
If the button changes from "Login" to "Sign In," you update it in one place: the LoginPage class. Every test that uses performLogin() automatically picks up the change. Zero test file edits.
When you're building out your page objects and choosing locator strategies, make sure you follow best practices there too. Good locators inside good page objects give you a solid framework.
Two Principles Every Page Object Must Follow
When designing page objects, two principles matter more than anything else. Get these right, and your framework will last. Ignore them, and you'll rewrite everything six months from now.
DRY: Don't Repeat Yourself
If you're copy-pasting the same code more than three times, stop. Create a reusable method.
This is the entire reason page objects exist. The moment you catch yourself pressing Ctrl+C and Ctrl+V on test steps, that's your signal to extract those steps into a page object method.
If you have a login flow that appears in 30 tests and the login form changes, would you rather update 30 files or one file? The answer is obvious.
DRY is the easy principle. Most engineers get it naturally. The next one is harder.
KISS: Keep It Simple Stupid
This is the more important principle, and honestly, the one that most people get wrong.
It is very easy to overcomplicate framework design. Much harder to keep your code simple and clear. I've seen it happen dozens of times. A new engineer learns about design patterns, gets super excited, and starts overbuilding everything. Extra methods for every tiny interaction. Helper classes wrapping helper classes. Utility files for things that don't need utilities. Abstract base page classes with three levels of inheritance.
The framework looks cool. It looks "professional." But it's a nightmare to actually use.
So what's the real test of your framework? What happens when you hand it to a new engineer who just joined the team?
If they can look at your page objects, understand how they work, and start writing tests on day one, you did a great job. They'll keep using the framework and keep developing and updating it.
But if it's too complex? Too abstracted? Too clever? They'll remove it and start from scratch. I've seen this happen. And if this happened, it means your test design failed.
Trust me, keeping the code simple and easy to use is not a bad thing. Simple code is easier to maintain. Simple code is easier to onboard new engineers with. Keep your framework as simple as possible.
Page Object Best Practices
So, in a nutshell, what do we have so far? Page objects put repetitive steps into methods, DRY says don't copy-paste, KISS says don't overbuild. Now let's talk about the practical stuff.
Use Descriptive Naming
Naming in programming, believe it or not, is the hardest thing to do. Getting the right name for a method, a class, or a variable takes more time than most people expect.
Avoid shortcuts. Avoid acronyms. Avoid abbreviations. Your future self and your teammates will thank you.
Instead of clickLogin(), name it clickLoginButton(). It says exactly what it does. Instead of fillForm(), name it fillRegistrationForm(). Instead of nav(), name it navigateToHomePage().
I like this trick. Create a method name, tap your colleague on the shoulder. Ask them: "Does this method name make sense to you? Can you suggest a more meaningful name?" Show them the test scenario and ask if they can understand what the test is about just from the method names alone.
If your colleague reads loginPage.performLogin() followed by dashboardPage.createNewProject() followed by projectPage.verifyProjectTitle(), they should understand the entire test flow without looking at a single line of implementation code.
Is it readable? Yes! Can a new team member understand what the test does? Yes! Yes, that simple :)
Avoid Tiny Single-Line Methods
This one is controversial, but I stand by it.
Methods like clickLoginButton() that contain just a single line of code:
async clickLoginButton() { await this.page.locator('#login').click()}
One line. That's all the method does.
I'm not saying never create methods like this. Sometimes you need them. But try to avoid it when you can.
Why?
Because if every single interaction on a page gets its own method, your page object class ends up with 50 tiny methods. You quickly get lost scrolling through them trying to find the one you need.
Instead, group several meaningful test steps into methods that represent a functional operation on the page. Don't think "what buttons can I click?" Think "what actions can a user perform?"
A user doesn't "click the email field, type an email, click the password field, type a password, click the login button." A user logs in. That's one method: performLogin(). Same idea for creating a project, submitting a form, or navigating to a section. Think in user actions, not individual clicks.
If you need a single-line method for a specific scenario, fine, do it. But if you can group steps into bigger methods that do a certain functionality on your page, it's better to do it that way. Your page objects will be cleaner and easier to navigate.
This approach also works well with other design patterns. If you've worked with the Fluent Interface pattern (I cover it in my article on how to automate APIs using Playwright), you'll see how grouping meaningful operations creates more readable code.
If you're choosing between Playwright and Cypress for your project, both support the page object pattern. But Playwright's native TypeScript support and fixture system make it particularly smooth to work with.
Final Thoughts
So what is Page Object Model in test automation? It is a design pattern that keeps your tests clean and maintainable. You take repetitive code, put it into well-named methods inside page classes, and give your tests a structure that scales.
Everyone can learn the definition. The harder part is keeping your page objects simple enough that any engineer on your team can pick them up and run with them.
Build your framework for the people who will use it. Follow DRY and KISS. Use descriptive names. Group meaningful operations instead of wrapping every click in its own method. And I wish you to build the frameworks in a way that the person who comes to the project after you will not say: "we need to start from scratch" :)
Playwright has excellent built-in support for the page object pattern. Check the official Playwright POM documentation to see how they recommend structuring page objects.
Ready to Master Playwright?
Playwright is growing in popularity on the market very quickly and soon will be a mainstream framework. If you want to learn Playwright the right way, from setting up your first test to building a professional page object framework, check out my course Playwright UI Testing Mastery.
Start from scratch and become an expert to increase your value on the market!
Frequently Asked Questions
What is the Page Object Model in test automation?
Page Object Model is a design pattern that organizes test automation code by creating a class for each page of your application. Each class contains methods that represent user actions on that page. Instead of repeating the same steps across tests, you call these methods.
What are the benefits of using Page Object Model?
The main benefits are maintainability, reusability, and readability. When the UI changes, you update code in one place instead of every test. You reuse methods across tests instead of copy-pasting steps. And your tests read like descriptions of user behavior instead of walls of locators.
Is there a standard way to implement Page Object Model?
No. There is no single industry standard. Some teams store locators as class properties, some use constants, some use external files. Some use inheritance with base page classes, others keep it flat. The right approach depends on your team and project. The key is consistency within your project.
