Skip to content
Independent guides for QA & test automationRSSEditorial policy
QA Vibes

Cypress vs Playwright vs Selenium: The Same Test in All Three, Timed

One test (log in, add a product, check the cart badge) written in Cypress, Playwright, and Selenium against the same public demo shop. How the code differs, how long each run took, what broke during setup, and which framework fits which team.

QA Vibes EditorialPublished Updated 7 minTested with Playwright 1.63.0, Cypress 16.0.0, Selenium 4.49.0, Chrome 153Revision history ↓

Key takeaways

  • On our machine the same test took 5.7 to 6.4 s in Playwright, 15.7 to 23.4 s in Selenium, and 26.3 to 27.0 s in Cypress, mostly startup time.
  • Cypress 16 removed Cypress.env(); read secrets with cy.env() instead.
  • Choose Playwright for new web projects, Cypress for front-end teams who want its interactive runner, and Selenium when tests must live in Java, C#, Python, or Ruby.
  • Measure on your own tests before you decide.
Contents (10 sections)

Introduction

Framework comparisons usually list features. Features matter, but they don't tell you what it's like to write a test, or what goes wrong on the first day. So we wrote the same test in all three frameworks, ran each one three times, and kept notes.

The test runs against Sauce Demo, a public practice shop:

  1. Log in as standard_user.
  2. Add the Sauce Labs Backpack to the cart.
  3. Check that the cart badge shows 1.

Setup: Windows 11, Chrome 153, Node.js 20.20.2, Java 21, and Maven 3.9.9, with Playwright 1.63.0, Cypress 16.0.0, and Selenium 4.49.0 (Java) with JUnit 6.

The short version

Cypress Playwright Selenium
Languages JavaScript, TypeScript JavaScript, TypeScript, Python, Java, .NET Java, Python, C#, Ruby, JavaScript
Browsers Chrome-family, Firefox; WebKit experimental Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari
Waiting Automatic retries Automatic waiting and retrying assertions Explicit waits you write
Parallel runs Via Cypress Cloud (paid) or third-party tools Built in: workers and sharding Via your test runner and Selenium Grid
License MIT (app); Cypress Cloud is commercial Apache 2.0 Apache 2.0
Our test, wall-clock time (3 runs) 26.3 – 27.0 s 5.7 – 6.4 s 15.7 – 23.4 s
Setup problems we hit 2 0 1

The rest of this article explains each row, starting with the code.

The same test, three ways

Playwright

import { test, expect } from "@playwright/test";
 
test.use({ baseURL: "https://www.saucedemo.com", testIdAttribute: "data-test" });
 
const PASSWORD = process.env.SAUCE_PASSWORD ?? "secret_sauce";
 
test("adds the backpack to the cart", async ({ page }) => {
  await page.goto("/");
  await page.getByPlaceholder("Username").fill("standard_user");
  await page.getByPlaceholder("Password").fill(PASSWORD);
  await page.getByRole("button", { name: "Login" }).click();
 
  await page
    .getByTestId("inventory-item")
    .filter({ hasText: "Sauce Labs Backpack" })
    .getByRole("button", { name: "Add to cart" })
    .click();
  await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");
});

Every line is awaited, and Playwright waits for each element to be ready before acting. Locators like getByPlaceholder and getByRole describe the page the way a user sees it. filter({ hasText }) finds the right product card before looking for its button.

Cypress

describe("cart", () => {
  it("adds the backpack to the cart", () => {
    cy.visit("/");
    cy.get('[data-test="username"]').type("standard_user");
    cy.env(["SAUCE_PASSWORD"]).then(({ SAUCE_PASSWORD }) => {
      cy.get('[data-test="password"]').type(SAUCE_PASSWORD ?? "secret_sauce", { log: false });
    });
    cy.get('[data-test="login-button"]').click();
 
    cy.contains('[data-test="inventory-item"]', "Sauce Labs Backpack").find("button").click();
    cy.get('[data-test="shopping-cart-badge"]').should("have.text", "1");
  });
});

There's no await: Cypress commands are queued and run in order, and each retries until it succeeds or times out. cy.contains(selector, text) finds the product card by its text in one call. The base URL lives in cypress.config.js.

Finding elements by role or label, as in the Playwright version, needs the separate Testing Library plugin in Cypress, so we used the shop's data-test attributes.

Selenium (Java)

import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
 
class CartTest {
    private WebDriver driver;
 
    @BeforeEach
    void startBrowser() {
        driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new"));
    }
 
    @AfterEach
    void stopBrowser() {
        driver.quit();
    }
 
    @Test
    void addsTheBackpackToTheCart() {
        driver.get("https://www.saucedemo.com/");
        driver.findElement(By.cssSelector("[data-test='username']")).sendKeys("standard_user");
        driver.findElement(By.cssSelector("[data-test='password']"))
            .sendKeys(System.getenv().getOrDefault("SAUCE_PASSWORD", "secret_sauce"));
        driver.findElement(By.cssSelector("[data-test='login-button']")).click();
 
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        WebElement backpack = wait.until(ExpectedConditions.visibilityOfElementLocated(
            By.xpath("//div[@data-test='inventory-item'][.//div[text()='Sauce Labs Backpack']]")));
        backpack.findElement(By.tagName("button")).click();
 
        assertEquals("1", driver.findElement(By.cssSelector("[data-test='shopping-cart-badge']")).getText());
    }
}

The Selenium version is the longest because more is explicit: starting and quitting the browser, the wait after login, and finding the product card by its text with XPath. None of that is a flaw. It's the same work the other two do for you, written out where you can control it. The full project setup is in our Selenium getting-started guide.

