Selenium to Playwright Migration

If you have spent years building Selenium frameworks in Java and are now looking at Playwright, the hardest part is not learning Playwright — it is un-learning Selenium habits.

Almost everything you do in Selenium has a direct Playwright equivalent, and in most cases, the Playwright version is shorter because the framework does work you previously had to handle manually.

This guide maps each major Selenium concept to its Playwright counterpart, side by side. It is written from the perspective of someone who ran both in real projects: I initially used Selenium on a dynamic web application, faced persistent flaky-test issues on dynamic pages, and migrated the critical flows — login, payment, and multi-user testing — to Playwright.

Parallel execution improved regression speed significantly. This guide is the mapping I wish I had at the start.

Advertisement

Why Teams Migrate — and Why Some Shouldn't

The three Selenium pain points that drive most migrations are:

  • Flaky tests due to manual waits. Every Selenium engineer has fought synchronization — implicit waits, explicit waits, fluent waits, and the occasional shameful Thread.sleep().
  • Modern JavaScript frameworks are difficult to handle. Single-page applications built with React or Angular can re-render the DOM constantly, and Selenium often requires additional waiting logic to cope with these dynamic changes.
  • Parallel execution setup is complex. Selenium Grid or TestNG parallel configuration works, but it requires additional setup and careful driver management.

Playwright addresses all three: auto-waiting reduces synchronization-related failures, SPAs and AJAX-heavy pages are handled dynamically, and parallel execution works naturally through isolated browser contexts.

That said, Selenium is not "wrong." It remains widely used in legacy projects, supports many enterprise environments, and can still be the right choice when your suite is stable and your team is Java-native.

Migration is a cost you should justify — not a default.

The rest of this guide assumes you have decided to migrate, or want to evaluate the move properly.

The Big-Picture Comparison

Feature Selenium Playwright
Browser support Chrome, Firefox, IE, Edge Chromium, Firefox, WebKit
Language support Java, Python, C#, JavaScript TypeScript/JavaScript, Python, Java, C#
Auto-wait / synchronization Needs explicit waits and synchronization logic Built-in auto-wait
Single Page Applications (SPA) Requires additional synchronization for dynamic pages Handles dynamic applications efficiently
Cross-browser testing Supported, often through Grid or cloud infrastructure Chromium, Firefox, and WebKit supported directly
Installation / setup WebDriver/browser-driver setup traditionally required No separate WebDriver required; Playwright manages browser binaries
Modern web features Supported with additional synchronization and tooling Strong support for AJAX, SPA, shadow DOM, and Web Components

Suggested image: A two-column architecture diagram with Selenium on the left and Playwright on the right. Show the driver layer in the Selenium architecture and direct browser communication in the Playwright architecture.

Architecture: What Actually Changes Under the Hood

Selenium

Selenium WebDriver architecture involves your test script communicating through the WebDriver API and browser-specific driver infrastructure before reaching the browser.

Think of it like sending a letter through a postman:

Test script → WebDriver API → ChromeDriver → Browser

In newer Selenium implementations, communication follows the W3C WebDriver protocol. Commands are sent through the WebDriver layer to the browser.

Playwright

Playwright removes the need for a separately managed browser driver.

You do not need to download ChromeDriver, match its version to Chrome, or configure:

System.setProperty("webdriver.chrome.driver", "...");

Playwright installs and manages its supported browser binaries using commands such as:

npx playwright install

The conceptual difference is:

Test script → Playwright → Browser

The separate driver-management layer is gone.

What This Means for Your Migration

All the driver-management code in your Selenium framework can usually be removed:

  • System.setProperty("webdriver.chrome.driver", ...)
  • WebDriverManager dependencies
  • Driver-version mismatch troubleshooting
  • Driver executable configuration
  • Browser-driver lifecycle management

Delete the concept, not just the code.

Setup and Project Structure

Selenium — Java

A typical Selenium Java project commonly contains:

  • Maven
  • pom.xml
  • TestNG
  • testng.xml
  • src/main/java
  • src/test/java
  • Page Object classes
  • Utility classes
  • Driver management
  • Reporting configuration

