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.
