FRAMEWORK DESIGNINTERMEDIATE

Stop Repeating Playwright Locators: Build Custom Locator Functions Like an Enterprise SDET

Learn how to design reusable Playwright custom locator wrapper functions and helper classes that keep tests clean, consistent, and maintainable across large enterprise automation frameworks.

iff Solution Academy July 28, 2026 12 min read Updated July 28, 2026
Playwright Custom Locators Locator Helper Wrapper Functions Page Object Model Enterprise Framework TypeScript Best Practices

Introduction

As your Playwright automation framework grows, you'll quickly notice a common problem—locator code starts appearing everywhere. Consider these examples:

typescript
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:

typescript
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:

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:

typescript
await button(page, 'Login').click();
await button(page, 'Save').click();
await button(page, 'Delete').click();

Much cleaner.

Wrapping Textboxes

typescript
export function textbox(page: Page, label: string): Locator {
  return page.getByRole('textbox', { name: label });
}

Usage:

typescript
await textbox(page, 'Username').fill('john');
await textbox(page, 'Password').fill('secret');

Notice how your tests now read almost like English.

typescript
export function link(page: Page, name: string): Locator {
  return page.getByRole('link', { name });
}

Usage:

typescript
await link(page, 'Products').click();

Wrapping Checkboxes

typescript
export function checkbox(page: Page, name: string): Locator {
  return page.getByRole('checkbox', { name });
}

Usage:

typescript
await checkbox(page, 'Remember Me').check();

Wrapping Dropdowns

typescript
export function dropdown(page: Page, label: string): Locator {
  return page.getByLabel(label);
}

Usage:

typescript
await dropdown(page, 'Country').selectOption('USA');

Wrapping Test IDs

Many enterprise applications standardize on data-testid.

typescript
export function testId(page: Page, id: string): Locator {
  return page.getByTestId(id);
}

Usage:

typescript
await testId(page, 'login-button').click();

Creating a Base Locator Class

Instead of exporting dozens of standalone functions, many teams create a reusable class.

utils/LocatorHelper.ts
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:

typescript
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

pages/LoginPage.ts
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.

typescript
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:

typescript
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:

typescript
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.

When Not to Wrap

Wrapper functions are a tool, not a religion. Applied without judgement, they produce a second framework that everyone has to learn on top of Playwright, and that is a worse outcome than a few repeated locator calls.

  • Do not wrap a Playwright method one-to-one if the wrapper adds nothing but a new name.
  • Do not wrap page-specific, one-off locators; those belong in the Page Object that owns them.
  • Do not hide assertions inside locator helpers — a function called button() should never also click and verify.
  • Do not create wrappers that accept ten optional parameters to cover every case; write two clear helpers instead.

A good rule of thumb: a wrapper earns its place when it is used in at least three different Page Objects, or when it encodes a project-wide convention such as a test-id naming scheme. Everything else is premature abstraction, and premature abstraction in a test framework is just as expensive as it is in production code.

Building a Testability Contract

Custom locator helpers are far more powerful when the application under test cooperates. Rather than writing increasingly clever CSS to reach fragile elements, agree on a testability contract with the development team and encode that contract in your helper layer.

The contract usually has three parts. First, every interactive element exposes a stable attribute — commonly data-testid — that is never changed for styling reasons. Second, the naming follows a predictable pattern such as area-component-action, for example checkout-submit-button. Third, dynamic lists expose an identifier on each row so tests can target a specific record instead of an index position.

Once that contract exists, a single helper can construct locators from semantic arguments. A method like testId('checkout', 'submit', 'button') centralises the naming rule, so if the team ever changes the attribute or the separator, you update one function instead of thousands of test lines. That is the real return on investment of a locator helper layer: it converts a global refactor into a local one.

Prefer Role-Based Locators First

Even with a testability contract in place, prefer Playwright's role-based locators wherever the accessible name is stable. Role locators fail when the application becomes inaccessible, which means your tests double as a lightweight accessibility check. Fall back to test ids for elements with no meaningful accessible name, such as icon-only controls and layout containers.

Migrating an Existing Suite

Most teams adopt locator helpers on a codebase that already has hundreds of tests. A big-bang rewrite is risky and rarely gets finished, so migrate incrementally instead.

  • Introduce the helper module first and use it only in new tests, so the pattern proves itself on low-risk work.
  • When a test fails because of a locator change, migrate that file as part of the fix rather than patching the old selector.
  • Pick the two or three most-edited Page Objects and convert them deliberately; they generate the most maintenance value.
  • Add a lint rule or code review checklist item that flags raw page.locator() calls in new Page Objects.
  • Track progress with a simple grep count in CI so the migration stays visible instead of stalling silently.

Expect the migration to surface duplicate Page Objects, dead tests, and selectors that never matched anything. That cleanup is part of the value. Teams that follow this incremental path typically report a large drop in the time it takes to fix a UI refactor, because a change to the markup now touches one helper rather than scattered selector strings.

Frequently Asked Questions

Should wrappers return Locator or perform actions?

Return a Locator. Returning the locator keeps Playwright's full API, auto-waiting, and assertion support available to the caller, and it prevents your helper layer from silently reimplementing behaviour Playwright already provides.

Where should the helper file live?

In a shared utilities or core folder alongside the base page, imported by Page Objects. Test files should generally use Page Objects rather than reaching for locator helpers directly.

Do wrappers slow down execution?

No. Building a Locator is a lazy, in-memory operation; nothing touches the browser until the locator is used. The overhead of a function call is irrelevant next to network and rendering time.

How do wrappers interact with the Page Object Model?

They sit underneath it. Page Objects describe what a screen contains and what a user can do there; locator helpers describe how any element is found. Keeping those responsibilities separate is what makes both layers easy to change.

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.

Playwright Framework Series

View all →
  1. 1Getting Started with Playwright: Installation, Setup, and Your First Test
  2. 2Playwright Locators: The Complete Guide with Real-World Examples
  3. 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
  4. 4Playwright Assertions Complete Guide: Web-First Assertions, Auto-Retry & Custom Matchers
  5. 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
  6. 6Playwright Fixtures: The Complete Guide with Real-World Examples
  7. 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
  8. 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
  9. 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
  10. 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
  11. 11Page Object Model with Playwright: A Practical Guide
  12. 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
  13. 13Building an Enterprise Playwright Framework from Scratch

API Testing Series

View all →
  1. 1Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
  2. 2Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
  3. 3Part 3: Mastering CRUD Operations in Playwright API Testing
  4. 4Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
  5. 5Part 5: Building an Enterprise-Level Playwright API Automation Framework
  6. 6Part 6: API Models, Schema Validation & Test Data Management in Playwright
  7. 7Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
  8. 8Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
  9. 9Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
  10. 10Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
  11. 11Playwright Backend Testing Tutorial – REST API, GraphQL, WebSocket, Kafka, Database & Microservices

Recommended Next Articles