The pom.xml manages dependencies and plugins, while TestNG controls test execution.

Playwright — TypeScript

Playwright typically uses a Node.js project structure.

You can scaffold a project with:

npm init playwright@latest

The playwright.config.ts file centralizes configuration such as:

  • Browser projects
  • Base URL
  • Workers
  • Retries
  • Timeouts
  • Reporters
  • Screenshots
  • Videos
  • Traces

Selenium to Playwright Setup Mapping

Selenium Playwright
pom.xml package.json
testng.xml playwright.config.ts
Maven Surefire Playwright Test Runner
mvn test npx playwright test
TestNG @Test test('name', async ({ page }) => {})
@BeforeMethod test.beforeEach()
@AfterMethod test.afterEach()
TestNG groups Tags/projects
TestNG parallel execution Playwright workers

Locators: From findElement() to getByRole()

Selenium

Locators identify elements using strategies such as:

  • ID
  • Name
  • Class name
  • Tag name
  • Link text
  • Partial link text
  • CSS selector
  • XPath

Many Selenium projects rely heavily on CSS selectors and relative XPath, with XPath axes used for complex relationships between elements.

// Selenium
WebElement loginBtn =
    driver.findElement(By.xpath("//button[text()='Login']"));

loginBtn.click();

Playwright

Playwright supports CSS and XPath, but the recommended strategy is different.

Prefer user-facing locators such as:

  • getByRole()
  • getByText()
  • getByLabel()
  • getByPlaceholder()

For example:

// Recommended
await page.getByRole('button', { name: 'Login' }).click();

// Good
await page.getByText('Login').click();

// XPath still works
await page.locator("//button[text()='Login']").click();

Migration Tip: Port Locator Intent, Not Locator Syntax

Do not mechanically convert every XPath.

If your Selenium locator is:

//input[@id='username']

and the field has an accessible label, the Playwright version can be:

await page.getByLabel('Username').fill('naveed');

This is generally more resilient than carrying the old XPath into the new framework.

Keep XPath for genuinely difficult cases where there is no stable attribute or accessible name.

findElement() vs findElements()

Selenium distinguishes between:

driver.findElement(...)

and:

driver.findElements(...)

The first returns a single element and throws an exception when it cannot find one. The second returns a collection.

Playwright uses a lazy Locator model:

const buttons = page.getByRole('button');

You can then use:

await buttons.count();

or:

await buttons.first().click();

or:

const allButtons = await buttons.all();

The locator itself does not immediately search for an element. Resolution happens when you perform an operation or assertion.

Waits and Synchronization: The Biggest Mindset Shift

This is the section that matters most because synchronization is where many Selenium suites lose stability.

Selenium

Selenium gives you several waiting mechanisms.

Implicit Wait

A global timeout applied when locating elements:

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

Explicit Wait

A condition-based wait using WebDriverWait and ExpectedConditions:

WebDriverWait wait =
    new WebDriverWait(driver, Duration.ofSeconds(10));

wait.until(ExpectedConditions.alertIsPresent());

Fluent Wait

A configurable wait that controls both timeout and polling:

Wait<WebDriver> wait = new FluentWait<>(driver)
    .withTimeout(Duration.ofSeconds(30))
    .pollingEvery(Duration.ofSeconds(5))
    .ignoring(Exception.class);

Thread.sleep()

The fixed wait:

Thread.sleep(5000);

It always waits the full five seconds, even if the element becomes ready after one second.

Playwright

Playwright replaces much of this manual synchronization with auto-waiting.

For example:

await page.getByRole('button', { name: 'Login' }).click();

Before clicking, Playwright automatically waits for the element to satisfy the action's required conditions.

Selenium Wait vs Playwright

Selenium Playwright
Implicit wait Usually nothing — auto-wait handles action readiness
visibilityOf() expect(locator).toBeVisible()
elementToBeClickable() Auto-wait built into .click()
FluentWait polling Web-first assertions and targeted waits
Thread.sleep() Avoid it; use condition-based waits instead
Page-load wait await page.waitForLoadState()

