Skip to main content
Version: 2x

Page Object Model

Introduction​

The Page Object Model (POM) is a design pattern that creates an object repository for web UI elements. qavajs provides a flexible implementation that allows you to define elements using plain English selectors, creating maintainable and readable test automation code.

Key Benefits​

  • Abstraction of UI elements - Separate test logic from element selectors
  • Reusable components - Define element once, use it throughout your tests
  • Improved maintainability - When the UI changes, update only the page object, not test cases
  • Enhanced readability - Use descriptive names for elements in your test scenarios

Getting Started with Page Objects​

Basic Setup​

First, configure the page object in your qavajs configuration file:

import App from 'page_object';

export default {
// Other configuration
pageObject: new App();
};

Creating Page Objects​

The entry point of your page object structure is the class defined in the pageObject.pages property of your config.

const { locator } = require('@qavajs/steps-playwright/po'); // Use '@qavajs/steps-wdio/po' for WebdriverIO

class App {
// Simple CSS selector
SimpleLocator = locator('.single-element');

// XPath selector
XPathElement = locator('xpath=//div[@class='example']');

// Nested component
Header = locator('.header-container').as(HeaderComponent);

// Component with multiple instances
ProductCards = locator('.product-card').as(ProductCard);
}

class HeaderComponent {
Logo = locator('.logo');
SearchBar = locator('input.search');
UserMenu = locator('.user-menu');
}

class ProductCard {
Title = locator('.product-title');
Price = locator('.product-price');
AddToCartButton = locator('button.add-to-cart');
}

module.exports = App;

Using Page Objects in Feature Files​

Once defined, page objects can be referenced in Gherkin scenarios using plain English:

Feature: Page Object Demo

Scenario: Basic element interactions
Given I open 'https://example.com' url
When I click 'Simple Locator'
And I click 'Header > Logo'
And I type 'search term' to 'Header > Search Bar'
Then I expect 'Header > User Menu' to be visible

Scenario: Working with component collections
Given I open 'https://example.com/products' url
When I click 'Product Cards > Add To Cart Button (2)'
# This clicks the Add To Cart button on the second Product Card
Then I expect text of 'Cart Items Count' to contain '1'

Advanced Locator Types​

qavajs provides several powerful ways to define and interact with elements:

Template Locators​

Template locators allow you to generate selectors dynamically based on parameters passed from your Gherkin steps.

const { locator } = require('@qavajs/steps-playwright/po');

class App {
// Select by index (1-based in Gherkin, converted to 0-based in implementation)
ElementByIndex = locator.template(index => `div.item:nth-child(${index})`);

// Select by text content
ElementByText = locator.template(text => `div:has-text('${text}')`);

// Select by attribute value
ElementByAttribute = locator.template(value => `[data-testid='${value}']`);

// Multiple parameters
TableCell = locator.template((row, col) => `table tr:nth-child(${row}) td:nth-child(${col})`);
}

Using template locators in feature files:

Feature: Template Locators

Scenario: Using different template locators
When I click 'Element By Index (3)'
And I click 'Element By Text (Add to Cart)'
And I click 'Element By Attribute (submit-button)'
And I expect text of 'Element By Text (Add to Cart)' to contain 'Add to Cart'

Native Framework Locators​

Native locators allow you to leverage the full power of your testing framework's built-in selector capabilities.

Playwright Example:​

const { locator } = require('@qavajs/steps-playwright/po');

class App {
// Using Playwright's getByRole
SubmitButton = locator.native(({ page }) => page.getByRole('button', { name: 'Submit' }));

// Using Playwright's getByText with regex
SpecialOffer = locator.native(({ page }) => page.getByText(/Special offer/i));

// Using Playwright's getByTestId
UserProfile = locator.native(({ page }) => page.getByTestId('user-profile'));

// Combining with Playwright's filters
ActiveMenuItem = locator.native(({ page }) =>
page.getByRole('menuitem').filter({ hasText: 'Active' }));

// Using the argument from feature file
DynamicElement = locator.native(({ page, argument }) =>
page.getByRole('button', { name: argument }));

// Element in iframe
ElementInIframe = locator.native(({ page }) => page.frameLocator('iframe#yourIframe').locator('#element'));
}

WebdriverIO Example:​

const { locator } = require('@qavajs/steps-wdio/po');

