create-pom
Installation
SKILL.md
Create Page Object Model
Generate a POM class for the specified page following all E2E test conventions.
Workflow
- Determine the page name from the user's request (e.g., "Settings" →
settings.page.ts) - Check if a POM already exists for this page in
e2e/poms/. If so, add methods to the existing POM — do NOT create a second POM for the same page - Identify the base page class in
e2e/poms/base.page.tsand extend it - Read the target page in the application to understand its elements and interactions
- Generate the POM file following the template below
- Verify the file compiles with no TypeScript errors
Rules
- One page = one POM — every distinct page/view gets exactly one POM file
- All POMs extend the base page class — check
e2e/poms/base.page.tsfor the abstract class - Implement
setUp()andtearDown()— inherited from the base class - Reusable helpers belong in
BasePage— if a helper method is useful across multiple POMs (e.g.,waitForToast,dismissModal), it must live inBasePage, not in a derived POM. Derived POMs should contain only page-specific behavior. If you find yourself writing the same helper in a second POM, promote it toBasePageimmediately - NEVER reference another POM from within a POM — if a test needs multiple pages, the spec file orchestrates between POMs
- Detailed JSDoc on every public method — describe what it does step by step, with
@paramtags - Use the selector priority order:
getByRole()>getByLabel()>getByText()>getByPlaceholder()>locator()
POM Structure
Organise every POM with these clearly separated sections using comment banners:
import { Page, Locator, expect } from '@playwright/test';
// TODO: Import your project's base page class
import { BasePage } from './base.page';
export class FeaturePage extends BasePage {
constructor(page: Page) {
super(page);
}
// ==========================================
// LIFECYCLE (from BasePage)
// ==========================================
/**
* Navigates to the feature page and cleans up any stale data
* left by previous failed test runs.
*
* Steps:
* 1. Navigates to /feature via direct URL.
* 2. Waits for the page heading to be visible.
* 3. Deletes any items matching the test data pattern.
*/
async setUp(): Promise<void> {
// TODO: Implement navigation and cleanup
}
/**
* Cleans up any data created during the test suite.
*
* Steps:
* 1. Navigates to /feature.
* 2. Deletes all items created by this test run.
*/
async tearDown(): Promise<void> {
// TODO: Implement cleanup
}
// ==========================================
// LOCATORS
// ==========================================
/** The main heading of the feature page. */
get heading(): Locator {
return this.page.getByRole('heading', { name: 'Feature Name' });
}
/** The "Create" button that opens the creation modal. */
get createButton(): Locator {
return this.page.getByRole('button', { name: /create/i });
}
// TODO: Add locators for all interactive elements on this page
// ==========================================
// NAVIGATION
// ==========================================
/**
* Navigates directly to the feature page via URL.
*
* Steps:
* 1. Calls page.goto('/feature').
* 2. Asserts the page heading is visible.
* 3. Asserts the URL contains '/feature'.
*/
async navigateToPage(): Promise<void> {
await this.page.goto('/feature');
await expect(this.heading).toBeVisible();
await expect(this.page).toHaveURL(/\/feature/);
}
// ==========================================
// VERIFICATION
// ==========================================
/**
* Verifies an item appears in the list with the expected name.
*
* Steps:
* 1. Locates the item row by name text.
* 2. Asserts the row is visible.
*
* @param name - The expected item name
*/
async verifyItemExists(name: string): Promise<void> {
await expect(
this.page.getByRole('row', { name: new RegExp(name, 'i') })
).toBeVisible();
}
// ==========================================
// CREATE
// ==========================================
/**
* Creates a new item via the creation modal.
*
* Steps:
* 1. Clicks the "Create" button to open the modal.
* 2. Fills in the name field.
* 3. Clicks "Submit" and waits for the modal to close.
*
* @param name - Display name for the new item
*/
async createItem(name: string): Promise<void> {
await this.createButton.click();
const modal = this.page.getByRole('dialog');
await modal.getByLabel('Name').fill(name);
await modal.getByRole('button', { name: 'Submit' }).click();
await expect(modal).toBeHidden();
}
// ==========================================
// EDIT
// ==========================================
// TODO: Add edit methods
// ==========================================
// DELETE
// ==========================================
// TODO: Add delete methods
}
JSDoc Requirements
Every public method MUST have a JSDoc comment with:
- Summary line — what the method does
- Steps block — numbered list of what happens when called
- @param tags — for every parameter
- @returns tag — if the method returns a value
This is critical because LLMs read these comments to understand what actions are available and generate test specs from them.
/**
* Updates an existing item's name and value.
*
* Steps:
* 1. Locates the item row by its current name.
* 2. Clicks the "Edit" button within that row.
* 3. Clears and fills the name field with the new name.
* 4. Clears and fills the value field with the new value.
* 5. Clicks "Save" and waits for the edit modal to close.
* 6. Waits for the success toast notification to appear.
*
* @param currentName - The item's current display name (used to locate the row)
* @param newName - The new display name
* @param newValue - The new value
*/
async editItem(currentName: string, newName: string, newValue: string): Promise<void> {
Naming
- File:
e2e/poms/{feature}.page.ts(lowercase, kebab-case feature name) - Class:
{Feature}Page(PascalCase) - The feature name must match the corresponding spec and test-data files
See references/pom-template.md for the full boilerplate.