Example:

await expect(
    page.getByRole('button', { name: 'Login' })
).toBeVisible();

The Honest Caveat

Auto-wait removes a large amount of synchronization code, but not every possible wait.

For example, if you are waiting for a backend-driven state change with no immediate UI signal, you may need:

await expect(locator).toHaveText('Completed');

or:

await page.waitForResponse('**/api/payment');

The goal is not "never wait."

The goal is wait for a meaningful condition instead of waiting for an arbitrary amount of time.

Selenium

HTML <select> elements are commonly handled using Selenium's Select class:

Select dropdown =
    new Select(driver.findElement(By.id("country")));

dropdown.selectByVisibleText("India");

Selenium's Select class provides methods such as:

  • selectByVisibleText()
  • selectByValue()
  • selectByIndex()
  • getOptions()
  • isMultiple()
  • deselectAll()

Playwright

Playwright does not require a wrapper class.

await page.locator('#country')
    .selectOption({ label: 'India' });

By value:

await page.locator('#country')
    .selectOption('IN');

Multiple values:

await page.locator('#country')
    .selectOption(['IN', 'US']);

Custom Dropdowns

A custom dropdown implemented using <div>, <ul>, or other elements is not a native <select>.

Therefore:

selectOption()

will not work with it.

The approach remains:

  1. Click the dropdown.

  2. Locate the option.

  3. Click the option.

This is one Selenium concept that transfers almost unchanged.

Alerts: switchTo().alert() → Dialog Events

Selenium

Selenium switches to the browser alert:

Alert alert = driver.switchTo().alert();

String alertText = alert.getText();

alert.accept();
alert.dismiss();
alert.sendKeys("text");

If the alert may appear asynchronously, you might wait for it:

wait.until(ExpectedConditions.alertIsPresent());

Playwright

Playwright uses the Dialog API.

Register the handler before triggering the dialog:

page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.accept();
});

await page.getByRole('button', { name: 'Delete' }).click();

You can also dismiss it:

page.on('dialog', async dialog => {
    await dialog.dismiss();
});

For prompt dialogs:

page.on('dialog', async dialog => {
    await dialog.accept('My response');
});

Migration Trap

The ordering is different.

In Selenium, the common mental model is:

Click → Alert appears → Switch to alert → Accept/Dismiss

In Playwright:

Register dialog handler → Click → Dialog handler executes

If no dialog handler is registered, Playwright automatically dismisses dialogs.

Therefore, when migrating confirmation-dialog tests, make sure the intended dialog behavior is explicitly registered.

Frames: switchTo().frame() → frameLocator()

Selenium

Selenium switches into a frame:

driver.switchTo().frame(0);

By name or ID:

driver.switchTo().frame("iframe1");

By WebElement:

driver.switchTo().frame(frameElement);

After interacting with the frame:

driver.switchTo().defaultContent();

Forgetting to return to the main document is a common source of Selenium test failures.

Playwright

Playwright uses frameLocator().

const frame = page.frameLocator('#iframe1');

await frame
    .getByRole('textbox', { name: 'Card number' })
    .fill('4111...');

You can continue interacting with the main page without switching context:

await page.getByRole('button', { name: 'Pay' }).click();

There is no need to maintain a global "current frame" state.

Windows, Tabs, and Popups: Window Handles → Pages

Selenium

Selenium uses window handles:

String originalWindow = driver.getWindowHandle();

driver.getWindowHandles();

You typically iterate through handles and switch:

driver.switchTo().window(handle);

This can become cumbersome when multiple tabs or windows are involved.

Playwright

Playwright treats browser tabs and popups as Page objects.

const [popup] = await Promise.all([
    page.waitForEvent('popup'),
    page.getByRole('link', { name: 'Open report' }).click(),
]);

await popup.getByRole('heading').isVisible();

Both objects remain available:

page
popup

No window-handle iteration is required.

HTTP Basic Authentication

Playwright can handle HTTP basic authentication through browser-context configuration:

use: {
    httpCredentials: {
        username: 'user',
        password: 'password'
    }
}

