DEV Community

Alex Spinov
Alex Spinov

Posted on

Playwright Has a Free E2E Testing Framework: Cross-Browser Tests That Run in Parallel With Auto-Waiting

Cypress runs only in Chromium. Selenium needs WebDriver setup and flaky waits. Puppeteer doesn't support Firefox or Safari. Your E2E tests are slow, flaky, and only cover one browser.

What if your E2E framework ran Chromium, Firefox, AND WebKit in parallel — with auto-waiting that eliminates flaky tests?

That's Playwright Test.

Quick Start

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

This creates:

  • playwright.config.ts — configuration
  • tests/ — test directory with example
  • tests-results/ — artifacts (screenshots, videos)

Your First Test

import { test, expect } from "@playwright/test";

test("user can log in", async ({ page }) => {
  await page.goto("/login");

  await page.fill('[name="email"]', "test@example.com");
  await page.fill('[name="password"]', "password123");
  await page.click('button[type="submit"]');

  // Auto-waits for element to appear
  await expect(page.getByText("Welcome back")).toBeVisible();
  await expect(page).toHaveURL("/dashboard");
});

test("search returns results", async ({ page }) => {
  await page.goto("/");

  await page.getByPlaceholder("Search...").fill("typescript");
  await page.keyboard.press("Enter");

  // Auto-waits and retries until condition is met
  await expect(page.getByTestId("result-count")).toContainText("results");
  const results = page.getByRole("article");
  await expect(results).toHaveCount(10);
});
Enter fullscreen mode Exit fullscreen mode

No await page.waitForTimeout(2000). Playwright auto-waits for elements to be visible, enabled, and stable before interacting.

Configuration

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [["html"], ["junit", { outputFile: "results.xml" }]],

  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    trace: "on-first-retry",
  },

  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile", use: { ...devices["iPhone 14"] } },
  ],

  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});
Enter fullscreen mode Exit fullscreen mode

Page Object Model

// pages/login.page.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto("/login");
  }

  async login(email: string, password: string) {
    await this.page.fill('[name="email"]', email);
    await this.page.fill('[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }

  async expectError(message: string) {
    await expect(this.page.getByRole("alert")).toContainText(message);
  }
}

// tests/login.spec.ts
test("shows error for invalid credentials", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login("wrong@email.com", "wrongpass");
  await loginPage.expectError("Invalid credentials");
});
Enter fullscreen mode Exit fullscreen mode

API Testing (No Browser Needed)

test("API: create and retrieve user", async ({ request }) => {
  const response = await request.post("/api/users", {
    data: { name: "Test User", email: "test@example.com" },
  });

  expect(response.ok()).toBeTruthy();
  const user = await response.json();
  expect(user.name).toBe("Test User");

  const getResponse = await request.get(`/api/users/${user.id}`);
  expect(getResponse.ok()).toBeTruthy();
});
Enter fullscreen mode Exit fullscreen mode

Visual Regression Testing

test("homepage matches snapshot", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png", {
    maxDiffPixelRatio: 0.01,
  });
});
Enter fullscreen mode Exit fullscreen mode

Playwright vs Cypress vs Selenium

Feature Playwright Cypress Selenium
Browsers Chromium, Firefox, WebKit Chromium only* All (via drivers)
Parallel Built-in Paid feature Manual setup
Auto-wait Yes Yes No (manual waits)
API testing Built-in Via cy.request Separate tool
Mobile emulation Built-in Limited Appium
Speed Fast Medium Slow
Debugging Trace viewer, VS Code Time travel Console logs

When to Choose Playwright

Choose Playwright when:

  • Cross-browser testing matters (Safari/WebKit is critical for iOS)
  • You want parallel test execution out of the box
  • You need API + UI testing in one framework
  • Flaky tests are killing your CI

Skip Playwright when:

  • You only target Chrome and want Cypress's interactive runner
  • Your team already has a large Cypress test suite
  • You need native mobile testing (use Appium or Detox)

The Bottom Line

Playwright eliminates the two biggest E2E testing problems: flakiness (auto-waiting) and coverage (all browsers in parallel). Your tests run fast, test real browsers, and don't break randomly.

Start here: playwright.dev


Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors

Top comments (0)