Skip to main content
page-object-model Complete POM design patterns for Playwright test automation. Use when building maintainable test architecture, creating page objects, base pages, component objects, page factories, or flow objects for multi-page journeys.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/AbhiRKeesara/test-automation-skills --skill page-object-model命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 同仓库更多 Skills Accessibility (a11y) testing patterns with Playwright and axe-core. Use when adding WCAG 2.1 compliance checks, keyboard navigation testing, screen reader compatibility, or color contrast validation to your test suite.
Storage state reuse, 2FA/TOTP testing, multi-role auth, session management, OAuth flows, and secure credential handling in Playwright
GitHub Actions setup, parallel execution, sharding, test filtering, artifact management, and pipeline optimization for Playwright tests
name page-object-model description Complete POM design patterns for Playwright test automation. Use when building maintainable test architecture, creating page objects, base pages, component objects, page factories, or flow objects for multi-page journeys.
Page Object Model (POM) Skill
A comprehensive guide to implementing the Page Object Model pattern in Playwright for maintainable, scalable test automation.
What is Page Object Model?
Page Object Model (POM) is a design pattern that creates an object repository for web UI elements. It reduces code duplication and improves test maintenance by separating test logic from page interactions.
Benefits of POM
Benefit Description Maintainability Change locators in one place when UI changes Reusability Share page objects across multiple tests Readability Tests read like user stories Abstraction Hide complex interactions behind simple methods Scalability Easy to add new tests and pages
Core Patterns
1. Basic Page Object
The foundation of POM - encapsulates page elements and actions.
import { Page , Locator , expect } from '@playwright/test' ;
export class LoginPage {
: ;
: ;
: ;
: ;
: ;
: ;
( ) {
. = page;
. = page. ( );
. = page. ( );
. = page. ( , { : });
. = page. ( );
. = page. ( , { : });
}
(): < > {
. . ( );
}
( : , : ): < > {
. . (email);
. . (password);
. . ();
}
(): < > {
( . . ()) ?? ;
}
( : ): < > {
( . ). (message);
}
}
readonly
page
Page
readonly
emailInput
Locator
readonly
passwordInput
Locator
readonly
loginButton
Locator
readonly
errorMessage
Locator
readonly
forgotPasswordLink
Locator
constructor
page : Page
this
page
this
emailInput
getByLabel
'Email address'
this
passwordInput
getByLabel
'Password'
this
loginButton
getByRole
'button'
name
'Sign in'
this
errorMessage
getByRole
'alert'
this
forgotPasswordLink
getByRole
'link'
name
'Forgot password?'
async
navigate
Promise
void
await
this
page
goto
'/login'
async
login
email
string
password
string
Promise
void
await
this
emailInput
fill
await
this
passwordInput
fill
await
this
loginButton
click
async
getErrorMessage
Promise
string
return
await
this
errorMessage
textContent
''
async
expectErrorMessage
message
string
Promise
void
await
expect
this
errorMessage
toHaveText
2. Base Page Object (Inheritance Pattern) Create a base class for shared functionality across all pages.
import { Page , Locator } from '@playwright/test' ;
export abstract class BasePage {
readonly page : Page ;
readonly header : Locator ;
readonly footer : Locator ;
readonly loadingSpinner : Locator ;
constructor (page : Page ) {
this .page = page;
this .header = page.getByRole ('banner' );
this .footer = page.getByRole ('contentinfo' );
this .loadingSpinner = page.getByTestId ('loading-spinner' );
}
abstract navigate (): Promise <void >;
async waitForPageLoad (): Promise <void > {
await this .page .waitForLoadState ('networkidle' );
}
async waitForSpinnerToDisappear (): Promise <void > {
await this .loadingSpinner .waitFor ({ state : 'hidden' });
}
async getPageTitle (): Promise <string > {
return this .page .title ();
}
async takeScreenshot (name : string ): Promise <void > {
await this .page .screenshot ({ path : `screenshots/${name} .png` });
}
async clickLogo (): Promise <void > {
await this .header .getByRole ('link' , { name : 'Home' }).click ();
}
async isLoggedIn (): Promise <boolean > {
return this .header .getByRole ('button' , { name : 'Account' }).isVisible ();
}
}
import { Page , Locator , expect } from '@playwright/test' ;
import { BasePage } from './BasePage' ;
export class ProductPage extends BasePage {
readonly productTitle : Locator ;
readonly productPrice : Locator ;
readonly productDescription : Locator ;
readonly addToCartButton : Locator ;
readonly quantityInput : Locator ;
readonly reviewsSection : Locator ;
constructor (page : Page ) {
super (page);
this .productTitle = page.getByRole ('heading' , { level : 1 });
this .productPrice = page.getByTestId ('product-price' );
this .productDescription = page.getByTestId ('product-description' );
this .addToCartButton = page.getByRole ('button' , { name : 'Add to Cart' });
this .quantityInput = page.getByLabel ('Quantity' );
this .reviewsSection = page.getByRole ('region' , { name : 'Customer Reviews' });
}
async navigate (productId ?: string ): Promise <void > {
const path = productId ? `/products/${productId} ` : '/products' ;
await this .page .goto (path);
await this .waitForPageLoad ();
}
async addToCart (quantity : number = 1 ): Promise <void > {
if (quantity > 1 ) {
await this .quantityInput .fill (quantity.toString ());
}
await this .addToCartButton .click ();
await this .waitForSpinnerToDisappear ();
}
async getPrice (): Promise <number > {
const priceText = await this .productPrice .textContent ();
return parseFloat (priceText?.replace ('$' , '' ) ?? '0' );
}
async expectProductName (name : string ): Promise <void > {
await expect (this .productTitle ).toHaveText (name);
}
}
3. Component Objects Pattern Break down complex pages into reusable component objects.
import { Page , Locator } from '@playwright/test' ;
export class HeaderComponent {
readonly page : Page ;
readonly root : Locator ;
readonly logo : Locator ;
readonly searchInput : Locator ;
readonly searchButton : Locator ;
readonly cartIcon : Locator ;
readonly cartCount : Locator ;
readonly accountMenu : Locator ;
constructor (page : Page ) {
this .page = page;
this .root = page.getByRole ('banner' );
this .logo = this .root .getByRole ('link' , { name : 'Home' });
this .searchInput = this .root .getByPlaceholder ('Search products...' );
this .searchButton = this .root .getByRole ('button' , { name : 'Search' });
this .cartIcon = this .root .getByRole ('link' , { name : /Cart/ });
this .cartCount = this .root .getByTestId ('cart-count' );
this .accountMenu = this .root .getByRole ('button' , { name : 'Account' });
}
async search (query : string ): Promise <void > {
await this .searchInput .fill (query);
await this .searchButton .click ();
}
async getCartItemCount (): Promise <number > {
const count = await this .cartCount .textContent ();
return parseInt (count ?? '0' , 10 );
}
async goToCart (): Promise <void > {
await this .cartIcon .click ();
}
async openAccountMenu (): Promise <void > {
await this .accountMenu .click ();
}
}
import { Locator } from '@playwright/test' ;
export class ProductCardComponent {
readonly root : Locator ;
readonly image : Locator ;
readonly title : Locator ;
readonly price : Locator ;
readonly addToCartButton : Locator ;
readonly wishlistButton : Locator ;
readonly rating : Locator ;
constructor (root : Locator ) {
this .root = root;
this .image = root.getByRole ('img' );
this .title = root.getByRole ('heading' );
this .price = root.getByTestId ('price' );
this .addToCartButton = root.getByRole ('button' , { name : 'Add to Cart' });
this .wishlistButton = root.getByRole ('button' , { name : 'Add to Wishlist' });
this .rating = root.getByRole ('img' , { name : /stars/ });
}
async click (): Promise <void > {
await this .root .click ();
}
async addToCart (): Promise <void > {
await this .addToCartButton .click ();
}
async addToWishlist (): Promise <void > {
await this .wishlistButton .click ();
}
async getName (): Promise <string > {
return (await this .title .textContent ()) ?? '' ;
}
async getPrice (): Promise <number > {
const priceText = await this .price .textContent ();
return parseFloat (priceText?.replace ('$' , '' ) ?? '0' );
}
}
import { Page , Locator } from '@playwright/test' ;
export class NavigationComponent {
readonly page : Page ;
readonly root : Locator ;
constructor (page : Page ) {
this .page = page;
this .root = page.getByRole ('navigation' , { name : 'Main' });
}
getCategory (name : string ): Locator {
return this .root .getByRole ('link' , { name });
}
async navigateToCategory (category : string ): Promise <void > {
await this .getCategory (category).click ();
}
async expandSubmenu (menuName : string ): Promise <void > {
await this .root .getByRole ('button' , { name : menuName }).hover ();
}
async getVisibleCategories (): Promise <string []> {
const links = await this .root .getByRole ('link' ).all ();
const categories : string [] = [];
for (const link of links) {
const text = await link.textContent ();
if (text) categories.push (text.trim ());
}
return categories;
}
}
import { Page , Locator } from '@playwright/test' ;
import { BasePage } from './BasePage' ;
import { HeaderComponent } from './components/HeaderComponent' ;
import { NavigationComponent } from './components/NavigationComponent' ;
import { ProductCardComponent } from './components/ProductCardComponent' ;
export class ProductListPage extends BasePage {
readonly header : HeaderComponent ;
readonly navigation : NavigationComponent ;
readonly productGrid : Locator ;
readonly sortDropdown : Locator ;
readonly filterPanel : Locator ;
readonly resultsCount : Locator ;
constructor (page : Page ) {
super (page);
this .header = new HeaderComponent (page);
this .navigation = new NavigationComponent (page);
this .productGrid = page.getByRole ('region' , { name : 'Products' });
this .sortDropdown = page.getByLabel ('Sort by' );
this .filterPanel = page.getByRole ('complementary' , { name : 'Filters' });
this .resultsCount = page.getByTestId ('results-count' );
}
async navigate (): Promise <void > {
await this .page .goto ('/products' );
}
async getProductCards (): Promise <ProductCardComponent []> {
const cards = await this .productGrid .getByTestId ('product-card' ).all ();
return cards.map (card => new ProductCardComponent (card));
}
getProductCard (productName : string ): ProductCardComponent {
const card = this .productGrid
.getByTestId ('product-card' )
.filter ({ hasText : productName });
return new ProductCardComponent (card);
}
async sortBy (option : string ): Promise <void > {
await this .sortDropdown .selectOption (option);
await this .waitForSpinnerToDisappear ();
}
async filterByCategory (category : string ): Promise <void > {
await this .filterPanel .getByLabel (category).check ();
await this .waitForSpinnerToDisappear ();
}
async filterByPriceRange (min : number , max : number ): Promise <void > {
await this .filterPanel .getByLabel ('Min price' ).fill (min.toString ());
await this .filterPanel .getByLabel ('Max price' ).fill (max.toString ());
await this .filterPanel .getByRole ('button' , { name : 'Apply' }).click ();
await this .waitForSpinnerToDisappear ();
}
async getProductCount (): Promise <number > {
const text = await this .resultsCount .textContent ();
const match = text?.match (/(\d+)/ );
return match ? parseInt (match[1 ], 10 ) : 0 ;
}
}
4. Page Factory Pattern Create pages on demand with proper type safety.
import { Page } from '@playwright/test' ;
import { LoginPage } from './LoginPage' ;
import { ProductListPage } from './ProductListPage' ;
import { ProductPage } from './ProductPage' ;
import { CartPage } from './CartPage' ;
import { CheckoutPage } from './CheckoutPage' ;
import { AccountPage } from './AccountPage' ;
export type PageType =
| 'login'
| 'productList'
| 'product'
| 'cart'
| 'checkout'
| 'account' ;
export class PageFactory {
private page : Page ;
private pageInstances : Map <PageType , unknown > = new Map ();
constructor (page : Page ) {
this .page = page;
}
getLoginPage (): LoginPage {
if (!this .pageInstances .has ('login' )) {
this .pageInstances .set ('login' , new LoginPage (this .page ));
}
return this .pageInstances .get ('login' ) as LoginPage ;
}
getProductListPage (): ProductListPage {
if (!this .pageInstances .has ('productList' )) {
this .pageInstances .set ('productList' , new ProductListPage (this .page ));
}
return this .pageInstances .get ('productList' ) as ProductListPage ;
}
getProductPage (): ProductPage {
if (!this .pageInstances .has ('product' )) {
this .pageInstances .set ('product' , new ProductPage (this .page ));
}
return this .pageInstances .get ('product' ) as ProductPage ;
}
getCartPage (): CartPage {
if (!this .pageInstances .has ('cart' )) {
this .pageInstances .set ('cart' , new CartPage (this .page ));
}
return this .pageInstances .get ('cart' ) as CartPage ;
}
getCheckoutPage (): CheckoutPage {
if (!this .pageInstances .has ('checkout' )) {
this .pageInstances .set ('checkout' , new CheckoutPage (this .page ));
}
return this .pageInstances .get ('checkout' ) as CheckoutPage ;
}
getAccountPage (): AccountPage {
if (!this .pageInstances .has ('account' )) {
this .pageInstances .set ('account' , new AccountPage (this .page ));
}
return this .pageInstances .get ('account' ) as AccountPage ;
}
clearCache (): void {
this .pageInstances .clear ();
}
}
import { test as base } from '@playwright/test' ;
import { PageFactory } from '../page-objects/PageFactory' ;
type PageFixtures = {
pages : PageFactory ;
};
export const test = base.extend <PageFixtures >({
pages : async ({ page }, use) => {
const factory = new PageFactory (page);
await use (factory);
factory.clearCache ();
},
});
export { expect } from '@playwright/test' ;
import { test, expect } from '../fixtures/pages.fixture' ;
test ('complete purchase flow' , async ({ pages }) => {
const loginPage = pages.getLoginPage ();
const productList = pages.getProductListPage ();
const cart = pages.getCartPage ();
const checkout = pages.getCheckoutPage ();
await loginPage.navigate ();
await loginPage.login ('user@example.com' , 'password' );
await productList.navigate ();
const product = productList.getProductCard ('Wireless Headphones' );
await product.addToCart ();
await cart.navigate ();
await expect (cart.itemCount ).toHaveText ('1' );
await cart.proceedToCheckout ();
await checkout.completeOrder ();
});
5. Fluent Interface Pattern Chain methods for more readable test code.
import { Page , Locator , expect } from '@playwright/test' ;
export class FluentCheckoutPage {
readonly page : Page ;
readonly firstNameInput : Locator ;
readonly lastNameInput : Locator ;
readonly emailInput : Locator ;
readonly addressInput : Locator ;
readonly cityInput : Locator ;
readonly zipInput : Locator ;
readonly cardNumberInput : Locator ;
readonly cardExpiryInput : Locator ;
readonly cardCvcInput : Locator ;
readonly placeOrderButton : Locator ;
constructor (page : Page ) {
this .page = page;
this .firstNameInput = page.getByLabel ('First name' );
this .lastNameInput = page.getByLabel ('Last name' );
this .emailInput = page.getByLabel ('Email' );
this .addressInput = page.getByLabel ('Address' );
this .cityInput = page.getByLabel ('City' );
this .zipInput = page.getByLabel ('ZIP code' );
this .cardNumberInput = page.getByLabel ('Card number' );
this .cardExpiryInput = page.getByLabel ('Expiry' );
this .cardCvcInput = page.getByLabel ('CVC' );
this .placeOrderButton = page.getByRole ('button' , { name : 'Place Order' });
}
async navigate (): Promise <this > {
await this .page .goto ('/checkout' );
return this ;
}
async withFirstName (name : string ): Promise <this > {
await this .firstNameInput .fill (name);
return this ;
}
async withLastName (name : string ): Promise <this > {
await this .lastNameInput .fill (name);
return this ;
}
async withEmail (email : string ): Promise <this > {
await this .emailInput .fill (email);
return this ;
}
async withAddress (address : string ): Promise <this > {
await this .addressInput .fill (address);
return this ;
}
async withCity (city : string ): Promise <this > {
await this .cityInput .fill (city);
return this ;
}
async withZipCode (zip : string ): Promise <this > {
await this .zipInput .fill (zip);
return this ;
}
async withCardNumber (number : string ): Promise <this > {
await this .cardNumberInput .fill (number );
return this ;
}
async withCardExpiry (expiry : string ): Promise <this > {
await this .cardExpiryInput .fill (expiry);
return this ;
}
async withCardCvc (cvc : string ): Promise <this > {
await this .cardCvcInput .fill (cvc);
return this ;
}
async placeOrder (): Promise <void > {
await this .placeOrderButton .click ();
await expect (this .page .getByRole ('heading' , { name : 'Order Confirmed' })).toBeVisible ();
}
}
test ('checkout with fluent interface' , async ({ page }) => {
const checkout = new FluentCheckoutPage (page);
await checkout
.navigate ()
.then (c => c.withFirstName ('John' ))
.then (c => c.withLastName ('Doe' ))
.then (c => c.withEmail ('john@example.com' ))
.then (c => c.withAddress ('123 Main St' ))
.then (c => c.withCity ('Seattle' ))
.then (c => c.withZipCode ('98101' ))
.then (c => c.withCardNumber ('4111111111111111' ))
.then (c => c.withCardExpiry ('12/25' ))
.then (c => c.withCardCvc ('123' ))
.then (c => c.placeOrder ());
});
test ('checkout with chaining' , async ({ page }) => {
const checkout = await new FluentCheckoutPage (page).navigate ();
await (await (await (await (await (await checkout
.withFirstName ('John' ))
.withLastName ('Doe' ))
.withEmail ('john@example.com' ))
.withAddress ('123 Main St' ))
.withCity ('Seattle' ))
.withZipCode ('98101' );
await checkout.placeOrder ();
});
6. Builder Pattern for Test Data Combine with page objects for clean test data setup.
export interface User {
firstName : string ;
lastName : string ;
email : string ;
password : string ;
phone ?: string ;
address ?: {
street : string ;
city : string ;
state : string ;
zip : string ;
};
}
export class UserBuilder {
private user : Partial <User > = {};
static aUser (): UserBuilder {
return new UserBuilder ();
}
static aDefaultUser (): User {
return UserBuilder .aUser ()
.withFirstName ('Test' )
.withLastName ('User' )
.withEmail (`test-${Date .now()} @example.com` )
.withPassword ('SecurePass123!' )
.build ();
}
withFirstName (firstName : string ): this {
this .user .firstName = firstName;
return this ;
}
withLastName (lastName : string ): this {
this .user .lastName = lastName;
return this ;
}
withEmail (email : string ): this {
this .user .email = email;
return this ;
}
withPassword (password : string ): this {
this .user .password = password;
return this ;
}
withPhone (phone : string ): this {
this .user .phone = phone;
return this ;
}
withAddress (street : string , city : string , state : string , zip : string ): this {
this .user .address = { street, city, state, zip };
return this ;
}
build (): User {
if (!this .user .firstName || !this .user .lastName || !this .user .email || !this .user .password ) {
throw new Error ('User must have firstName, lastName, email, and password' );
}
return this .user as User ;
}
}
import { UserBuilder } from '../test-data/builders/UserBuilder' ;
test ('register new user' , async ({ page }) => {
const registrationPage = new RegistrationPage (page);
const user = UserBuilder .aUser ()
.withFirstName ('Jane' )
.withLastName ('Smith' )
.withEmail ('jane.smith@example.com' )
.withPassword ('SecurePassword123!' )
.withPhone ('555-123-4567' )
.withAddress ('456 Oak Ave' , 'Portland' , 'OR' , '97201' )
.build ();
await registrationPage.navigate ();
await registrationPage.registerUser (user);
await expect (page.getByText ('Registration successful' )).toBeVisible ();
});
Advanced Patterns
7. Page Object with State Management Track page state for complex interactions.
import { Page , Locator , expect } from '@playwright/test' ;
interface CartItem {
id : string ;
name : string ;
quantity : number ;
price : number ;
}
export class StatefulCartPage {
readonly page : Page ;
private items : CartItem [] = [];
readonly cartContainer : Locator ;
readonly emptyCartMessage : Locator ;
readonly checkoutButton : Locator ;
readonly totalPrice : Locator ;
constructor (page : Page ) {
this .page = page;
this .cartContainer = page.getByRole ('region' , { name : 'Shopping Cart' });
this .emptyCartMessage = page.getByText ('Your cart is empty' );
this .checkoutButton = page.getByRole ('button' , { name : 'Checkout' });
this .totalPrice = page.getByTestId ('cart-total' );
}
async navigate (): Promise <void > {
await this .page .goto ('/cart' );
await this .syncState ();
}
async syncState (): Promise <void > {
this .items = [];
const itemElements = await this .cartContainer .getByTestId ('cart-item' ).all ();
for (const element of itemElements) {
const id = await element.getAttribute ('data-product-id' ) ?? '' ;
const name = await element.getByTestId ('item-name' ).textContent () ?? '' ;
const quantityText = await element.getByTestId ('item-quantity' ).inputValue ();
const priceText = await element.getByTestId ('item-price' ).textContent () ?? '0' ;
this .items .push ({
id,
name,
quantity : parseInt (quantityText, 10 ),
price : parseFloat (priceText.replace ('$' , '' )),
});
}
}
getCachedItems (): CartItem [] {
return [...this .items ];
}
async getItems (): Promise <CartItem []> {
await this .syncState ();
return this .getCachedItems ();
}
async getItemCount (): Promise <number > {
await this .syncState ();
return this .items .reduce ((sum, item ) => sum + item.quantity , 0 );
}
async getTotal (): Promise <number > {
await this .syncState ();
return this .items .reduce ((sum, item ) => sum + item.price * item.quantity , 0 );
}
async updateQuantity (productId : string , quantity : number ): Promise <void > {
const item = this .cartContainer .locator (`[data-product-id="${productId} "]` );
await item.getByLabel ('Quantity' ).fill (quantity.toString ());
await item.getByRole ('button' , { name : 'Update' }).click ();
await this .syncState ();
}
async removeItem (productId : string ): Promise <void > {
const item = this .cartContainer .locator (`[data-product-id="${productId} "]` );
await item.getByRole ('button' , { name : 'Remove' }).click ();
await this .syncState ();
}
async isEmpty (): Promise <boolean > {
return this .emptyCartMessage .isVisible ();
}
async expectTotal (expected : number ): Promise <void > {
await expect (this .totalPrice ).toHaveText (`$${expected.toFixed(2 )} ` );
}
}
8. Multi-Page Flow Objects Handle complex flows spanning multiple pages.
import { Page , expect } from '@playwright/test' ;
import { CartPage } from '../CartPage' ;
import { ShippingPage } from '../ShippingPage' ;
import { PaymentPage } from '../PaymentPage' ;
import { ConfirmationPage } from '../ConfirmationPage' ;
export interface ShippingInfo {
firstName : string ;
lastName : string ;
address : string ;
city : string ;
state : string ;
zip : string ;
}
export interface PaymentInfo {
cardNumber : string ;
expiry : string ;
cvc : string ;
nameOnCard : string ;
}
export class CheckoutFlow {
readonly page : Page ;
readonly cart : CartPage ;
readonly shipping : ShippingPage ;
readonly payment : PaymentPage ;
readonly confirmation : ConfirmationPage ;
constructor (page : Page ) {
this .page = page;
this .cart = new CartPage (page);
this .shipping = new ShippingPage (page);
this .payment = new PaymentPage (page);
this .confirmation = new ConfirmationPage (page);
}
async startFromCart (): Promise <this > {
await this .cart .navigate ();
return this ;
}
async proceedToShipping (): Promise <this > {
await this .cart .proceedToCheckout ();
await expect (this .page ).toHaveURL (/.*shipping/ );
return this ;
}
async fillShipping (info : ShippingInfo ): Promise <this > {
await this .shipping .fillForm (info);
return this ;
}
async proceedToPayment (): Promise <this > {
await this .shipping .continue ();
await expect (this .page ).toHaveURL (/.*payment/ );
return this ;
}
async fillPayment (info : PaymentInfo ): Promise <this > {
await this .payment .fillForm (info);
return this ;
}
async placeOrder (): Promise <string > {
await this .payment .placeOrder ();
await expect (this .page ).toHaveURL (/.*confirmation/ );
return this .confirmation .getOrderNumber ();
}
async completeCheckout (shipping : ShippingInfo , payment : PaymentInfo ): Promise <string > {
await this .startFromCart ();
await this .proceedToShipping ();
await this .fillShipping (shipping);
await this .proceedToPayment ();
await this .fillPayment (payment);
return this .placeOrder ();
}
}
test ('complete checkout flow' , async ({ page }) => {
const checkoutFlow = new CheckoutFlow (page);
const orderId = await checkoutFlow.completeCheckout (
{
firstName : 'John' ,
lastName : 'Doe' ,
address : '123 Main St' ,
city : 'Seattle' ,
state : 'WA' ,
zip : '98101' ,
},
{
cardNumber : '4111111111111111' ,
expiry : '12/25' ,
cvc : '123' ,
nameOnCard : 'John Doe' ,
}
);
expect (orderId).toMatch (/^ORD-\d+$/ );
});
9. Modal and Dialog Handling Handle modals as separate components.
import { Page , Locator } from '@playwright/test' ;
export class ConfirmationModal {
readonly page : Page ;
readonly root : Locator ;
readonly title : Locator ;
readonly message : Locator ;
readonly confirmButton : Locator ;
readonly cancelButton : Locator ;
readonly closeButton : Locator ;
constructor (page : Page ) {
this .page = page;
this .root = page.getByRole ('dialog' );
this .title = this .root .getByRole ('heading' );
this .message = this .root .getByTestId ('modal-message' );
this .confirmButton = this .root .getByRole ('button' , { name : /confirm|yes|ok/i });
this .cancelButton = this .root .getByRole ('button' , { name : /cancel|no/i });
this .closeButton = this .root .getByRole ('button' , { name : 'Close' });
}
async waitForOpen (): Promise <void > {
await this .root .waitFor ({ state : 'visible' });
}
async waitForClose (): Promise <void > {
await this .root .waitFor ({ state : 'hidden' });
}
async isOpen (): Promise <boolean > {
return this .root .isVisible ();
}
async confirm (): Promise <void > {
await this .confirmButton .click ();
await this .waitForClose ();
}
async cancel (): Promise <void > {
await this .cancelButton .click ();
await this .waitForClose ();
}
async close (): Promise <void > {
await this .closeButton .click ();
await this .waitForClose ();
}
async getMessage (): Promise <string > {
return (await this .message .textContent ()) ?? '' ;
}
}
import { ConfirmationModal } from './components/ConfirmationModal' ;
export class CartPage {
readonly page : Page ;
readonly confirmationModal : ConfirmationModal ;
constructor (page : Page ) {
this .page = page;
this .confirmationModal = new ConfirmationModal (page);
}
async removeItem (productId : string ): Promise <void > {
await this .getRemoveButton (productId).click ();
await this .confirmationModal .waitForOpen ();
await this .confirmationModal .confirm ();
}
async removeItemWithCancel (productId : string ): Promise <void > {
await this .getRemoveButton (productId).click ();
await this .confirmationModal .waitForOpen ();
await this .confirmationModal .cancel ();
}
}
Folder Structure for POM your-project/
├── page-objects/
│ ├── BasePage.ts # Abstract base class
│ ├── PageFactory.ts # Page factory
│ ├── index.ts # Barrel exports
│ │
│ ├── components/ # Reusable components
│ │ ├── HeaderComponent.ts
│ │ ├── FooterComponent.ts
│ │ ├── NavigationComponent.ts
│ │ ├── ProductCardComponent.ts
│ │ ├── ConfirmationModal.ts
│ │ └── index.ts
│ │
│ ├── flows/ # Multi-page flows
│ │ ├── CheckoutFlow.ts
│ │ ├── RegistrationFlow.ts
│ │ └── index.ts
│ │
│ ├── auth/ # Auth pages
│ │ ├── LoginPage.ts
│ │ ├── RegisterPage.ts
│ │ ├── ForgotPasswordPage.ts
│ │ └── index.ts
│ │
│ ├── products/ # Product pages
│ │ ├── ProductListPage.ts
│ │ ├── ProductPage.ts
│ │ ├── SearchResultsPage.ts
│ │ └── index.ts
│ │
│ ├── checkout/ # Checkout pages
│ │ ├── CartPage.ts
│ │ ├── ShippingPage.ts
│ │ ├── PaymentPage.ts
│ │ ├── ConfirmationPage.ts
│ │ └── index.ts
│ │
│ └── account/ # Account pages
│ ├── ProfilePage.ts
│ ├── OrderHistoryPage.ts
│ ├── AddressBookPage.ts
│ └── index.ts
│
├── test-data/
│ ├── builders/ # Test data builders
│ │ ├── UserBuilder.ts
│ │ ├── ProductBuilder.ts
│ │ └── OrderBuilder.ts
│ └── fixtures/
│ ├── users.json
│ └── products.json
│
├── fixtures/ # Playwright fixtures
│ ├── pages.fixture.ts
│ ├── auth.fixture.ts
│ └── index.ts
│
└── tests/
├── auth/
├── products/
├── checkout/
└── account/
Best Practices Checklist
Do's
Don'ts
Anti-Patterns to Avoid
1. God Page Object
class AllInOnePage {
async login ( ) {}
async searchProduct ( ) {}
async addToCart ( ) {}
async checkout ( ) {}
async updateProfile ( ) {}
}
2. Assertions in Page Objects
class ProductPage {
async verifyProductVisible ( ) {
expect (this .productTitle ).toBeVisible ();
}
}
class ProductPage {
get productTitle (): Locator {
return this .page .getByRole ('heading' , { level : 1 });
}
}
await expect (productPage.productTitle ).toBeVisible ();
3. Exposing Implementation Details
class LoginPage {
readonly emailSelector = '#email-input' ;
readonly passwordSelector = '[data-testid="password"]' ;
}
class LoginPage {
readonly emailInput : Locator ;
readonly passwordInput : Locator ;
constructor (page : Page ) {
this .emailInput = page.getByLabel ('Email' );
this .passwordInput = page.getByLabel ('Password' );
}
}
Related Resources