Playwright is faster and more reliable than Selenium because it controls the browser directly, bypassing the WebDriver protocol that Selenium relies on.

This is a Playwright test running against a simple Express.js server:

// server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>Test Page</title>
    </head>
    <body>
      <h1 id="greeting">Hello, World!</h1>
      <button id="myButton">Click Me</button>
      <div id="message"></div>
      <script>
        document.getElementById('myButton').addEventListener('click', () => {
          document.getElementById('message').innerText = 'Button clicked!';
        });
      </script>
    </body>
    </html>
  `);
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});
// test.js
const { test, expect } = require('@playwright/test');

test('should interact with the page', async ({ page }) => {
  await page.goto('http://localhost:3000');

  // Check initial state
  await expect(page.locator('#greeting')).toHaveText('Hello, World!');

  // Interact with the button
  await page.locator('#myButton').click();

  // Verify the message appeared
  await expect(page.locator('#message')).toHaveText('Button clicked!');
});

To run this:

  1. Save the first code block as server.js and the second as test.js.
  2. Install dependencies: npm install express @playwright/test
  3. Start the server: node server.js
  4. Run the test in a separate terminal: npx playwright test test.js

Playwright’s architecture allows it to send commands directly to the browser’s debugging protocol (like Chrome DevTools Protocol or similar for Firefox and WebKit). Selenium, on the other hand, uses the WebDriver protocol, which acts as an intermediary. WebDriver translates commands into browser-specific instructions, adding an extra layer of communication. This extra hop in Selenium can introduce latency and flakiness, especially in scenarios involving complex DOM manipulations, network interception, or asynchronous operations. Playwright’s direct connection means it can execute commands more swiftly and reliably, leading to faster test execution and fewer "flaky" tests that pass or fail inconsistently.

The core problem Playwright solves is the inherent unreliability and slowness of browser automation, largely stemming from the limitations of the WebDriver protocol. Selenium, while a mature and widely adopted standard, suffers from issues like race conditions, slow element location, and difficulty in handling modern web application patterns like SPAs and micro-frontends. Playwright was designed from the ground up to address these challenges. It achieves this through several key mechanisms:

  • Direct Browser Communication: As mentioned, Playwright bypasses WebDriver and communicates directly with the browser’s debugging interface. This allows for immediate command execution and richer control.
  • Auto-Waits: Playwright automatically waits for elements to be actionable (visible, enabled, stable) before performing actions. This drastically reduces the need for manual waits and eliminates a common source of flakiness.
  • Network Interception: Playwright provides powerful tools to intercept, mock, and modify network requests and responses. This is invaluable for testing offline scenarios, simulating slow network conditions, or stubbing out external APIs.
  • Multi-Browser Support: Playwright supports Chromium (Chrome, Edge), Firefox, and WebKit (Safari) with a single API, ensuring consistent behavior across different rendering engines.
  • Context Isolation: Playwright allows you to create independent browser contexts, akin to incognito windows. This prevents state leakage between tests, ensuring that each test runs in a clean environment.

Consider a scenario where you need to test a form submission that involves an AJAX request.

With Selenium, you might write something like this:

// Selenium example (conceptual)
WebDriver driver = new ChromeDriver();
driver.get("http://example.com/form");

driver.findElement(By.id("username")).sendKeys("testuser");
driver.findElement(By.id("password")).sendKeys("password123");
driver.findElement(By.id("submitButton")).click();

// Manual wait for success message
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("successMessage")));

This often requires careful tuning of WebDriverWait to avoid race conditions.

Playwright simplifies this significantly:

// Playwright example
const { test, expect } = require('@playwright/test');

test('submit form', async ({ page }) => {
  await page.goto('http://example.com/form');

  await page.locator('#username').fill('testuser');
  await page.locator('#password').fill('password123');
  await page.locator('#submitButton').click();

  // Playwright auto-waits for the element to appear and be visible
  await expect(page.locator('#successMessage')).toBeVisible();
});

Playwright’s auto-wait mechanism handles the waiting for #successMessage to appear, making the test more robust and less prone to timing issues.

A key aspect of Playwright’s power is its ability to execute arbitrary JavaScript within the browser context, not just for assertions but for complex state manipulation. This is achieved through page.evaluate(). For instance, if you needed to manipulate localStorage before a test, you could do:

await page.evaluate(() => {
  localStorage.setItem('userToken', 'fake-token-123');
});
await page.goto('http://example.com/dashboard'); // Now dashboard uses the token

This direct injection of code allows for precise control over the browser’s state, which is often difficult or impossible with WebDriver-based tools. It’s like having a direct console access to the browser for every test step.

The biggest differentiator and often the most underutilized feature of Playwright is its ability to intercept network requests. This is not just for mocking API responses but for modifying them on the fly or even aborting them. For example, to simulate a slow-loading image or a failed API call:

await page.route('**/api/data', async route => {
  // Simulate a slow response
  await route.fulfill({
    status: 200,
    body: JSON.stringify({ message: 'Data loaded slowly' }),
    headers: {
      'Content-Type': 'application/json',
      'X-Response-Time': '2000ms' // Custom header
    }
  });
});
// Then navigate to the page that fetches this data

This level of network control allows for testing a much wider range of user experiences and error conditions that are hard to replicate with other tools.

The next step in mastering Playwright is exploring its parallel execution capabilities and advanced reporting features.

Want structured learning?

Take the full Playwright course →