playwright-page-object-model
Use when creating page objects or refactoring Playwright tests for better maintainability with Page Object Model patterns.
What this skill does
# Playwright Page Object Model
Master the Page Object Model (POM) pattern to create maintainable, reusable,
and scalable test automation code. This skill covers modern Playwright
patterns including component-based architecture, locator strategies, and
app actions.
## Core POM Principles
### Single Responsibility
Each page object should represent one page or component with a single,
well-defined responsibility.
### Encapsulation
Hide implementation details and expose only meaningful actions and
assertions.
### Reusability
Create reusable components that can be composed into larger page objects.
### Maintainability
When UI changes, update page objects in one place rather than across
multiple tests.
## Basic Page Object Pattern
### Simple Page Object
```typescript
// pages/login-page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage() {
return await this.errorMessage.textContent();
}
}
```
### Using Page Object in Tests
```typescript
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test.describe('Login', () => {
test('should login successfully', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await expect(page).toHaveURL('/dashboard');
});
test('should show error on invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpassword');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
});
```
## Locator Strategies
### Recommended Locator Priority
1. User-visible locators (getByRole, getByText, getByLabel)
2. Test IDs (getByTestId)
3. CSS/XPath (only as last resort)
### User-Visible Locators
```typescript
export class HomePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// By role (ARIA role)
get searchButton() {
return this.page.getByRole('button', { name: 'Search' });
}
// By label (form inputs)
get searchInput() {
return this.page.getByLabel('Search products');
}
// By text
get welcomeMessage() {
return this.page.getByText('Welcome back');
}
// By placeholder
get emailInput() {
return this.page.getByPlaceholder('Enter your email');
}
// By alt text (images)
get logo() {
return this.page.getByAltText('Company Logo');
}
// By title
get helpIcon() {
return this.page.getByTitle('Help');
}
}
```
### Test ID Locators
```typescript
// Component with test IDs
// <button data-testid="submit-button">Submit</button>
export class FormPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get submitButton() {
return this.page.getByTestId('submit-button');
}
get formContainer() {
return this.page.getByTestId('form-container');
}
}
```
### Locator Chaining
```typescript
export class ProductPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Chain locators for specificity
get priceInCart() {
return this.page
.getByTestId('shopping-cart')
.getByRole('cell', { name: 'Price' });
}
// Filter locators
getProductByName(name: string) {
return this.page
.getByRole('listitem')
.filter({ hasText: name });
}
// Nth element
get firstProduct() {
return this.page.getByRole('article').nth(0);
}
}
```
## Component-Based Architecture
### Reusable Component Objects
```typescript
// components/navigation.ts
export class Navigation {
readonly page: Page;
readonly homeLink: Locator;
readonly productsLink: Locator;
readonly cartLink: Locator;
readonly profileMenu: Locator;
constructor(page: Page) {
this.page = page;
this.homeLink = page.getByRole('link', { name: 'Home' });
this.productsLink = page.getByRole('link', { name: 'Products' });
this.cartLink = page.getByRole('link', { name: 'Cart' });
this.profileMenu = page.getByRole('button', { name: 'Profile' });
}
async navigateToHome() {
await this.homeLink.click();
}
async navigateToProducts() {
await this.productsLink.click();
}
async navigateToCart() {
await this.cartLink.click();
}
async openProfileMenu() {
await this.profileMenu.click();
}
}
```
### Composing Page Objects with Components
```typescript
// pages/base-page.ts
import { Page } from '@playwright/test';
import { Navigation } from '../components/navigation';
import { Footer } from '../components/footer';
export class BasePage {
readonly page: Page;
readonly navigation: Navigation;
readonly footer: Footer;
constructor(page: Page) {
this.page = page;
this.navigation = new Navigation(page);
this.footer = new Footer(page);
}
}
```
```typescript
// pages/product-page.ts
import { BasePage } from './base-page';
import { Page } from '@playwright/test';
export class ProductPage extends BasePage {
readonly addToCartButton: Locator;
readonly productTitle: Locator;
readonly productPrice: Locator;
constructor(page: Page) {
super(page);
this.addToCartButton = page.getByRole('button', { name: 'Add to Cart' });
this.productTitle = page.getByRole('heading', { level: 1 });
this.productPrice = page.getByTestId('product-price');
}
async goto(productId: string) {
await this.page.goto(`/products/${productId}`);
}
async addToCart() {
await this.addToCartButton.click();
// Wait for cart update
await this.page.waitForResponse(
(response) => response.url().includes('/api/cart')
);
}
async getProductTitle() {
return await this.productTitle.textContent();
}
async getProductPrice() {
const text = await this.productPrice.textContent();
return parseFloat(text?.replace('$', '') || '0');
}
}
```
### Modal and Dialog Components
```typescript
// components/modal.ts
export class Modal {
readonly page: Page;
readonly container: Locator;
readonly closeButton: Locator;
readonly title: Locator;
constructor(page: Page) {
this.page = page;
this.container = page.getByRole('dialog');
this.closeButton = this.container.getByRole('button', { name: 'Close' });
this.title = this.container.getByRole('heading');
}
async isVisible() {
return await this.container.isVisible();
}
async getTitle() {
return await this.title.textContent();
}
async close() {
await this.closeButton.click();
await this.container.waitFor({ state: 'hidden' });
}
}
```
```typescript
// components/confirmation-modal.ts
import { Modal } from './modal';
import { Page } from '@playwright/test';
export class ConfirmationModal extends Modal {
readonly confirmButton: Locator;
readonly cancelButton: Locator;
readonly message: Locator;
constructor(page: Page) {
super(page);
this.confirmButton = this.container.getByRole('button', {
name: 'Confirm',
});
this.cancelButton = this.container.getByRole('button', {
name: 'Cancel',
});
this.message = this.container.getByTestId('modal-message');
}
async confirm() {
await this.confirmButton.click(Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.