Page Object Model is JavaScript classes applied to browser automation: a LoginPage class has a constructor that stores locators, methods that wrap interactions, and inherits common navigation from a BasePage via extends. The most common mistake when learning this pattern is forgetting super() as the first line of a child constructor, which prevents the parent's locators from being set up. This guide covers constructors, this, extends, super, and static utility methods, building to a complete page object example.
What Is a Class?
A class is a blueprint for creating objects. Every LoginPage you create from the class gets the same methods but operates independently.
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
}
async navigate() {
await this.page.goto('/login');
}
async login(email, password) {
await this.navigate();
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}Then you use it like this:
const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'ValidPass1');The constructor
The constructor runs when you create a new instance with new. It's where you initialize properties.
class ProductPage {
constructor(page) {
// 'this' refers to the instance being created
this.page = page;
this.productTitle = page.getByTestId('product-title');
this.addToCartButton = page.getByTestId('add-to-cart');
this.price = page.getByTestId('product-price');
this.baseURL = '/products';
}
}
// When you call new ProductPage(page):
// - constructor runs immediately
// - 'this.page' is set to the page argument
// - locators are stored on the instance
const productPage = new ProductPage(page);this in Classes
this refers to the current instance. It's how methods access the instance's data.
class DashboardPage {
constructor(page) {
this.page = page;
this.welcomeMessage = page.getByTestId('welcome');
this.userName = page.getByTestId('user-name');
this.logoutButton = page.getByTestId('logout');
}
async getUserName() {
// 'this.userName' is the locator stored in the constructor
return await this.userName.textContent();
}
async logout() {
// 'this.logoutButton' is the button locator
await this.logoutButton.click();
}
async verifyUser(expectedName) {
// Methods can call other methods on 'this'
const name = await this.getUserName();
if (name !== expectedName) {
throw new Error(`Expected ${expectedName}, got ${name}`);
}
}
}Methods
Methods are functions defined on a class. They can be synchronous or async.
class SearchPage {
constructor(page) {
this.page = page;
this.searchInput = page.getByTestId('search-input');
this.searchButton = page.getByTestId('search-button');
this.results = page.getByTestId('search-result');
this.noResultsMessage = page.getByTestId('no-results');
}
// Simple async method
async search(query) {
await this.searchInput.fill(query);
await this.searchButton.click();
}
// Method that returns a value
async getResultCount() {
return await this.results.count();
}
// Method with multiple steps
async searchAndVerify(query, expectedCount) {
await this.search(query);
await this.page.waitForLoadState('networkidle');
const count = await this.getResultCount();
return count === expectedCount;
}
}Inheritance with extends
Inheritance lets one class build on another. In POM, you often have a BasePage that all pages extend.
// Base class with common behavior
class BasePage {
constructor(page) {
this.page = page;
this.header = page.getByTestId('header');
this.footer = page.getByTestId('footer');
}
async navigate(path) {
await this.page.goto(path);
}
async getTitle() {
return await this.page.title();
}
async takeScreenshot(name) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
}
// Child class inherits everything from BasePage
class LoginPage extends BasePage {
constructor(page) {
super(page); // Must call super() first — runs BasePage's constructor
this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.submitButton = page.getByTestId('submit-btn');
this.errorMessage = page.getByTestId('error-message');
}
async login(email, password) {
await this.navigate('/login'); // Inherited from BasePage
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorText() {
return await this.errorMessage.textContent();
}
}Now LoginPage has both its own methods AND everything from BasePage:
const loginPage = new LoginPage(page);
await loginPage.login('user@test.com', 'pass'); // LoginPage method
await loginPage.takeScreenshot('after-login'); // BasePage method
const title = await loginPage.getTitle(); // BasePage methodsuper — Calling the Parent
super() calls the parent class's constructor. Required as the first line in a child constructor.
class AdminPage extends BasePage {
constructor(page) {
super(page); // Runs BasePage constructor: sets this.page, this.header, this.footer
// Now add AdminPage-specific things
this.userTable = page.getByTestId('users-table');
this.addUserButton = page.getByTestId('add-user');
}
}You can also call parent methods with super.methodName():
class CheckoutPage extends BasePage {
async navigate() {
// Call the parent navigate, then do extra stuff
await super.navigate('/checkout');
await this.page.waitForSelector('[data-testid="checkout-form"]');
}
}Static Methods
Static methods belong to the class itself, not instances. Useful for utility functions.
class TestData {
static generateEmail() {
return `user_${Date.now()}@test.com`;
}
static generateUser() {
return {
email: TestData.generateEmail(),
password: 'ValidPass1',
name: 'Test User',
};
}
static generateProduct(overrides = {}) {
return {
name: 'Test Product',
price: 99.99,
category: 'electronics',
...overrides,
};
}
}
// Use without 'new' — call directly on the class
const email = TestData.generateEmail();
const user = TestData.generateUser();
const product = TestData.generateProduct({ price: 49.99 });Getters
Getters look like properties but run a function when accessed:
class CartPage extends BasePage {
constructor(page) {
super(page);
this.items = page.getByTestId('cart-item');
this.totalElement = page.getByTestId('cart-total');
}
get url() {
return '/cart'; // Computed property
}
async navigate() {
await super.navigate(this.url); // Uses the getter
}
async getItemCount() {
return await this.items.count();
}
}
const cartPage = new CartPage(page);
console.log(cartPage.url); // '/cart' — no () neededA Complete Page Object Example
Putting it all together — a realistic page object:
class ProductListPage extends BasePage {
constructor(page) {
super(page);
this.productCards = page.getByTestId('product-card');
this.filterDropdown = page.getByTestId('category-filter');
this.sortDropdown = page.getByTestId('sort-select');
this.searchInput = page.getByTestId('product-search');
this.loadingSpinner = page.getByTestId('loading');
}
async navigate() {
await super.navigate('/products');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async filterByCategory(category) {
await this.filterDropdown.selectOption(category);
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async sortBy(sortOption) {
await this.sortDropdown.selectOption(sortOption);
}
async search(query) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async getProductCount() {
return await this.productCards.count();
}
async getProductNames() {
return await this.productCards.getByTestId('product-name').allTextContents();
}
async clickProduct(index) {
await this.productCards.nth(index).click();
}
}// In tests
test('filter products by category', async ({ page }) => {
const products = new ProductListPage(page);
await products.navigate();
await products.filterByCategory('electronics');
const count = await products.getProductCount();
expect(count).toBeGreaterThan(0);
});Class vs Object Literal
For one-off things, an object literal is simpler. For something you reuse across tests, a class makes sense.
// Object literal — fine for constants
const TEST_USERS = {
admin: { email: 'admin@test.com', password: 'AdminPass1' },
member: { email: 'member@test.com', password: 'MemberPass1' },
};
// Class — good for page objects used in many tests
class LoginPage extends BasePage {
// ...
}Summary
constructor(page)— runs onnew, store locators herethis— refers to the instance; how methods access the object's dataextends— inherit from another class (e.g.,BasePage)super()— call parent constructor; required first line in child constructorstatic— class-level methods, no instance needed- Methods can be async — return promises, use
awaitinside
Classes in Playwright Page Objects aren't magic. They're just JavaScript's way of bundling related locators and methods into one reusable unit. Once you understand constructor, this, and extends, the rest follows naturally.