A familiar QA chore: someone shipped a feature, you need to add Playwright coverage, and the first hour goes into building a Page Object — clicking through the page, copying selectors, writing methods, naming locators. Playwright MCP collapses that hour into a few minutes. The assistant browses the page, identifies every interactive element by role and name, and emits a typed Page Object class with locators, action methods, and getters. This lesson covers the prompt template, the iterative refinement loop, and the review discipline that decides whether the generated POM lasts a quarter or breaks within a week.
The same caveat as every other lesson in this chapter: the AI output is a draft. The structure is right; the selectors need verification; the method names probably need to match your team's conventions. Treat reverse-engineering as a 5-minute scaffold step, not a 5-second checkbox.
A prompt template that works
Visit https://myapp.com/products/123 and analyse the page. Generate a Playwright
Page Object class called ProductDetailPage that:
- Uses getByRole, getByLabel, getByTestId locators (in that order of preference)
- Has methods for every interactive element (add to cart, change quantity, view reviews, expand specs)
- Has getter methods for every visible data field (name, price, stock status, ratings count)
- Returns Promises with proper TypeScript types
- Includes brief JSDoc on the class and on each public method
- Imports from @playwright/test only (no other deps)
The class should match the style of tests/pages/CartPage.ts in our repo — pasted below for reference.
[paste one existing POM file here]The pasted reference file is doing a lot of work. It anchors imports, naming, the constructor pattern, whether locators are public or private, and how methods are documented. The assistant matches the style line by line. Without the example, you get generic POM; with it, you get your POM.
What the assistant does behind the scenes
browser_navigateto the URL.browser_snapshot— the accessibility tree gives every interactive element with its role and accessible name.- The assistant scans the tree, classifies elements (interactive vs informational), and groups them by purpose.
- It writes the class — locator declarations in the constructor, action methods for the interactives, getter methods for the readables.
- It returns the code, often with a short explanation of which elements it identified and any it couldn't.
The selectors come from real elements with real roles, not from training-data guesses. That's the whole point of doing this through MCP rather than handing the URL to a chat-only LLM.
Example output
import { Locator, Page } from '@playwright/test';
/**
* Page Object for the Product Detail page.
* Covers product reads, quantity changes, cart actions, and the reviews section.
*/
export class ProductDetailPage {
readonly productName: Locator;
readonly price: Locator;
readonly stockStatus: Locator;
readonly addToCartButton: Locator;
readonly quantityInput: Locator;
readonly reviewsTab: Locator;
constructor(private page: Page) {
this.productName = page.getByRole('heading', { level: 1 });
this.price = page.getByTestId('product-price');
this.stockStatus = page.getByTestId('stock-status');
this.addToCartButton = page.getByRole('button', { name: 'Add to cart' });
this.quantityInput = page.getByLabel('Quantity');
this.reviewsTab = page.getByRole('tab', { name: 'Reviews' });
}
/** Set the quantity input to a numeric value. */
async setQuantity(qty: number): Promise<void> {
await this.quantityInput.fill(qty.toString());
}
/** Click the Add to Cart button. */
async addToCart(): Promise<void> {
await this.addToCartButton.click();
}
/** Open the Reviews tab. */
async openReviews(): Promise<void> {
await this.reviewsTab.click();
}
/** Read the product name from the page heading. */
async getProductName(): Promise<string> {
return (await this.productName.textContent())?.trim() ?? '';
}
/** Read the displayed price. */
async getPrice(): Promise<string> {
return (await this.price.textContent())?.trim() ?? '';
}
}A few things worth noticing about the output:
- Locators are declared
readonlyand built once in the constructor — the convention you'd write by hand. - The mix of
getByRole(heading, button, tab) andgetByTestId(price, stock) reflects what the page actually exposes; the assistant didn't force a single strategy. - Getters return strings with
?.trim()and the?? ''fallback, dodging the "textContentreturns string-or-null" pitfall. The model has seen this enough times to handle it cleanly. - Action methods are short and focused — one click per method, one fill per method. That's the right grain for composition in tests.
Iterating to fill the gaps
The first pass is usually 80% of the surface. Common follow-ups:
Add a method for the "Notify me" button that appears when the product is out of stock.
The element only renders when stockStatus is "Sold out" — make sure the locator handles that gracefully.
Also expose a method to expand the "Specifications" section, and a getter that
returns the spec list as Record<string, string>.The assistant snapshots again (the page state may have changed), locates the conditional element, and adds the methods. Repeat for any other interactives you care about. Three or four passes typically cover everything worth covering — and the diff between passes is small enough to review easily.
The reverse-engineering loop
Step 1 of 6
Visit the page
Prompt the assistant to navigate to the URL. Browser opens, page loads, snapshot is taken.
What to verify before committing
The generated POM compiles. That's not the same as it being right. Walk it through these checks:
- Are the test ids real? The assistant occasionally suggests
getByTestId('stock-status')because the role of an element looked like a stock status. Confirm by inspecting the live DOM — if the testid isn't actually there, the locator will fail at runtime. (Or better: add the testid in the app, since the AI told you what would be useful.) - Are roles unique?
getByRole('button', { name: 'Save' })is fine if there's one Save button on the page. If there are three (one per section), the locator matches all and Playwright will throw. Scope to a section:page.getByRole('region', { name: 'Profile' }).getByRole('button', { name: 'Save' }). - Do the method names match your team's POM style? Some teams use
clickAddToCart(), some useaddToCart(), some usecart.add(). Pick one in your reference file and the assistant will follow. - Are there interactions the snapshot couldn't see? Hover-only menus, conditional dialogs, drag-to-reorder. These need explicit prompts to include — the assistant won't infer them from a still snapshot.
A 5-minute review converts a 5-minute generation into a POM that lasts. Skip it and you've just paid the AI a tax in invisible debt.
Reference: where this fits in the broader course
If you haven't yet built a Page Object by hand, see the POM lesson in the Playwright with TypeScript course before relying on this workflow. Reviewing AI output is much harder when you don't know what good looks like.
⚠️ Common mistakes
- Treating the generated POM as finished. A class with seven methods feels comprehensive, but the page might have ten user-facing interactions. Walk the live page after the generation step and note anything the POM doesn't expose. The assistant only sees what's visible in the current snapshot.
- Letting the AI invent test ids that don't exist. Sometimes the model suggests
getByTestId('foo')because the element should have one, not because it does. Verify each testid in the live DOM. Better still, make the suggestion a backlog item — if the AI thinks an element should have a stable hook, your real users probably need it for accessibility too. - Generating a POM and never running it. Code that compiles can still be wrong. Wire the new POM into one real test and exercise every method before merging — locator mismatches surface in seconds, not weeks.
🎯 Practice task
Reverse-engineer a POM for a real page. 30 minutes.
- Pick a page in your app that doesn't yet have a Page Object — a settings screen, a product detail page, a multi-section dashboard.
- Pick one existing POM file in your repo as a style reference. Open it and re-read it briefly to internalise the conventions.
- Run the prompt template above against the chosen URL, with your existing POM pasted in as the style reference.
- Save the output to
tests/pages/. Open it next to the reference POM. Note three things that match the style and one thing that doesn't. Send a follow-up prompt to fix the mismatch. - Wire it in: write one new test that uses the generated POM end-to-end. Run it — every method, every getter. Anything that fails, debug against the live page, and update either the POM or the underlying app.
- Stretch: add a hover-only or conditional element to the page (or pick a page that already has one) and prompt the assistant to extend the POM to cover it. This is where pure-snapshot reverse-engineering needs explicit narration to fill the gap.
The next lesson flips back to maintenance: when an existing test breaks because a selector changed, AI can be the difference between 15 minutes of digging and 30 seconds of healing.