Building Page Objects with Claude Code

9 min read

Page Objects are the right abstraction for almost every UI test suite — they isolate selectors from test logic, make tests readable, and reduce maintenance cost when the UI changes. Building them is also time-consuming work that Claude Code handles well, especially when Playwright MCP gives it access to the real page.

Two paths to a Page Object

Path 1 — with Playwright MCP (preferred): Claude Code navigates the real page, analyses the DOM, and generates locators from what it actually finds. Selectors are real. This requires Playwright MCP to be connected — covered in Chapter 4 of this course and in the Playwright MCP course.

Visit https://staging.myapp.com/products/123 and analyse the page.
 
Generate a Playwright Page Object:
- Save to src/pages/ProductDetailPage.ts
- Use getByRole, getByLabel, getByTestId in that preference order
- Methods: addToCart(quantity), selectVariant(variantName), expandReviews()
- Getters: productName, currentPrice, stockStatus
- TypeScript, constructor takes only page

Path 2 — from source code: When you cannot connect Playwright MCP, point Claude Code at the application source instead of pasted HTML:

The React component for the product detail page is at src/components/ProductDetail.tsx.
Read it and generate a Playwright Page Object for testing.
 
Save to src/pages/ProductDetailPage.ts.
Match the style of the other page objects in src/pages/.

Source code is more reliable than HTML snapshots because it shows the data-testid values the developers added — not what a browser happened to render at one point in time.

Structuring Page Objects for real applications

For pages with multiple distinct sections, generate component-level objects rather than one large class:

The order history page has a search bar, a filter panel, and a results table.
Each is a standalone component in the source.
 
Generate three objects:
- src/pages/components/OrderSearchBar.ts
- src/pages/components/OrderFilterPanel.ts
- src/pages/OrderHistoryPage.ts (composes the above two)
 
Read the existing page objects in src/pages/ to match the composition pattern.

This mirrors the component hierarchy. When the filter panel changes, only OrderFilterPanel.ts needs updating.

What to include in the prompt

Good Page Object prompts specify:

  • File path and class name — where it lives
  • Locator strategy — which approach to prefer (getByRole vs getByTestId vs CSS)
  • Methods to expose — name the user-facing actions, not the implementation
  • Getters for readable state — values the test needs to assert on
  • An existing example to match — always point at one existing page object for style

What to leave out: assertions (they belong in tests, not page objects), and overly specific implementation detail (name the method addToCart, let Claude Code decide how to implement it).

After generation — verifying selectors

Generated Page Objects need the same review as generated tests, plus one specific check: verify selectors against the real page.

Paste three or four locators from the generated file into your browser's DevTools console and confirm they find elements. For getByRole('button', { name: 'Add to cart' }), check that a button with exactly that accessible name exists on the page.

If a selector is wrong, fix it precisely:

> The generated POM uses getByRole('button', { name: 'Add to cart' }). 
  The actual button text is 'Add to Bag'. Fix ProductDetailPage.ts.

For Selenium/Java POMs, the same workflow applies with @FindBy annotations:

Generate a Selenium WebDriver Page Object in Java for the same page.
Use @FindBy with id, css, or xpath. Include the @PageFactory.initElements pattern.
Extend BasePage from src/test/java/com/myapp/base/BasePage.java.

⚠️ Common Mistakes

  • Putting assertions in Page Objects. A Page Object that calls expect(this.productName).toBe('...') mixes responsibilities. Assertions belong in tests. Page Objects expose state and actions — that is all.
  • Generating a POM without verifying selectors. A Page Object with broken locators is worse than no Page Object — it fails in ways that look like test logic bugs. Always verify a few selectors before committing.
  • One massive Page Object per page. A 300-method class for a complex page is a maintenance burden. Split by component or by user journey, not by page URL.

🎯 Practice Task

Build a Page Object for a real page. 20–30 minutes.

  1. Pick a page in a project you test that currently has inline selectors scattered across test files.
  2. Ask Claude Code to read one existing Page Object as a style reference (if you have one).
  3. Use Path 1 (Playwright MCP) or Path 2 (component source) to generate the POM. Be specific about the methods and getters you need.
  4. Verify three or four of the generated selectors against the real page in DevTools.
  5. Write one test that uses the generated POM for a happy-path scenario.

The next lesson covers the part of test setup that is arguably more tedious than writing tests: generating and managing test data and fixtures.

// tip to track lessons you complete and pick up where you left off across devices.