When 10 tests all interact with the orders page, any selector change breaks all 10. A page object wraps those locators in one class — fix the selector once, tests are fixed. That's the whole point.
When you need page objects
Multiple test files share one page object — change a selector once and all tests are fixed
Page objects make sense when the same UI is accessed by multiple test files. If only one test touches the orders page, an inline helper is fine. When 5+ tests all do page.getByRole('button', { name: 'Create order' }) — that's the signal to extract an OrdersPage class.
The key rule: page objects contain locators and actions, not assertions. Assertions belong in the test — that's where the intent is documented. If a page object method throws or returns a boolean based on state, you've gone too far.
Creating a page object
A page object is a TypeScript class that takes page in the constructor, defines locators as properties, and exposes methods for common interactions. Locators are defined once in the constructor — they're lazy by default so they don't cause issues until used.
Tests become readable English: "go to orders, filter by pending, check count". The locator details are in the page object. The intent is in the test.
ts
import { test, expect } from'@playwright/test'import { OrdersPage } from'../pages/orders-page'test('filter shows only pending orders', async ({ page }) => {
const orders = new OrdersPage(page)
await orders.goto()
await orders.filterByStatus('pending')
// Перевірка — у тесті, не в page objectawaitexpect(orders.orderList.getByRole('row').filter({
hasNot: orders.page.getByRole('cell', { name: 'Pending' })
})).toHaveCount(0)
})
test('cancel order removes it from list', async ({ page }) => {
const orders = new OrdersPage(page)
await orders.goto()
await orders.cancelOrder('ORDER-042')
// getOrderRow повертає локатор — expect у тестіawaitexpect(orders.getOrderRow('ORDER-042')).not.toBeVisible()
})
test('create order appears in list', async ({ page }) => {
const orders = new OrdersPage(page)
await orders.goto()
await orders.createOrder('Laptop Stand', 2)
awaitexpect(orders.orderList).toContainText('Laptop Stand')
})
Combine with fixtures
The cleanest pattern: page objects for locator/action abstraction, fixtures for setup/teardown. The fixture creates the page object, navigates, seeds data — and the test gets a ready-to-use page object.
ts
// fixtures/index.tsimport { test as base } from'@playwright/test'import { OrdersPage } from'../pages/orders-page'exportconsttest = base.extend<{ ordersPage: OrdersPage }>({
ordersPage: async ({ page }, use) => {
const orders = new OrdersPage(page)
await orders.goto()
await use(orders)
},
})
// tests/orders.spec.tsimport { test, expect } from'../fixtures'// Тест отримує вже готову сторінку — без goto() всерединіtest('orders page loads', async ({ ordersPage }) => {
awaitexpect(ordersPage.orderList).toBeVisible()
})