This removes the need for some of the workarounds traditionally used with Selenium.

True OS-native dialogs remain outside the browser automation scope of both tools.

File Upload: sendKeys() → setInputFiles()

Selenium

For an HTML file input:

driver.findElement(By.id("upload"))
    .sendKeys("/path/to/file.pdf");

When the application opens a native OS file dialog instead, Selenium projects may require tools such as Robot or AutoIt.

Playwright

Playwright provides a direct API:

await page.locator('#upload')
    .setInputFiles('/path/to/file.pdf');

Multiple files:

await page.locator('#upload')
    .setInputFiles([
        'a.pdf',
        'b.pdf'
    ]);

For applications that trigger a file chooser:

const fileChooserPromise =
    page.waitForEvent('filechooser');

await page.getByRole('button', { name: 'Upload' }).click();

const fileChooser = await fileChooserPromise;

await fileChooser.setFiles('/path/to/file.pdf');

This is another area where Playwright can eliminate external desktop-automation work.

Actions Class → Built-In Locator Methods

Selenium

Mouse and keyboard interactions commonly use the Actions class:

Actions actions = new Actions(driver);

actions.contextClick(element)
    .build()
    .perform();

actions.dragAndDrop(source, target)
    .build()
    .perform();

actions.moveToElement(menu)
    .build()
    .perform();

element.sendKeys(Keys.ENTER);

element.sendKeys(Keys.SHIFT, Keys.TAB);

Playwright

Many common actions are directly available on locators:

await locator.click({ button: 'right' });

await source.dragTo(target);

await locator.hover();

await locator.press('Enter');

await locator.press('Shift+Tab');

For coordinate-based mouse actions, Playwright provides the mouse API:

await page.mouse.move(100, 100);
await page.mouse.down();
await page.mouse.move(300, 300);
await page.mouse.up();

Common Mapping

Selenium Playwright
contextClick() click({ button: 'right' })
dragAndDrop() dragTo()
moveToElement() hover()
Keys.ENTER press('Enter')
Keys.SHIFT + TAB press('Shift+Tab')

JavascriptExecutor → page.evaluate()

Selenium

JavascriptExecutor is commonly used as an escape hatch:

JavascriptExecutor js =
    (JavascriptExecutor) driver;

js.executeScript(
    "arguments[0].click();",
    element
);

js.executeScript(
    "window.scrollBy(0,500)"
);

It is often used for:

  • Force-clicking
  • Scrolling
  • Reading hidden values
  • Manipulating DOM attributes
  • Executing custom JavaScript

Playwright

Playwright provides evaluate():

await page.evaluate(() => {
    window.scrollBy(0, 500);
});

You can evaluate against a locator:

const text = await locator.evaluate(
    element => element.textContent
);

But Use It Less

This is an important migration mindset.

Do not automatically convert every Selenium JavascriptExecutor call to page.evaluate().

Many Selenium JavaScript workarounds exist because the automation framework needed help with:

  • Scrolling
  • Element readiness
  • Clickability
  • Synchronization

Playwright handles many of these automatically.

If your migrated suite contains the same amount of JavaScript execution as the old Selenium suite, you may simply be carrying old workarounds into the new framework.

Assertions: assert / verify → Web-First expect()

Selenium/TestNG

Traditional TestNG assertions evaluate the value at the time the assertion is executed.

For example:

Assert.assertEquals(
    element.getText(),
    "Payment successful"
);

If the application has not finished rendering the text yet, the assertion can fail even though the application eventually reaches the expected state.

TestNG also supports soft assertions through SoftAssert.

Playwright

Playwright uses expect().

await expect(
    page.getByRole('alert')
).toHaveText('Payment successful');

The assertion automatically retries until the condition is satisfied or the timeout is reached.

Selenium/TestNG → Playwright

Selenium/TestNG Playwright
Assert.assertEquals(el.getText(), "...") await expect(locator).toHaveText('...')
Assert.assertTrue(el.isDisplayed()) await expect(locator).toBeVisible()
isSelected() await expect(locator).toBeChecked()
SoftAssert expect.soft()
Hard assertion expect()