class App {
// Using WebdriverIO's custom selectors
SubmitButton = locator.native(({ browser }) =>
browser.$('button=Submit'));

// Using WebdriverIO's shadow DOM support
ShadowDomElement = locator.native(({ browser }) =>
browser.$('host-element').shadow$('.shadow-child'));

// Using WebdriverIO's recursive find
NestedElement = locator.native(({ browser, argument }) =>
browser.$$('div.container')[Number(argument) - 1].$('.child-element'));
}

Using native locators in feature files:

Feature: Native Locators

Scenario: Use native locators
When I click 'Submit Button'
And I expect 'Special Offer' to be visible
When I click 'Dynamic Element (Accept Terms)'

Default Resolver​

The defaultResolver provides a way to define default logic for identifying elements that aren't explicitly defined in your page object. This is useful for components with many similar elements.

const { locator } = require('@qavajs/steps-playwright/po');

class ListComponent {
// Define locators for specific elements
AddButton = locator('.add-btn');

// Default resolver for undefined elements - will find by text
defaultResolver({ alias }) {
return ({ parent }) => parent.getByText(alias);
}
}

class App {
List = locator('.list-component').as(ListComponent);
}

Using default resolver in feature files:

Feature: Default Resolver

Scenario: Using the default resolver to find elements by text
# Uses the specific defined locator
When I click 'List > Add Button'

# These use the default resolver to find elements by their text
And I click 'List > Edit Item'
And I click 'List > Delete'
And I expect 'List > No items found' to be visible

Collections and Indexed Elements​

qavajs provides powerful ways to work with collections of elements:

Working with Collections​

const { locator } = require('@qavajs/steps-playwright/po');

class App {
// Items in a list
ListItems = locator.template(index => `ul.list li:nth-child(${index})`);

// Rows in a table (with 1-based indexing)
TableRows = locator.template(index => `table tr:nth-child(${parseInt(index) + 1})`);

// Elements by text content
ElementByText = locator.template(text => `div:has-text('${text}')`);

// Get element by partial text match
ElementByPartialText = locator.template(text => `xpath=//div[contains(text(), '${text}')]`);

// Get element by multiple criteria (index and category)
CategoryItem = locator.template((category, index) =>
`div.${category}-list div.item:nth-child(${index})`);
}

Usage in feature files:

Feature: Collection Elements

Scenario: Interacting with collection elements
# Using index-based access
When I click 'List Items (3)'
And I expect text of 'Table Rows (2)' to contain 'Product Details'

# Using text-based access
When I click 'Element By Text (View Details)'

# Using partial text matching
When I click 'Element By Partial Text (Add)'

Native Collection Handling​

Using the native locator approach for more complex collection operations:

const { locator } = require('@qavajs/steps-playwright/po');

class App {
// Find nth item with specific text
ItemWithText = locator.native(({ page, argument }) => {
return page.getByText(argument);
});

// Find element by attribute value and index
DataItems = locator.native(({ page, argument }) => {
return page.locator(`[data-test='${argument}']`);
});

// Filter collection and get specific item
FilteredItems = locator.native(({ page, argument }) => {
return page.locator('li.item')
.filter({ hasText: argument })
.nth(idx);
});
}

Usage in feature files:

Feature: Advanced Collection Handling

Scenario: Working with filtered collections
When I click 'Item With Text (Product)'
And I expect 'Data Items (user-card)' to be visible
And I click 'Filtered Items (Premium)'

Using Page Objects in Custom Steps​

You can use page objects in your custom step definitions for more advanced interactions:

const { When, Then } = require('@qavajs/core');
const { memory } = require('@qavajs/memory');

// For Playwright
When('I hover over {playwrightLocator} and click {playwrightLocator}', async function(hoverElement, clickElement) {
await hoverElement.hover();
await clickElement.click();
});

Then('I store text from {playwrightLocator} as {string}', async function(element, variableName) {
const text = await element.innerText();
memory.setValue(variableName, text);
});

// For WebdriverIO
When('I hover over {wdioLocator} and click {wdioLocator}', async function(hoverElement, clickElement) {
await hoverElement().moveTo();
await clickElement().click();
});

Then('I store text from {wdioLocator} as {string}', async function(element, variableName) {
const text = await element().getText();
memory.setValue(variableName, text);
});

Usage in feature files:

Feature: Custom Steps with Page Objects

