Related articles: Also see why Playwright wins the 2026 framework comparison, setting up scalable fixture architecture in Playwright, and getting up and running with automated Playwright testing.
Migrating from Selenium to Playwright: Step-by-Step Without Losing a Single Test
You've been running Selenium tests for years. They work—mostly. But you're spending more time fighting flaky tests, debugging timeouts, and maintaining complex WebDriver configurations than actually testing new features.
Meanwhile, your colleagues won't stop talking about Playwright: "Auto-waiting eliminates flakiness!" "Runs 3x faster!" "Multiple browsers with one API!" "Built-in test runner!"
Should you migrate? And if so, how do you migrate thousands of tests without breaking everything?
This comprehensive guide walks you through the entire migration process—from evaluation and planning to execution and optimization. Whether you're migrating 50 tests or 5,000, you'll learn proven strategies, code conversion patterns, and best practices that make the transition smooth and successful.
Why Migrate from Selenium to Playwright?
Selenium's Limitations in Modern Testing
Selenium WebDriver revolutionized test automation in 2004, but web applications have evolved dramatically. Today's challenges include:
1. Flakiness from Manual Waits
// Selenium: Manual wait management
await driver.wait(until.elementLocated(By.id('button')), 5000);
const element = await driver.findElement(By.id('button'));
await driver.wait(until.elementIsVisible(element), 5000);
await driver.wait(until.elementIsEnabled(element), 5000);
await element.click();
2. Complex Setup and Configuration
Different drivers for different browsers, versioning nightmares, PATH management, and browser-specific quirks.
3. Slow Execution
No built-in parallelization, sequential execution, and network idle waiting.
4. Limited Modern Web Support
Shadow DOM, iframes, web components, and SPA routing require workarounds.
Playwright's Advantages
| Feature | Selenium WebDriver | Playwright |
|---|---|---|
| Auto-waiting | Manual wait() calls |
Automatic, built-in |
| Browser support | Chrome, Firefox, Safari (limited) | Chromium, Firefox, WebKit |
| Installation | Driver per browser | Single npm install |
| Parallelization | External tools required | Built-in test runner |
| Network control | Limited proxy support | Full request/response mocking |
| Modern APIs | Callback-based | Promise-based, async/await |
| Shadow DOM | Complex workarounds | Native support |
| Speed | Baseline | 2-3x faster |
| Debugging | External tools | Built-in trace viewer, inspector |
When Migration Makes Sense
✅ Migrate if:
- Test flakiness is consuming >20% of maintenance time
- You need faster CI/CD pipelines
- Modern web features (Shadow DOM, SPAs) are problematic
- Team is comfortable with JavaScript/TypeScript
- You want better developer experience
âš ï¸ Consider carefully if:
- Tests are stable and maintenance is minimal
- Team lacks JavaScript expertise
- You require IE11 support (Playwright doesn't support it)
- Using Selenium Grid infrastructure heavily
Migration Strategy: The Four-Phase Approach
Phase 1: Assessment (Week 1)
1.1 Inventory Your Test Suite
// analysis-script.ts
import { parse } from '@typescript-eslint/typescript-estree';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
interface TestInventory {
totalFiles: number;
totalTests: number;
seleniumPatterns: Map<string, number>;
complexity: {
customWaits: number;
pageObjects: number;
dataProviders: number;
};
}
function analyzeTestSuite(directory: string): TestInventory {
const inventory: TestInventory = {
totalFiles: 0,
totalTests: 0,
seleniumPatterns: new Map(),
complexity: {
customWaits: 0,
pageObjects: 0,
dataProviders: 0,
},
};
const files = readdirSync(directory, { recursive: true });
for (const file of files) {
if (!file.toString().endsWith('.ts') && !file.toString().endsWith('.js')) continue;
inventory.totalFiles++;
const content = readFileSync(join(directory, file.toString()), 'utf-8');
// Count Selenium patterns
const patterns = {
findElement: (content.match(/findElement/g) || []).length,
WebDriver: (content.match(/WebDriver/g) || []).length,
'until.': (content.match(/until\./g) || []).length,
Actions: (content.match(/new Actions/g) || []).length,
};
for (const [pattern, count] of Object.entries(patterns)) {
inventory.seleniumPatterns.set(pattern, (inventory.seleniumPatterns.get(pattern) || 0) + count);
}
// Detect complexity patterns
if (content.includes('PageObject') || content.includes('BasePage')) {
inventory.complexity.pageObjects++;
}
if (content.includes('@DataProvider') || content.includes('test.each')) {
inventory.complexity.dataProviders++;
}
if (content.match(/sleep|Thread\.sleep|waitFor/)) {
inventory.complexity.customWaits++;
}
}
return inventory;
}
// Run analysis
const results = analyzeTestSuite('./tests');
console.log('Migration Complexity Assessment:', results);
1.2 Identify Migration Priorities
Categorize tests by migration difficulty:
| Category | Criteria | Migration Effort |
|---|---|---|
| Low | Simple locators, no custom waits, standard actions | 1-2 hours per test |
| Medium | Page objects, some custom waits, file uploads | 3-5 hours per test |
| High | Complex Actions API usage, custom frameworks, browser-specific code | 1-2 days per test |
1.3 Create Migration Roadmap
gantt
title Selenium to Playwright Migration Timeline
dateFormat YYYY-MM-DD
section Phase 1: Setup
Install Playwright & tooling :2026-02-03, 2d
Create migration patterns :2026-02-05, 3d
section Phase 2: Pilot
Migrate 10 low-complexity tests :2026-02-08, 5d
Run parallel with Selenium :2026-02-13, 7d
section Phase 3: Bulk Migration
Migrate remaining tests (50% batch):2026-02-20, 14d
Migrate remaining tests (final) :2026-03-06, 14d
section Phase 4: Cleanup
Remove Selenium dependencies :2026-03-20, 3d
Optimize & refactor :2026-03-23, 5d
Phase 2: Setup and Pilot (Week 2-3)
2.1 Install Playwright
npm init playwright@latest
This creates:
playwright.config.ts- Configurationtests/- Test directorytests-examples/- Example tests
2.2 Configure Parallel Execution
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
workers: process.env.CI ? 4 : undefined, // Auto-detect locally
// Timeout configuration
timeout: 30 * 1000,
expect: {
timeout: 5000,
},
// Run tests in multiple browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
// Reporter configuration
reporter: [['html'], ['json', { outputFile: 'test-results.json' }], ['junit', { outputFile: 'junit-results.xml' }]],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
2.3 Create Conversion Patterns
Common Selenium → Playwright conversions:
// SELENIUM
const driver = await new Builder().forBrowser('chrome').build();
await driver.get('https://example.com');
const element = await driver.findElement(By.id('username'));
await element.sendKeys('john@example.com');
await driver.findElement(By.css('button[type="submit"]')).click();
await driver.wait(until.urlContains('/dashboard'), 5000);
await driver.quit();
// PLAYWRIGHT
import { test, expect } from '@playwright/test';
test('login flow', async ({ page }) => {
await page.goto('https://example.com');
await page.fill('#username', 'john@example.com');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard/);
// Auto-cleanup, no quit() needed
});
Phase 3: Bulk Migration (Week 4-7)
3.1 Locator Migration Patterns
// Selenium locator strategies → Playwright
const conversionMap = {
// By.id
'By.id("login")': 'page.locator("#login")',
// By.css
'By.css(".button")': 'page.locator(".button")',
// By.xpath
'By.xpath("//button[@type=\'submit\']")': 'page.locator("button[type=\'submit\']")',
// By.linkText
'By.linkText("Click here")': 'page.getByRole("link", { name: "Click here" })',
// By.className
'By.className("active")': 'page.locator(".active")',
// By.name
'By.name("email")': 'page.locator("[name=\'email\']")',
};
3.2 Wait Strategy Migration
// Selenium: Explicit waits
await driver.wait(until.elementLocated(By.id('result')), 10000);
await driver.wait(until.elementIsVisible(element), 5000);
await driver.wait(until.elementIsEnabled(element), 3000);
// Playwright: Auto-waiting (no code needed!)
await page.click('#submit'); // Auto-waits for element to be visible, enabled, stable
// Custom waits only for specific conditions
await page.waitForURL('**/dashboard');
await page.waitForResponse((resp) => resp.url().includes('/api/user'));
await page.waitForLoadState('networkidle');
3.3 Actions API Migration
// Selenium Actions
const actions = driver.actions({ async: true });
await actions.move({ origin: element }).perform();
await actions.click().perform();
await actions.sendKeys('text').perform();
await actions.dragAndDrop(source, target).perform();
// Playwright equivalents
await page.hover('selector');
await page.click('selector');
await page.fill('selector', 'text');
await page.dragAndDrop('#source', '#target');
// Complex sequence
await page.locator('#element').hover();
await page.mouse.down();
await page.mouse.move(100, 200);
await page.mouse.up();
3.4 Page Object Pattern Migration
// Selenium Page Object
class LoginPageSelenium {
private driver: WebDriver;
constructor(driver: WebDriver) {
this.driver = driver;
}
async navigateTo() {
await this.driver.get('https://example.com/login');
}
async login(email: string, password: string) {
await this.driver.findElement(By.id('email')).sendKeys(email);
await this.driver.findElement(By.id('password')).sendKeys(password);
await this.driver.findElement(By.css('button[type="submit"]')).click();
await this.driver.wait(until.urlContains('/dashboard'), 5000);
}
}
// Playwright Page Object
import { Page, Locator } from '@playwright/test';
export class LoginPagePlaywright {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('#email');
this.passwordInput = page.locator('#password');
this.submitButton = page.locator('button[type="submit"]');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await this.page.waitForURL('**/dashboard');
}
async loginWithAPI(email: string, password: string) {
// Playwright advantage: Skip UI, use API for faster setup
const response = await this.page.request.post('/api/auth/login', {
data: { email, password },
});
const { token } = await response.json();
await this.page.context().addCookies([
{
name: 'auth_token',
value: token,
domain: 'example.com',
path: '/',
},
]);
}
}
// Usage
test('login test', async ({ page }) => {
const loginPage = new LoginPagePlaywright(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
});
3.5 Automated Migration Script
For large suites, create a codemod:
// migrate-selenium-playwright.ts
import { readFileSync, writeFileSync } from 'fs';
function convertSeleniumToPlaywright(code: string): string {
let converted = code;
// Import conversion
converted = converted.replace(
/import.*from ['"]selenium-webdriver['"]/g,
"import { test, expect } from '@playwright/test';",
);
// Driver → Page
converted = converted.replace(/driver\./g, 'page.');
// findElement conversions
converted = converted.replace(/\.findElement\(By\.id\(['"](.*?)['"]\)\)/g, ".locator('#$1')");
converted = converted.replace(/\.findElement\(By\.css\(['"](.*?)['"]\)\)/g, ".locator('$1')");
// Action conversions
converted = converted.replace(/\.sendKeys\((.*?)\)/g, '.fill($1)');
converted = converted.replace(/\.getText\(\)/g, '.textContent()');
converted = converted.replace(/\.click\(\)/g, '.click()'); // Same method!
// Wait conversions (remove most)
converted = converted.replace(
/await driver\.wait\(until\..*?\);\s*/g,
'// Auto-wait removed (Playwright handles this)\n',
);
// Assertions
converted = converted.replace(/assert\.equal\((.*?), (.*?)\)/g, 'await expect($1).toBe($2)');
return converted;
}
// Process file
const inputFile = process.argv[2];
const code = readFileSync(inputFile, 'utf-8');
const converted = convertSeleniumToPlaywright(code);
writeFileSync(inputFile.replace('.selenium.ts', '.playwright.ts'), converted);
Phase 4: Optimization and Cleanup (Week 8)
4.1 Remove Selenium Dependencies
npm uninstall selenium-webdriver chromedriver geckodriver
rm -rf node_modules/.cache/selenium
4.2 Leverage Playwright-Specific Features
// Network mocking (impossible in Selenium)
test('mock API responses', async ({ page }) => {
await page.route('**/api/user', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ name: 'Test User', role: 'admin' }),
});
});
await page.goto('/profile');
await expect(page.locator('.user-name')).toHaveText('Test User');
});
// Request interception
test('capture API calls', async ({ page }) => {
const requests: string[] = [];
page.on('request', (request) => {
if (request.url().includes('/api/')) {
requests.push(request.url());
}
});
await page.goto('/');
console.log('API calls made:', requests);
});
// Multiple contexts (parallel sessions)
test('multi-user scenario', async ({ browser }) => {
const admin = await browser.newContext();
const user = await browser.newContext();
const adminPage = await admin.newPage();
const userPage = await user.newPage();
// Both users interact simultaneously
await Promise.all([adminPage.goto('/admin'), userPage.goto('/dashboard')]);
});
Common Migration Challenges and Solutions
Challenge 1: Grid/Selenium Hub Infrastructure
Problem: You have existing Selenium Grid with distributed execution.
Solution: Use Playwright's built-in parallelization or run Playwright in containers:
# docker-compose.yml for distributed Playwright
version: '3'
services:
playwright-worker-1:
image: mcr.microsoft.com/playwright:latest
command: npx playwright test --shard=1/4
playwright-worker-2:
image: mcr.microsoft.com/playwright:latest
command: npx playwright test --shard=2/4
playwright-worker-3:
image: mcr.microsoft.com/playwright:latest
command: npx playwright test --shard=3/4
playwright-worker-4:
image: mcr.microsoft.com/playwright:latest
command: npx playwright test --shard=4/4
Challenge 2: TestNG/JUnit Integration (Java)
Problem: Tests use Java + TestNG.
Solution: Use Playwright Java bindings (though TypeScript/JS is recommended):
// Playwright has Java support
import com.microsoft.playwright.*;
public class PlaywrightTest {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("https://example.com");
page.click("#submit");
}
}
}
Challenge 3: Custom Selenium Extensions
Problem: Custom WebDriver extensions for specific needs.
Solution: Replace with Playwright's extensibility:
// Custom fixture for authenticated sessions
import { test as base } from '@playwright/test';
type CustomFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<CustomFixtures>({
authenticatedPage: async ({ page }, use) => {
// Setup: login before each test
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await use(page);
// Teardown: logout after test
await page.click('#logout');
},
});
// Use in tests
test('view profile', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/profile');
await expect(authenticatedPage.locator('h1')).toHaveText('My Profile');
});
Migration Validation Checklist
Before considering migration complete:
- All tests migrated and passing
- Execution time improved (target: 30-50% faster)
- Flaky test reduction (target: >80% reduction)
- CI/CD integration working
- Team trained on Playwright
- Documentation updated
- Monitoring and reporting configured
- Selenium dependencies removed
- Parallel execution optimized
- Code review process adapted
ROI and Success Metrics
Track these metrics to validate migration success:
| Metric | Before (Selenium) | After (Playwright) | Improvement |
|---|---|---|---|
| Test execution time | 45 min | 18 min | 60% faster |
| Flaky test rate | 8% | 1% | 87.5% reduction |
| Maintenance hours/week | 12 hours | 3 hours | 75% reduction |
| Developer satisfaction | 6/10 | 9/10 | +50% |
| Test coverage | 65% | 78% | +20% |
Conclusion
Migrating from Selenium to Playwright is a significant investment—but one that pays dividends in faster tests, reduced flakiness, and improved developer experience. The key is methodical planning: assess your suite, pilot with simple tests, develop conversion patterns, then scale to bulk migration.
The modern web demands modern testing tools. Playwright's auto-waiting, built-in parallelization, and developer-friendly APIs make it the right choice for teams serious about quality and velocity.
Ready to modernize your test automation? Start your free trial with ScanlyApp and leverage our intelligent test orchestration platform to manage your Playwright tests at scale with advanced scheduling, parallel execution, and comprehensive reporting—no infrastructure setup required.