Migration Payoff

A significant amount of Selenium synchronization exists only to make assertions safe.

In Playwright:

await expect(locator).toHaveText('Completed');

acts as both:

Assertion + Wait

This is one of the biggest changes in the migration.

Parallel Execution: Grid/TestNG Parallel → Workers

Selenium

Selenium commonly uses two approaches.

Selenium Grid

Selenium Grid distributes tests across multiple machines or browser environments.

TestNG Parallel Execution

TestNG can configure parallel execution through testng.xml:

<suite parallel="methods" thread-count="4">

Parallel Selenium execution often requires careful handling of:

  • ThreadLocal<WebDriver>
  • Shared test data
  • Shared browser state
  • Driver lifecycle
  • Thread safety

Playwright

Playwright uses worker processes for parallel test execution.

Example:

// playwright.config.ts

export default defineConfig({
    workers: 4,
    fullyParallel: true
});

Each worker receives isolated browser contexts.

That means:

  • Cookies are isolated.
  • Local storage is isolated.
  • Sessions are isolated.
  • Browser state is isolated.

Cross-Browser Testing

Instead of configuring separate Grid nodes, Playwright can define browser projects:

projects: [
    {
        name: 'chromium',
        use: { ...devices['Desktop Chrome'] }
    },
    {
        name: 'firefox',
        use: { ...devices['Desktop Firefox'] }
    },
    {
        name: 'webkit',
        use: { ...devices['Desktop Safari'] }
    }
]

Cross-browser testing and parallel execution can therefore be managed from the same configuration.

Page Object Model and Page Factory → POM and Fixtures

Selenium

Page Object Model is a common Selenium design pattern.

Page Factory adds Selenium-specific annotations such as:

@FindBy(id = "username")
WebElement username;

and initialization through:

PageFactory.initElements(driver, this);

Playwright

POM transfers naturally to plain classes.

There is no need for @FindBy or PageFactory.initElements().

import { Page } from '@playwright/test';

export class LoginPage {
    constructor(private page: Page) {}

    usernameInput = () =>
        this.page.getByLabel('Username');

    loginButton = () =>
        this.page.getByRole('button', {
            name: 'Login'
        });

    async login(user: string, pass: string) {
        await this.usernameInput().fill(user);

        await this.page
            .getByLabel('Password')
            .fill(pass);

        await this.loginButton().click();
    }
}

Fixtures Replace Framework Plumbing

Fixtures provide dependency injection for tests.

For example:

export const test = base.extend<{
    loginPage: LoginPage
}>({
    loginPage: async ({ page }, use) => {
        await use(new LoginPage(page));
    }
});

Then:

test('valid login', async ({ loginPage }) => {
    await loginPage.login(
        'naveed',
        'secret'
    );
});

The overall framework architecture remains familiar:

Pages
Tests
Utilities
Config
Test Data
Reports

The major change is how these layers are connected.

Selenium frameworks often rely heavily on inheritance, annotations, and driver managers.

Playwright encourages fixtures and dependency injection.

What You Gain That Has No Direct Selenium Equivalent

These are not simply migrations. They are capabilities that can significantly change how your automation framework works.

Network Interception

Playwright can intercept, modify, mock, or block network requests using page.route().

For example:

await page.route('**/api/payment', async route => {
    await route.fulfill({
        status: 500,
        body: JSON.stringify({
            error: 'Payment failed'
        })
    });
});

This allows you to test scenarios such as:

  • API failures
  • Slow APIs
  • Payment failures
  • Network errors
  • Mock responses
  • Blocking analytics
  • Blocking unnecessary resources

Traditional Selenium WebDriver does not provide this network interception capability directly.

Storage State — Reuse Login Sessions

Playwright's storageState can save cookies and local storage after authentication.

This allows later tests to reuse the authenticated state instead of performing the login UI repeatedly.

Conceptually:

Login once
    ↓
Save storage state
    ↓
Reuse state
    ↓
Tests start already authenticated

This can significantly reduce test execution time.

Trace Viewer