Scenario: Using custom steps with page objects
When I hover over 'Header > User Menu' and click 'Header > Profile Link'
Then I store text from 'Profile > User Name' as 'username'
And I expect '$username' to equal 'John Doe'

Combining with Memory Variables​

qavajs page objects can be powerfully combined with the memory module:

Feature: Memory Variables with Page Objects

Scenario: Using stored values with page objects
# Store some values
Given I save 'John Doe' as 'username'
And I save 'password123' as 'password'

# Use them with page objects
When I type '$username' to 'Login > Username Field'
And I type '$password' to 'Login > Password Field'
And I click 'Login > Submit Button'

# Use template locators with stored values
When I click 'Element By Text ($username)'

Best Practices​

Organization​

  1. Structure by functionality: Organize page objects by feature or page functionality
  2. Use nested components: Create reusable components for repeated UI patterns
  3. Use descriptive naming: Name elements clearly to match their purpose

Selector Strategy​

  1. Prefer stable selectors: Use IDs, data attributes, or roles over CSS classes when possible
  2. Keep selectors simple: Simple selectors are easier to maintain
  3. Use template locators for dynamic elements that follow a pattern
  4. Use native locators for complex selection logic

Maintainability​

  1. Keep page objects DRY: Avoid duplicating selectors
  2. Document complex selectors: Add comments to explain complex selection strategies
  3. Use component inheritance for shared behavior

Complete Example​

// App.js
const { locator } = require('@qavajs/steps-playwright/po');

class App {
Header = locator('header').as(HeaderComponent);
Footer = locator('footer').as(FooterComponent);

// Dynamic page content based on current route
Main = locator.native(({ page }) => {
const url = page.url();
if (url.includes('/products')) return page.locator('main').as(ProductsPage);
if (url.includes('/cart')) return page.locator('main').as(CartPage);
return page.locator('main').as(HomePage);
});
}

class HeaderComponent {
Logo = locator('.logo');
SearchBar = locator('input.search');
SearchButton = locator('button.search-submit');
NavigationItem = locator.template(text => `nav a:has-text('${text}')`);

// Handle complex navigation menu with hover
MenuDropdown = locator.template(category =>
`nav .dropdown:has-text('${category}')`);
SubMenuItem = locator.template((category, item) =>
`nav .dropdown:has-text('${category}') .submenu a:has-text('${item}')`);
}

class FooterComponent {
Copyright = locator('.copyright');
SocialLink = locator.template(platform => `.social-links a[title='${platform}']`);
LanguageSelector = locator('select.language');
}

class HomePage {
HeroImage = locator('.hero-image');
FeaturedProducts = locator('.featured-product').as(ProductCard);
NewsletterSignup = locator('form.newsletter');
}

class ProductsPage {
CategoryFilter = locator.template(category =>
`aside .filter-category:has-text('${category}')`);
ProductCards = locator('.product-card').as(ProductCard);
SortDropdown = locator('select.sort-by');
PaginationNext = locator('.pagination .next');
}

class ProductCard {
Title = locator('.product-title');
Price = locator('.product-price');
DiscountBadge = locator('.discount');
Rating = locator('.rating');
AddToCartButton = locator('button.add-to-cart');
QuickView = locator('button.quick-view');
}

class CartPage {
CartItems = locator('.cart-item').as(CartItem);
ContinueShopping = locator('a.continue-shopping');
CheckoutButton = locator('button.checkout');
CartTotal = locator('.cart-total');
}

class CartItem {
Title = locator('.item-title');
Price = locator('.item-price');
Quantity = locator('input.quantity');
Remove = locator('button.remove');
}

module.exports = App;

Example feature file using the complex page object:

Feature: E-commerce Website

Scenario: Search for and add product to cart
Given I open 'https://example-shop.com' url
When I type 'wireless headphones' to 'Header > Search Bar'
And I click 'Header > Search Button'

# Filter results
When I click 'Main > Category Filter (Electronics)'
And I select 'Price: Low to High' option from 'Main > Sort Dropdown' dropdown

# Add second product to cart
When I click 'Main > Product Cards > Add To Cart Button (2)'

# Go to cart
When I click 'Header > Navigation Item (Cart)'
Then I expect number of elements in 'Main > Cart Items' collection to equal '1'
And I expect text of 'Main > Cart Total' contains '$'

# Checkout
When I click 'Main > Checkout Button'