Introduction
As your Playwright automation framework grows, you'll quickly notice a common problem—locator code starts appearing everywhere. Consider these examples:
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Submit' }).click();The problem isn't that these locators are wrong—they're actually good locators. The problem is that you're repeating the same locator-building logic hundreds of times throughout your framework.
Large enterprise automation frameworks solve this by creating custom locator wrapper functions. Instead of writing Playwright locators directly in every Page Object or test, they build reusable helper methods that make tests shorter, cleaner, easier to maintain, and more consistent.
In this tutorial, you'll learn how to design reusable locator wrapper functions, understand why enterprise teams use them, and build your own locator utility library.
Why Wrapper Functions?
Imagine your application has over 500 test cases, 120 Page Objects, and 3,000 locators. If your team decides to change its locator strategy—for example, moving from getByRole() to getByTestId()—you may need to update hundreds of files. With wrapper functions, you change the implementation in one place.
The Traditional Approach
Most beginners write code like this:
await page.getByRole('button', { name: 'Login' }).click();
await page.getByRole('button', { name: 'Register' }).click();
await page.getByRole('button', { name: 'Logout' }).click();Nothing is technically wrong with it. However, imagine doing this thousands of times across a large framework.
Creating Your First Wrapper
Create a new file at utils/locatorHelper.ts:
import { Page, Locator } from '@playwright/test';
export function button(page: Page, name: string): Locator {
return page.getByRole('button', { name });
}Now your test becomes:
await button(page, 'Login').click();
await button(page, 'Save').click();
await button(page, 'Delete').click();Much cleaner.
Wrapping Textboxes
export function textbox(page: Page, label: string): Locator {
return page.getByRole('textbox', { name: label });
}Usage:
await textbox(page, 'Username').fill('john');
await textbox(page, 'Password').fill('secret');Notice how your tests now read almost like English.
Wrapping Links
export function link(page: Page, name: string): Locator {
return page.getByRole('link', { name });
}Usage:
await link(page, 'Products').click();Wrapping Checkboxes
export function checkbox(page: Page, name: string): Locator {
return page.getByRole('checkbox', { name });
}Usage:
await checkbox(page, 'Remember Me').check();Wrapping Dropdowns
export function dropdown(page: Page, label: string): Locator {
return page.getByLabel(label);
}Usage:
await dropdown(page, 'Country').selectOption('USA');Wrapping Test IDs
Many enterprise applications standardize on data-testid.
export function testId(page: Page, id: string): Locator {
return page.getByTestId(id);
}Usage:
await testId(page, 'login-button').click();Creating a Base Locator Class
Instead of exporting dozens of standalone functions, many teams create a reusable class.
import { Locator, Page } from '@playwright/test';
export class LocatorHelper {
constructor(private page: Page) {}
button(name: string): Locator {
return this.page.getByRole('button', { name });
}
textbox(name: string): Locator {
return this.page.getByRole('textbox', { name });
}
link(name: string): Locator {
return this.page.getByRole('link', { name });
}
checkbox(name: string): Locator {
return this.page.getByRole('checkbox', { name });
}
}Usage:
const locator = new LocatorHelper(page);
await locator.button('Login').click();
await locator.textbox('Username').fill('john');This pattern centralizes locator creation and keeps Page Objects concise.
Using Wrapper Functions in a Page Object
export class LoginPage {
constructor(
private page: Page,
private locator = new LocatorHelper(page)
) {}
async login(username: string, password: string) {
await this.locator.textbox('Username').fill(username);
await this.locator.textbox('Password').fill(password);
await this.locator.button('Login').click();
}
}Now your Page Object focuses on business actions instead of locator details.
Adding Logging
Wrapper functions are also a great place to add logging.
button(name: string): Locator {
console.log(`Finding button: ${name}`);
return this.page.getByRole('button', { name });
}Every locator lookup is now automatically logged without changing any test code.
Adding Automatic Waiting
You can extend wrapper functions to wait for visibility before returning or interacting with elements. For example:
async clickButton(name: string) {
const button = this.page.getByRole('button', { name });
await button.waitFor();
await button.click();
}This keeps synchronization logic consistent across the framework.
Supporting Multiple Locator Strategies
Suppose your team decides to switch to data-testid. Instead of updating hundreds of Page Objects, you only modify the helper:
button(name: string): Locator {
return this.page.getByTestId(name);
}Every test continues to work without changes.
Advantages
Using custom locator wrappers provides:
- Cleaner code
- Reusable locator logic
- Centralized maintenance
- Consistent naming conventions
- Easier debugging
- Better logging
- Faster framework evolution
- Simpler onboarding for new team members
These are some of the reasons enterprise automation frameworks remain maintainable as they grow.
Best Practices
- Keep wrapper methods focused on a single responsibility.
- Return Locator objects so callers can choose the action (click, fill, check, etc.).
- Avoid hiding too much Playwright functionality—wrappers should simplify, not limit.
- Use descriptive method names such as button(), textbox(), and link().
- Document the preferred locator strategy so the whole team follows the same pattern.
- Combine wrapper functions with the Page Object Model for maximum maintainability.
Common Mistakes
- Creating wrappers that duplicate every Playwright API without adding value.
- Embedding business logic inside locator helpers.
- Returning ElementHandle instead of Locator.
- Mixing multiple locator strategies inconsistently across the framework.
- Making wrapper methods too generic to understand.
Conclusion
Custom locator wrapper functions are a simple idea with a significant impact. They reduce repetition, enforce consistency, and make it much easier to evolve your automation framework over time. Whether you're building a small test suite or an enterprise framework with thousands of tests, centralizing locator creation pays off in maintainability and readability.
The key is to keep your wrappers lightweight and let Playwright's powerful Locator API do the heavy lifting. When combined with the Page Object Model and good framework design, custom locator helpers become an important building block of a professional Playwright automation framework.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.
Playwright Framework Series
View all →- 1Getting Started with Playwright: Installation, Setup, and Your First Test
- 2Playwright Locators: The Complete Guide with Real-World Examples
- 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
- 4Playwright Assertions: The Complete Guide with Real-World Examples
- 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
- 6Playwright Fixtures: The Complete Guide with Real-World Examples
- 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
- 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
- 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
- 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
- 11Page Object Model with Playwright: A Practical Guide
- 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
- 13Building an Enterprise Playwright Framework from Scratch
API Testing Series
View all →- 1Playwright API Testing: The Complete Guide with Real-World Examples
- 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
- 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
- 4Part 3: Mastering CRUD Operations in Playwright API Testing
- 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
- 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
- 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
- 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
- 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
- 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
- 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
- 12API Testing with Playwright: Request Context, Auth, and Assertions