Playwright's trace functionality can capture:

  • DOM snapshots
  • Screenshots
  • Network activity
  • Console information
  • Actions
  • Timing information

For example:

use: {
    trace: 'on-first-retry'
}

You can then inspect the trace using:

npx playwright show-trace trace.zip

This can replace a large portion of the traditional:

Screenshot + logs + console + manual reproduction

debugging workflow.

What Does NOT Transfer — The Honest Section

Migration is not simply replacing Selenium syntax with Playwright syntax.

Some parts of your existing ecosystem need to be redesigned.

1. Your Language

If your current framework is entirely Java-based, moving to Playwright with TypeScript is also a language migration.

Playwright supports:

  • TypeScript/JavaScript
  • Java
  • Python
  • C#

However, the Playwright Test ecosystem, fixtures, configuration patterns, and much of the community content are centered around TypeScript/JavaScript.

Budget for the language-learning component if moving from Selenium Java to Playwright TypeScript.

2. Apache POI / Excel-Driven Data

If your Selenium framework uses Apache POI for Excel-based test data, that utility layer does not directly transfer.

Node.js projects commonly use:

  • JSON
  • CSV
  • xlsx
  • Custom data fixtures

The data-driven testing concept transfers, but the implementation changes.

3. TestNG-Specific Features

TestNG features such as:

  • dependsOnMethods
  • Priorities
  • Complex testng.xml orchestration
  • Groups
  • Thread-count configuration

do not always have one-to-one equivalents.

Playwright provides mechanisms such as:

  • test.describe()
  • Tags
  • Projects
  • Fixtures
  • test.describe.serial()

But Playwright encourages independent tests.

If your Selenium suite heavily depends on execution order, redesign may be better than direct translation.

4. Legacy Browser Coverage

Selenium's historical browser coverage, including Internet Explorer, does not transfer to Playwright.

If IE testing is still a hard requirement, maintaining Selenium for those scenarios may be necessary.

5. AutoIt/Robot Flows

Many Selenium projects use AutoIt or Java Robot for native dialogs.

Playwright eliminates the need for some of these workflows through APIs such as:

  • File chooser handling
  • File uploads
  • Browser authentication

However, genuinely OS-native automation remains outside the normal scope of browser automation frameworks.

A Migration Strategy That Actually Works

Do not rewrite the entire Selenium suite in one shot.

A safer approach is incremental migration.

1. Run Both Frameworks in Parallel

Start with critical flows.

For example:

  • Login
  • Payment
  • Checkout
  • Multi-user scenarios
  • High-value business workflows

Keep the existing Selenium suite running for the remaining tests.

2. Port Intent, Not Code

For every Selenium workaround, ask:

"What problem was this code solving?"

For example:

Thread.sleep(5000);

Do not automatically translate it into:

await page.waitForTimeout(5000);

Instead ask why the five-second wait existed.

Maybe the actual requirement is:

await expect(locator).toBeVisible();

or:

await expect(locator).toHaveText('Completed');

The goal is to migrate the behavior, not the old implementation.

3. Adopt Playwright Capabilities During Migration

Do not postpone Playwright-specific features.

Set up:

  • storageState
  • Trace-on-retry
  • Parallel workers
  • Browser projects
  • Network interception
  • Fixtures

during the migration itself.

Otherwise, you may end up touching every test twice.

4. Rebuild Your Locators

Migration is a good opportunity to move from:

XPath everywhere

to:

getByRole()
getByLabel()
getByText()
getByPlaceholder()
CSS
XPath only when necessary

This improves maintainability without requiring a separate locator-refactoring project.

5. Delete Selenium Only After Stabilization

Do not delete the old Selenium test because the Playwright version compiles.

Delete it after the Playwright version has passed a meaningful regression cycle and demonstrated equivalent coverage.

Stability, not compilation, is the definition of "migrated."

6. Measure the Results

Track metrics before and after migration:

  • Regression execution time
  • Flaky-test rate
  • Test maintenance effort
  • Failure investigation time
  • Parallel execution capacity
  • Cross-browser coverage