Run times

We ran each test three times, one framework after another, and measured the wall-clock time of the whole command: starting the tool, launching Chrome, running the test, and shutting down.

Framework Command Run 1 Run 2 Run 3
Playwright npx playwright test tests/compare --workers=1 5.7 s 6.4 s 6.2 s
Cypress npx cypress run --browser chrome 26.4 s 27.0 s 26.3 s
Selenium mvn -q test -Dtest=CartTest 16.6 s 23.4 s 15.7 s

The tests themselves were quick in all three. Playwright reported 0.65 to 0.73 seconds for the test, and Cypress 1.6 to 2.6 seconds. Most of the difference is startup:

  • Cypress starts its own app, prepares the browser, and bundles the spec before the test runs.
  • Selenium starts Maven and a JVM, and Selenium Manager locates the browser driver before Chrome opens.
  • Playwright starts Node and a bundled browser directly.

Be careful with these numbers. This is one short test on one machine against a public website, so network speed varies from run to run (see Selenium's run 2). Startup cost matters most when you run a single test repeatedly while writing it. For a large suite on CI, parallelism and test design matter far more than a 20-second startup difference.

Setup problems we hit

Honest first-day experience, in the order we hit it:

Cypress: Cypress.env() no longer works in Cypress 16. Our first spec read the password with Cypress.env("SAUCE_PASSWORD") and failed immediately with an error pointing to a migration guide. Cypress 16 replaces it with cy.env() for secrets (asynchronous, as in the code above) and Cypress.expose() for non-sensitive values. Many tutorials still show the old API.

Cypress: Electron is deprecated as the default browser. The Cypress 16 migration guide recommends Chrome or Edge instead, so we ran with --browser chrome.

Selenium: Unable to establish loopback connection on Windows. Every test failed before Chrome opened. The error came from the Java HTTP client Selenium uses, not from Selenium itself, and the JVM option -Djdk.net.unixdomain.tmpdir=C:/t fixed it. Details are in the Selenium guide. This is a Java-on-Windows issue you may never see, but it's the kind of thing that eats a morning.

Playwright: no problems on this test. npm init playwright installed the package and the browser, and the test passed on the first run.

Architecture: why they behave differently

  • Cypress runs inside the browser, alongside your application. That gives it direct access to the app and a very good interactive runner with time-travel snapshots. It also means one browser tab per test; the Cypress documentation lists this among its permanent trade-offs. Visiting several origins in one test needs cy.origin().
  • Playwright controls the browser from outside, over the browser's own debugging protocols. One test can open several tabs, browser contexts, or users at once, and the same API covers Chromium, Firefox, and WebKit.
  • Selenium speaks the W3C WebDriver standard to a driver for each browser. Any browser vendor that implements WebDriver works, and every cloud grid supports it. The trade-off is that waiting and many conveniences are up to you and your libraries.

Which one should you choose?

Choose Playwright if you're starting a new web project, want several browsers including WebKit, need tests that use more than one tab or user, or want free built-in parallelism and tracing. It's the default we'd recommend for a team with no existing investment.

Choose Cypress if your team is front-end JavaScript developers who will write the tests themselves, you value its interactive runner for building tests, and you also want component testing in the same tool. Budget for Cypress Cloud if you need its parallelization and dashboards. Already on Cypress? Paste a spec into our free Cypress test reviewer to find fixed waits, commands chained after actions, and APIs that Cypress 16 removed, including the Cypress.env() call we hit above.

Choose Selenium if your test code must live in Java, C#, Python, or Ruby alongside an existing codebase, you need a browser only WebDriver supports, or you already run Selenium Grid or a cloud provider. A large, working Selenium suite is rarely worth rewriting just to switch tools.

Whatever you pick, the habits matter more than the tool: user-facing locators, waiting for conditions instead of time, and assertions that can actually fail. Our UI automation mistakes article shows what happens without them.

Try it yourself

All three tests use a public site and public demo credentials, so you can reproduce this comparison in under an hour:

  1. Follow our Playwright tutorial to set up a project, then add the Playwright test.
  2. Install Cypress with npm install -D cypress, set baseUrl in cypress.config.js, and add the Cypress test under cypress/e2e/.
  3. Create the Maven project from our Selenium guide and add CartTest.java.

Time each run on your own machine. Your numbers will differ from ours, and that's the point: measure before you decide.

Conclusion

Written side by side, the three tests do the same work in different styles: Playwright with awaited, user-facing locators; Cypress with a retrying command chain; Selenium with everything explicit. On our machine, Playwright started fastest and set up without surprises, Cypress had the best interactive runner but the slowest start and two breaking changes to work around, and Selenium was verbose but fits any language and any grid. Pick the one your team will maintain, and measure it on your own tests.

Sources and further reading

Tools mentioned

PlaywrightUI AutomationOpen source
CypressUI AutomationOpen source
SeleniumUI AutomationOpen source
BrowserStackDevice CloudFree trial
Sauce LabsDevice CloudFree trial

Links go to each tool’s official site. How we choose and link tools

Revision history

Rewritten as a hands-on comparison: the same test implemented and run in all three frameworks, with measured run times and the setup problems we hit.
Revised during a site-wide content audit.
First published.

Spotted a mistake? Report it — corrections land here.

Written and reviewed by

QA Vibes Editorial

Articles are written and reviewed by practicing QA and automation engineers. Every article lists its sources and shows when it was last updated.

Check your own tests

Cypress test reviewer

Paste a Cypress spec to find fixed waits, missing assertions, and APIs Cypress 16 removed.