Selenium to Playwright Migration Part 4: Page Objects and Test Conversion
This is part of the Selenium to Playwright Migration Series. Follow the complete 7-part tutorial to migrate your test suite from Selenium Java to Playwright TypeScript.
๐ญ Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
The biggest structural change in migration: converting your Page Object Model, eliminating base classes, introducing the Module layer, and transforming your test structure from TestNG to Playwright’s built-in runner.
Contents
POM Conversion: LoginPage
Before (Selenium Java)
public class LoginPage extends CommonToAllPage {
@FindBy(id = "login-username")
private WebElement usernameInput;
@FindBy(id = "login-password")
private WebElement passwordInput;
@FindBy(id = "login-btn")
private WebElement loginButton;
@FindBy(id = "login-error")
private WebElement errorMessage;
public LoginPage(WebDriver driver) {
super(driver);
PageFactory.initElements(driver, this);
}
public void enterUsername(String username) {
waitForVisibility(usernameInput);
usernameInput.clear();
usernameInput.sendKeys(username);
}
public void enterPassword(String password) {
passwordInput.clear();
passwordInput.sendKeys(password);
}
public DashboardPage clickLogin() {
loginButton.click();
return new DashboardPage(driver);
}
}
After (Playwright TypeScript)
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.locator('[data-testid="login-error"]');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}
Key changes: No inheritance from base class. No PageFactory. No manual waits. Locators are lazy (resolved at action time). Semantic locators (getByLabel, getByRole) replace ID-based @FindBy.
Base Class Elimination
In Selenium, CommonToAllPage held the WebDriver instance, wait helpers, and screenshot utilities. In Playwright, all of this is built-in:
| Selenium Base Class Method | Playwright Built-in |
|---|---|
waitForVisibility(element) | Auto-wait (built into every action) |
takeScreenshot() | screenshot: 'only-on-failure' in config |
getDriver() | page fixture (injected automatically) |
scrollToElement() | Auto-scroll (built into click/fill) |
highlightElement() | Trace Viewer shows actions visually |
๐ Level Up Your Playwright
From locators to CI pipelines โ build a production-grade Playwright + TypeScript framework step by step.
The Module Layer (New in Playwright)
Playwright introduces a pattern Selenium rarely uses: Modules that compose multiple Page Objects into business flows.
// src/modules/LoginModule.ts
import { Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
export class LoginModule {
constructor(private page: Page) {}
async loginAsAdmin() {
const loginPage = new LoginPage(this.page);
await this.page.goto('/login');
await loginPage.login(
process.env.ADMIN_EMAIL!,
process.env.ADMIN_PASSWORD!
);
return new DashboardPage(this.page);
}
}
Test Structure Conversion
Before (TestNG)
@Test(groups = {"smoke"}, priority = 1)
public void testPositiveLogin() {
loginPage.enterUsername(PropertiesReader.readKey("username"));
loginPage.enterPassword(PropertiesReader.readKey("password"));
DashboardPage dashboard = loginPage.clickLogin();
Assert.assertTrue(dashboard.isDashboardDisplayed());
}
After (Playwright)
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login Tests', () => {
test('should login with valid credentials @smoke', async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto('/login');
await loginPage.login(
process.env.TEST_USERNAME!,
process.env.TEST_PASSWORD!
);
await expect(page.getByText('Dashboard')).toBeVisible();
await expect(page).toHaveURL(/dashboard/);
});
});
Next: Part 5: Waits, Retries, and Infrastructure โ converting WebDriverWait, retry analyzers, and screenshot listeners.
๐ Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