For many teams, the strongest business case is not simply "Playwright is newer."

It is:

The same regression suite finishes faster and requires less synchronization maintenance.

Quick-Reference Cheat Sheet

Task Selenium (Java) Playwright (TypeScript)
Open URL driver.get(url) await page.goto(url)
Find element driver.findElement(By...) page.locator() / getByRole()
Click element.click() await locator.click()
Type text element.sendKeys("...") await locator.fill('...')
Clear textbox element.clear() await locator.clear()
Get text element.getText() await locator.textContent()
Get attribute element.getAttribute("href") await locator.getAttribute('href')
Current URL driver.getCurrentUrl() page.url()
Back / Forward navigate().back() / forward() page.goBack() / page.goForward()
Window size manage().window().maximize() Configure viewport in Playwright
Screenshot TakesScreenshot await page.screenshot({ path })
Dropdown new Select(el).selectByVisibleText() await locator.selectOption({ label })
Alert driver.switchTo().alert().accept() page.on('dialog', d => d.accept())
Frame driver.switchTo().frame("id") page.frameLocator('#id')
New window/tab Window handles + switchTo().window() page.waitForEvent('popup')
Upload input.sendKeys(filePath) await locator.setInputFiles(path)
Hover Actions.moveToElement().perform() await locator.hover()
Drag & drop Actions.dragAndDrop().perform() await source.dragTo(target)
Execute JavaScript JavascriptExecutor.executeScript() await page.evaluate()
Explicit wait WebDriverWait + ExpectedConditions Auto-wait / expect()
Parallel execution Grid / TestNG parallel Workers in config
Run tests mvn test npx playwright test

📚 Official documentation: Playwright docs: Installation · Selenium documentation

Frequently Asked Questions

Is Playwright Better Than Selenium?

For modern SPAs such as React and Angular applications, Playwright's auto-waiting, parallel execution, browser automation capabilities, and debugging tools make it a strong choice.

Selenium can remain the better choice for:

  • Legacy applications
  • Existing Java-heavy enterprise frameworks
  • Environments with established Selenium infrastructure
  • Legacy browser requirements
  • Stable suites where migration costs outweigh the benefits

The right question is not:

"Which tool is better?"

It is:

"Which tool gives this project the best reliability, maintainability, coverage, and total cost?"

Can I Convert Selenium Tests to Playwright Automatically?

There is no reliable one-to-one conversion process that can correctly migrate an entire Selenium framework.

Mechanical conversion misses the point because much of your Selenium code exists to solve problems around:

  • Driver management
  • Explicit waits
  • Synchronization
  • JavaScript workarounds
  • Window switching
  • Framework plumbing

Those patterns often do not need to be reproduced in Playwright.

Port test intent, not code.

Do I Have to Learn TypeScript to Use Playwright?

No.

Playwright supports:

  • Java
  • Python
  • C#
  • TypeScript/JavaScript

However, if you want to use the Playwright Test ecosystem with its fixtures, configuration, and broader TypeScript/JavaScript community ecosystem, TypeScript is worth learning. You may also find Playwright vs Selenium vs Cypress + Framework Architecture useful.

How Long Does a Selenium-to-Playwright Migration Take?

The timeline depends heavily on:

  • Number of tests
  • Framework complexity
  • Number of pages
  • Amount of custom utilities
  • Test data architecture
  • CI/CD integration
  • Reporting
  • Parallel execution requirements
  • Application complexity

A practical strategy is to migrate critical flows first, then gradually move the remaining suite while Selenium continues running.

The biggest bottleneck is usually stabilization, not writing the initial Playwright code.

Does Playwright Need Explicit Waits Like Selenium?

Almost never for ordinary element interactions.

Playwright automatically waits for actionability conditions, and web-first assertions retry automatically.

For example:

await expect(
    page.getByRole('status')
).toHaveText('Payment successful');

For specific asynchronous operations, you may still use:

await page.waitForLoadState();

or:

await page.waitForResponse('**/api/payment');

The important principle remains:

Wait for a meaningful condition, not an arbitrary amount of time.