Back to Blog

Migrating from Selenium to Playwright: Step-by-Step Without Losing a Single Test

A comprehensive step-by-step guide to migrating your test automation suite from Selenium WebDriver to Playwright, including code examples, strategy, and best practices for a smooth transition.

David Johnson

Senior QA Automation Engineer with 10+ years of experience in test frameworks

Published

11 min read

Reading time

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 - Configuration
  • tests/ - Test directory
  • tests-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.

Related Posts