| |

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.