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:
- Log in as
standard_user. - Add the Sauce Labs Backpack to the cart.
- 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:
- Follow our Playwright tutorial to set up a project, then add the Playwright test.
- Install Cypress with
npm install -D cypress, setbaseUrlincypress.config.js, and add the Cypress test undercypress/e2e/. - 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.