Create maintainable and reusable Page Object Models (POMs) for Playwright tests. Generates TypeScript classes that encapsulate page-specific locators and actions, following the Page Object Model design pattern with data-testid locators exclusively.
Generates TypeScript Page Object Models for Playwright tests using data-testid locators. Use when creating maintainable page classes to encapsulate element locators and user actions for complex test suites.
/plugin marketplace add joel611/claude-plugins/plugin install playwright-e2e@joel-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
resources/base-page.tsresources/component-template.tsresources/page-template.tsCreate maintainable and reusable Page Object Models (POMs) for Playwright tests. Generates TypeScript classes that encapsulate page-specific locators and actions, following the Page Object Model design pattern with data-testid locators exclusively.
Use this skill when you need to:
Do NOT use this skill when:
Before using this skill:
Gather from the user:
Determine:
LoginPage, DashboardPage, CheckoutPage)Generate a TypeScript class with:
Structure:
import { Page, Locator } from '@playwright/test';
export class PageName {
readonly page: Page;
// Locators
readonly elementName: Locator;
constructor(page: Page) {
this.page = page;
this.elementName = page.locator('[data-testid="element-name"]');
}
// Navigation
async goto() {
await this.page.goto('/page-url');
}
// Actions
async performAction() {
await this.elementName.click();
}
// Getters for assertions
getElement() {
return this.elementName;
}
}
Key Requirements:
For each element:
readonly elementName: Locator;
constructor(page: Page) {
this.page = page;
this.elementName = page.locator('[data-testid="element-name"]');
}
Naming Convention:
submitButton, emailInput, errorMessage)For each user action:
/**
* Descriptive action name
* @param param - Parameter description if needed
*/
async actionName(param?: string): Promise<void> {
// Wait for element if needed
await this.element.waitFor({ state: 'visible' });
// Perform action
await this.element.click();
// or
await this.element.fill(param);
}
Common Actions:
async fillForm(data: FormData)async clickButton()async navigateTo(section: string)async completeCheckout(details: CheckoutDetails)For elements that tests will assert against:
getElementName(): Locator {
return this.elementName;
}
async getTextContent(): Promise<string> {
return await this.element.textContent() || '';
}
async isElementVisible(): Promise<boolean> {
return await this.element.isVisible();
}
Ensure the Page Object includes:
Show how to use the Page Object in tests:
import { test, expect } from '@playwright/test';
import { PageName } from './page-objects/PageName';
test('test description', async ({ page }) => {
const pageName = new PageName(page);
await pageName.goto();
await pageName.performAction();
await expect(pageName.getElement()).toBeVisible();
});
Input: "Create a Page Object for the login page with username, password fields, login button, and error message."
Output:
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for the Login Page
*
* Handles user authentication flow including:
* - Filling login credentials
* - Submitting the form
* - Accessing error messages
*/
export class LoginPage {
readonly page: Page;
// Locators
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('[data-testid="username-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error-message"]');
this.forgotPasswordLink = page.locator('[data-testid="forgot-password-link"]');
}
/**
* Navigate to the login page
*/
async goto(): Promise<void> {
await this.page.goto('/login');
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Perform login with credentials
* @param username - User's username or email
* @param password - User's password
*/
async login(username: string, password: string): Promise<void> {
await this.usernameInput.waitFor({ state: 'visible' });
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
/**
* Fill only the username field
*/
async fillUsername(username: string): Promise<void> {
await this.usernameInput.fill(username);
}
/**
* Fill only the password field
*/
async fillPassword(password: string): Promise<void> {
await this.passwordInput.fill(password);
}
/**
* Click the login button
*/
async clickLogin(): Promise<void> {
await this.loginButton.click();
}
/**
* Click forgot password link
*/
async clickForgotPassword(): Promise<void> {
await this.forgotPasswordLink.click();
}
/**
* Get the error message element for assertions
*/
getErrorMessage(): Locator {
return this.errorMessage;
}
/**
* Check if error message is visible
*/
async hasError(): Promise<boolean> {
try {
await this.errorMessage.waitFor({ state: 'visible', timeout: 2000 });
return true;
} catch {
return false;
}
}
/**
* Get the text content of the error message
*/
async getErrorText(): Promise<string> {
const text = await this.errorMessage.textContent();
return text?.trim() || '';
}
}
Usage:
import { test, expect } from '@playwright/test';
import { LoginPage } from './page-objects/LoginPage';
test.describe('Login Flow', () => {
test('should login successfully with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'SecurePass123');
// Assert navigation to dashboard
await page.waitForURL('/dashboard');
});
test('should show error with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrongpass');
// Assert error message
await expect(loginPage.getErrorMessage()).toBeVisible();
await expect(loginPage.getErrorMessage()).toContainText('Invalid credentials');
});
});
Input: "Create a Page Object for a product details page with product name, price, description, quantity selector, add to cart button, and reviews section."
Output:
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for Product Details Page
*
* Handles product viewing and purchasing actions including:
* - Viewing product details
* - Selecting quantity
* - Adding to cart
* - Reading reviews
*/
export class ProductPage {
readonly page: Page;
// Product Information Locators
readonly productName: Locator;
readonly productPrice: Locator;
readonly productDescription: Locator;
readonly productImage: Locator;
// Purchase Locators
readonly quantityInput: Locator;
readonly addToCartButton: Locator;
readonly buyNowButton: Locator;
// Reviews Locators
readonly reviewsSection: Locator;
readonly reviewItems: Locator;
readonly averageRating: Locator;
// Additional Actions
readonly wishlistButton: Locator;
readonly shareButton: Locator;
constructor(page: Page) {
this.page = page;
// Product information
this.productName = page.locator('[data-testid="product-name"]');
this.productPrice = page.locator('[data-testid="product-price"]');
this.productDescription = page.locator('[data-testid="product-description"]');
this.productImage = page.locator('[data-testid="product-image"]');
// Purchase
this.quantityInput = page.locator('[data-testid="quantity-input"]');
this.addToCartButton = page.locator('[data-testid="add-to-cart-button"]');
this.buyNowButton = page.locator('[data-testid="buy-now-button"]');
// Reviews
this.reviewsSection = page.locator('[data-testid="reviews-section"]');
this.reviewItems = page.locator('[data-testid="review-item"]');
this.averageRating = page.locator('[data-testid="average-rating"]');
// Actions
this.wishlistButton = page.locator('[data-testid="wishlist-button"]');
this.shareButton = page.locator('[data-testid="share-button"]');
}
/**
* Navigate to a product page by ID
*/
async goto(productId: string): Promise<void> {
await this.page.goto(`/products/${productId}`);
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Set the quantity for purchase
*/
async setQuantity(quantity: number): Promise<void> {
await this.quantityInput.waitFor({ state: 'visible' });
await this.quantityInput.clear();
await this.quantityInput.fill(quantity.toString());
}
/**
* Add product to cart with specified quantity
*/
async addToCart(quantity = 1): Promise<void> {
if (quantity > 1) {
await this.setQuantity(quantity);
}
await this.addToCartButton.waitFor({ state: 'enabled' });
await this.addToCartButton.click();
}
/**
* Click Buy Now button
*/
async buyNow(): Promise<void> {
await this.buyNowButton.click();
}
/**
* Add product to wishlist
*/
async addToWishlist(): Promise<void> {
await this.wishlistButton.click();
}
/**
* Click share button
*/
async shareProduct(): Promise<void> {
await this.shareButton.click();
}
/**
* Get product name text
*/
async getProductName(): Promise<string> {
const text = await this.productName.textContent();
return text?.trim() || '';
}
/**
* Get product price text
*/
async getProductPrice(): Promise<string> {
const text = await this.productPrice.textContent();
return text?.trim() || '';
}
/**
* Get number of reviews
*/
async getReviewCount(): Promise<number> {
return await this.reviewItems.count();
}
/**
* Get average rating text
*/
async getAverageRating(): Promise<string> {
const text = await this.averageRating.textContent();
return text?.trim() || '';
}
// Getters for assertions
getProductNameElement(): Locator {
return this.productName;
}
getProductPriceElement(): Locator {
return this.productPrice;
}
getAddToCartButton(): Locator {
return this.addToCartButton;
}
getReviewsSection(): Locator {
return this.reviewsSection;
}
}
Input: "Create a Page Object for a dashboard with header, sidebar navigation, main content area, and user profile dropdown."
Output:
import { Page, Locator } from '@playwright/test';
/**
* Page Object Model for Dashboard Page
*
* Handles dashboard navigation and interactions including:
* - Sidebar navigation
* - User profile actions
* - Dashboard content
*/
export class DashboardPage {
readonly page: Page;
// Header Locators
readonly header: Locator;
readonly logo: Locator;
readonly searchBar: Locator;
readonly notificationIcon: Locator;
readonly userProfileDropdown: Locator;
// Sidebar Locators
readonly sidebar: Locator;
readonly homeLink: Locator;
readonly projectsLink: Locator;
readonly settingsLink: Locator;
readonly logoutButton: Locator;
// Main Content Locators
readonly mainContent: Locator;
readonly dashboardTitle: Locator;
readonly statsCards: Locator;
// Profile Dropdown Locators
readonly profileMenu: Locator;
readonly profileLink: Locator;
readonly accountSettingsLink: Locator;
constructor(page: Page) {
this.page = page;
// Header
this.header = page.locator('[data-testid="dashboard-header"]');
this.logo = page.locator('[data-testid="logo"]');
this.searchBar = page.locator('[data-testid="search-bar"]');
this.notificationIcon = page.locator('[data-testid="notification-icon"]');
this.userProfileDropdown = page.locator('[data-testid="user-profile-dropdown"]');
// Sidebar
this.sidebar = page.locator('[data-testid="sidebar"]');
this.homeLink = page.locator('[data-testid="nav-home"]');
this.projectsLink = page.locator('[data-testid="nav-projects"]');
this.settingsLink = page.locator('[data-testid="nav-settings"]');
this.logoutButton = page.locator('[data-testid="logout-button"]');
// Main Content
this.mainContent = page.locator('[data-testid="main-content"]');
this.dashboardTitle = page.locator('[data-testid="dashboard-title"]');
this.statsCards = page.locator('[data-testid="stat-card"]');
// Profile Dropdown
this.profileMenu = page.locator('[data-testid="profile-menu"]');
this.profileLink = page.locator('[data-testid="profile-link"]');
this.accountSettingsLink = page.locator('[data-testid="account-settings-link"]');
}
async goto(): Promise<void> {
await this.page.goto('/dashboard');
await this.page.waitForLoadState('domcontentloaded');
}
/**
* Navigate using sidebar
*/
async navigateToHome(): Promise<void> {
await this.homeLink.click();
}
async navigateToProjects(): Promise<void> {
await this.projectsLink.click();
}
async navigateToSettings(): Promise<void> {
await this.settingsLink.click();
}
/**
* Search functionality
*/
async search(query: string): Promise<void> {
await this.searchBar.fill(query);
await this.page.keyboard.press('Enter');
}
/**
* Profile dropdown actions
*/
async openProfileDropdown(): Promise<void> {
await this.userProfileDropdown.click();
await this.profileMenu.waitFor({ state: 'visible' });
}
async navigateToProfile(): Promise<void> {
await this.openProfileDropdown();
await this.profileLink.click();
}
async navigateToAccountSettings(): Promise<void> {
await this.openProfileDropdown();
await this.accountSettingsLink.click();
}
async logout(): Promise<void> {
await this.logoutButton.click();
}
/**
* Get stats count
*/
async getStatsCount(): Promise<number> {
return await this.statsCards.count();
}
// Getters for assertions
getHeader(): Locator {
return this.header;
}
getSidebar(): Locator {
return this.sidebar;
}
getMainContent(): Locator {
return this.mainContent;
}
getDashboardTitle(): Locator {
return this.dashboardTitle;
}
}
page-objects/ or pages/ directoryProblem: Page Object has 30+ locators making it hard to maintain
Solutions:
Problem: Tests fail despite using POMs
Solutions:
Problem: Same logic repeated in multiple Page Objects
Solutions:
Problem: Action methods contain complex logic and are hard to test
Solutions:
Problem: Can't verify Page Object behavior without full tests
Solutions:
The resources/ directory contains templates for common patterns:
page-template.ts - Basic Page Object structurecomponent-template.ts - Component-based Page Objectbase-page.ts - Base class with common functionalityUse when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.