From Selenium PageFactory to Playwright Locators: The Migration Guide for Experienced Engineers

Pratik Sarkar, an Automation Engineer working with both Playwright and Selenium, recently posted a visual comparison that crystallized a problem I see in every Selenium-to-Playwright migration: experienced engineers try to translate their PageFactory patterns directly into Playwright, and the results are brittle, verbose, and miss Playwright’s core advantages. The conceptual shift from WebElement to Locator is not just a syntax change — it is a fundamentally different approach to element interaction that requires unlearning as much as learning.

SURIYAPRAKASH R, an AI and Tech Mentor who coaches full-stack SDETs, put it simply: “If you are a Selenium tester, the easiest way to understand Playwright is to stop thinking like a Selenium tester.” That advice sounds flippant but contains a crucial insight. Playwright is not Selenium with a different API. It is built on different architectural assumptions that require different design patterns.

Contents

The Conceptual Shift: WebElement vs Locator

In Selenium, findElement() immediately queries the DOM and returns a WebElement — a direct reference to a specific DOM node at that moment in time. If the DOM changes after you get the reference (the element is removed and re-added, the page re-renders), your WebElement becomes stale and throws a StaleElementReferenceException. PageFactory’s @FindBy annotations add lazy initialization but the same fundamental behavior — the element is resolved at the moment of first use and can become stale.

Playwright’s Locator is fundamentally different. A Locator does not query the DOM when you create it. It is a description of how to find an element — a recipe, not a reference. Every time you interact with a Locator (click it, read its text, assert on it), Playwright freshly queries the DOM at that moment, waits for the element to meet actionability criteria, and then performs the action. There is no stale element concept because there is no cached element reference.

This difference has profound implications for test stability. In Selenium, you constantly manage element freshness — re-finding elements after page transitions, adding waits before interactions, catching and retrying stale element exceptions. In Playwright, the framework handles all of this automatically. You write page.locator('#submit') once and use it throughout your test. It resolves correctly every time, regardless of how many re-renders occurred between interactions.

PageFactory Does Not Exist — And That Is a Good Thing

Selenium’s PageFactory pattern initializes element references when the page object is created. In Java: PageFactory.initElements(driver, this) walks through your class, finds every @FindBy annotation, and creates proxy WebElements for each. This was a clever solution to the verbosity of repeated findElement() calls, but it introduced a lifecycle management problem — if the page re-renders, those proxy elements can become stale.

Playwright does not need PageFactory because Locators are inherently lazy and self-refreshing. In your Playwright page object, you declare locators as readonly properties: readonly submitButton = this.page.locator('#submit'). No initialization step. No proxy mechanism. No stale element risk. The page object becomes simpler, more readable, and more reliable because the framework’s architecture eliminated the problem that PageFactory was designed to solve.

Code Transformation Patterns

The transformation from Selenium page objects to Playwright page objects follows consistent patterns. Selenium’s @FindBy(id = "username") WebElement usernameField; becomes Playwright’s readonly usernameField = this.page.locator('#username');. Selenium’s usernameField.sendKeys("admin") becomes await this.usernameField.fill('admin');. Selenium’s new WebDriverWait(driver, 10).until(ExpectedConditions.visibilityOf(element)) simply disappears — Playwright’s auto-waiting makes explicit waits unnecessary for standard interactions.

The most impactful transformation is in assertion patterns. Selenium assertions are typically immediate: assertEquals(element.getText(), "expected") — if the text has not loaded yet, the assertion fails. Playwright assertions are auto-retrying: await expect(this.messageElement).toHaveText('expected') — the assertion retries until the text matches or the timeout expires. This single change eliminates the most common source of timing-related test flakiness.

Common Migration Mistakes

The most common mistake is adding explicit waits that Playwright does not need. Engineers accustomed to Selenium’s wait patterns add await page.waitForSelector() before every interaction. Playwright’s auto-waiting makes this redundant — it adds unnecessary code and can actually slow down tests by waiting twice (once explicitly, once via auto-wait).

The second mistake is using page.$() instead of page.locator(). The dollar-sign method returns an ElementHandle (similar to a WebElement) which does not auto-wait or auto-retry. Engineers familiar with Selenium gravitate toward it because it feels familiar, but it reintroduces exactly the fragility problems they migrated away from.

The third mistake is over-using CSS selectors. Selenium engineers are conditioned to write CSS selectors because Selenium’s CSS support is strong. Playwright offers role-based locators (page.getByRole('button', { name: 'Submit' })) and text-based locators (page.getByText('Welcome back')) that are more readable, more resilient to UI changes, and closer to how users interact with the page. Prefer these over CSS selectors whenever possible.

The Honest Caveats

The migration is not purely additive — you lose some things. Selenium’s extensive community of Java and C# developers, its mature integration with tools like TestNG and Allure, and its deep compatibility with cloud testing platforms are genuine advantages. Playwright’s TypeScript-first ecosystem is excellent but narrower. If your team is deeply invested in Java, the TypeScript learning curve is a real cost beyond just learning Playwright’s API.

The page object transformations I described are the straightforward cases. Complex Selenium patterns — custom WebElement extensions, decorator patterns around driver interactions, sophisticated wait chains for complex AJAX flows — require more thoughtful redesign rather than mechanical transformation.

The complete Selenium-to-Playwright migration path — including code transformation guides, migration checklists, and side-by-side pattern comparisons — is covered in Module 3 of my AI-Powered Testing Mastery course.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.