| |

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

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.