Why POM · Page class anatomy · Selectors as constants · Assertions in methods · beforeAll vs beforeEach
→ next slide | ESC overview
1. The problem — why flat tests break down
2. POM concept — one class per page
3. Anatomy of a page class
4. Flat vs POM — live refactor of cart test
5. beforeAll vs beforeEach — when to use each
pages/, move all selectors there, and make the test file selector-free. Open a PR.The same selectors appear in every file. One site change breaks everything.
await page.locator('.cartbtn-event.forward').click();
await expect(page.locator('.item-messagebox'))
.toContainText('Toode lisati');
await page.locator('.cartbtn-event.forward').click();
await expect(page.locator('.item-messagebox'))
.toContainText('Toode lisati');
.cartbtn-event → .cart-btn. You hunt through every spec file to find all copies — 10 files, 10 chances to miss one.
.cartbtn-event.forward tells you nothing.home.openShoppingCart() is self-explanatory.
POM fixes both: selectors in one place, test steps read like user stories.
Each page gets its own class. The class owns all selectors and actions — the test file stays selector-free.
await home.search('harry potter');
await home.addToCart(0);
cart = await home.openShoppingCart();
await cart.verifyCartQuantity(2);
const cartForwardBtn = '.cartbtn-event.forward';
async openShoppingCart() {
await this.goToCartBtn.click();
return new CartPage(this.page);
}
HomePage — navigation, search, add to cartCartPage — quantities, totals, removeProductPage — product details, add to cart
.locator( or .getByRole( in a spec.ts file, it belongs in a page class instead.
import { Page, Locator, expect } from '@playwright/test';
import { CartPage } from './CartPage';
// ① Selectors — module-level constants
const logoSelector = '.logo-icon';
const cartForwardBtn = '.cartbtn-event.forward';
export class HomePage {
private logo: Locator;
private goToCartBtn: Locator;
constructor(private page: Page) {
this.logo = page.locator(logoSelector); // ② lazy locator
this.goToCartBtn = page.locator(cartForwardBtn);
}
async openUrl() { // ③ action method
await this.page.goto('https://www.kriso.ee/');
}
async verifyLogo() { // ④ verify — assertion inside
await expect(this.logo).toBeVisible();
}
async openShoppingCart(): Promise<CartPage> { // ⑤ factory
await this.goToCartBtn.click();
return new CartPage(this.page);
}
}
.click(), .fill(), etc.
await home.verifyLogo()
openShoppingCart() navigates and returns a new CartPage.
test('Test add book to cart', async () => {
await page.getByRole('link', {
name: 'Lisa ostukorvi'
}).first().click();
await expect(
page.locator('.item-messagebox')
).toContainText('Toode lisati ostukorvi');
await expect(
page.locator('.cart-products')
).toContainText('1');
});
test('Test verify cart has two items', async () => {
await page.locator('.cartbtn-event.forward').click();
await expect(
page.locator('.order-qty > .o-value')
).toContainText('2');
});
test('Test add book to cart', async () => {
await home.addToCart(0);
await home.verifyItemAddedToCart();
await home.verifyCartCount('1');
});
test('Test verify cart has two items', async () => {
cart = await home.openShoppingCart();
await cart.verifyCartQuantity(2);
});
// CartPage.ts
async verifyCartQuantity(number: number) {
await expect(this.cartQuantityEl)
.toContainText(number.toString());
}
// Asserts rows sum == displayed total,
// then returns the sum for cross-test comparison.
async verifyCartSumIsCorrect(): Promise<number> {
const items = await this.rowSubtotals.all();
let sum = 0;
for (const item of items) {
sum += parseFloat(
(await item.textContent() ?? '')
.replace(/€/g, '').replace(',', '.')
);
}
const total = parseFloat(
(await this.orderTotal.textContent() ?? '')
.replace(/€/g, '').replace(',', '.')
);
expect(total).toBeCloseTo(sum, 2);
return sum;
}
test('Test verify cart has two items', async () => {
cart = await home.openShoppingCart();
await cart.verifyCartQuantity(2);
});
test('Test verify cart sum of two', async () => {
cartSumOfTwo = await cart.verifyCartSumIsCorrect();
});
test('Test verify cart sum of one', async () => {
cartSumOfOne = await cart.verifyCartSumIsCorrect();
expect(cartSumOfOne).toBeLessThan(cartSumOfTwo);
});
cartSumOfTwo is stored so the last test can prove the total actually dropped — not just that the new total is internally consistent.
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
page = await context.newPage();
home = new HomePage(page);
await home.openUrl();
await home.acceptCookies();
});
test.describe.configure({ mode: 'serial' }) to guarantee order.
test.beforeEach(async ({ page }) => {
await page.goto('https://www.kriso.ee/');
await page.fill(searchInput, 'harry potter');
await page.click(searchButton);
});
test('shows multiple results', async ({ page }) => { ... });
test('has add-to-cart buttons', async ({ page }) => { ... });
beforeAll= shared, sequential, stateful ·beforeEach= isolated, parallel, clean
Page objects and values shared across tests are declared at module scope — outside the describe block.
test.describe.configure({ mode: 'serial' });
// ── Module scope — all tests share these ──
let page: Page;
let home: HomePage;
let cart: CartPage; // set by home.openShoppingCart()
let cartSumOfTwo = 0; // written in test 7, read in test 9
let cartSumOfOne = 0;
test.describe('Add Books to Shopping Cart (POM)', () => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
page = await context.newPage();
home = new HomePage(page);
await home.openUrl();
await home.acceptCookies();
});
test.afterAll(async () => {
await page.context().close();
});
// tests read and write home, cart, cartSumOfTwo, cartSumOfOne
});
beforeAll runs once — the page object must live somewhere all tests can reach it. Module scope is the standard place.
cart doesn't exist at setup time. Test 6 navigates to the cart and assigns it: cart = await home.openShoppingCart(). Tests 7–9 then use it.
openABC()
Navigation or opening something.openUrl()openBookPage(1)openShoppingCart()
verifyABC()
Contains an assertion. Test calls it and trusts it.verifyLogo()verifyCartQuantity(2)verifyCartSumIsCorrect()
addABC() / removeABC()
Mutations — add or remove something.addToCart(0)removeItemFromCart(0)
acceptABC()
Dismissing dialogs or banners.acceptCookies()acceptConsent()
continueABC()
Going back or resuming a flow.continueShopping()
search(keyword)
Short, verb-only names for simple actions.search('harry potter')search('tolkien')
Method names describe user intent, not implementation.openShoppingCart()notclickForwardButton().
Refactor your flat cart tests from Task 1 into the POM pattern.
pages/HomePage.ts — selectors + methods for home page and search resultspages/CartPage.ts — selectors + verify methods for cart pagetests/pom/cart.pom.spec.ts.locator( or .getByRole( in the test filebeforeAll + test.describe.configure({ mode: 'serial' })Push your changes to your fork and open a PR.