A page object is a small class that represents one screen of the app. It holds the locators for that screen and a few methods that act on it. Your tests then talk to the page object instead of poking at raw selectors. This is the single biggest readability and maintainability win in the whole framework.
The problem with inline selectors
Without page objects, every test repeats the same selectors and steps. Here is what a login test looks like with the selectors inlined:
test('user can login', async ({ page }) => {
await page.goto('/')
await page.getByTestId('username').fill('standard_user')
await page.getByTestId('password').fill('secret_sauce')
await page.getByTestId('login-button').click()
})Copy that into ten tests and you have those three selectors written ten times.
What goes wrong at scale
That duplication hurts in three ways:
- If a locator changes, you edit it in every test that used it.
- The test reads like a list of clicks, not like a description of behavior.
- Logging in is mixed into tests that are really about products or checkout.
The fix is to move the locators and the steps into one place.
The same flow with a page object
A page object pulls the locators and steps out of the test:
export class LoginPage {
readonly username: Locator
readonly password: Locator
readonly loginButton: Locator
constructor(page: Page) {
this.username = page.getByTestId('username')
this.password = page.getByTestId('password')
this.loginButton = page.getByTestId('login-button')
}
async login(user: string, pass: string) {
await this.username.fill(user)
await this.password.fill(pass)
await this.loginButton.click()
}
}The test gets shorter and clearer
Now the test says what it does, not how:
test('standard_user logs in successfully', async () => {
await login.login(USERS.standard, PASSWORD)
await login.expectLoggedIn()
})One line to log in. Anyone can read it. The selectors live in LoginPage, so the test stays focused on behavior.
Maintainability: change in one place
When the login button's data-test value changes, you update a single line in LoginPage:
this.loginButton = page.getByTestId('signin-button')Every test that logs in keeps working. No find-and-replace across the suite. That is the maintainability payoff.
Reusability across suites
The same LoginPage is used by the login suite, the products suite, the cart suite, and the checkout suite. They all start by logging in, and they all do it through the one page object. Write the flow once, reuse it everywhere.
The pattern this framework follows
This framework has one page object per screen: LoginPage, InventoryPage, ProductDetailPage, CartPage, and CheckoutPage. Each one owns its locators in the constructor and exposes a handful of action methods. Tests import them and read like plain English. The next two lessons walk through the real classes.