🧪

Playwright vs Cypress


This is a copy paste of the README I wrote in this repository
You can find the test codes there

Back around 2020, I chose to use Cypress for E2E tests and had a relatively good experience. With Playwright rising in popularity, how does it compare to Cypress which had a head start?

TL;DR

  • Playwright is improving at a better cadence than Cypress did; some of Cypress decisions also favor their SaaS offering
  • Playwright is more performant than Cypress
  • Subjectively, I prefer Playwright in APIs and DX
  • There are a good number of features that Playwright has built-in, while you’d have to use plugins in Cypress

General differences

Star History Chart

  • Popularity
    • Playwright caught up to Cypress in 2022 and has been climbing faster than Cypress
  • Browsers
    • Cypress uses an app to run tests, and you can easily switch browsers in the app while running tests
    • Playwright directly interfaces with browser binaries, which contributes to its strength in performance
    • Cypress’s Webkit browser is still experimental
  • Writing tests
    • Playwright has a VSCode extension to facilitate development. You can easily run/debug tests within VSCode or use the Codegen tool to record tests
    • The Cypress app can do the same, but also offer the ability to “time travel” back to specific test steps for debugging
      • In my experience however, the time travel ability is rarely useful
  • Open source
    • While both tools have an entity behind them, Cypress currently has vested interest in their SaaS Cypress Cloud
    • This slowed QoL features that could’ve existed in the base library
    • It has also in my opinion, stunted the development in the performance angle (running tests in parallel, sharding tests.. etc)
    • A popular open source alternative is to use Sorry Cypress, which can be self-hosted
  • Component tests
    • Both tools have ways to write component tests, but this is out of scope for this comparison (I have no intention to deviate from Vitest+RTL)

playwright In my opinion, Playwright is the safer pick moving forward. It has more eyes watching it and has a respectable cadence. The Cypress app had more time in the oven, but Playwright’s VSCode extension is not far behind.

Foreword: Comparison setup

In this repository, some of Cypress’s kitchen sink test suites were migrated to Playwright. Even though they were written for Playwright, the titles were kept in sync for easy comparison.

I’ll use these selected test suites for discussion points:

CypressPlaywright
todotodo
actionsactions
aliasingaliasing
cookiescookies
network_requestsnetwork_requests

Note that it is definitely not an apples-to-apples comparison, there are some minor differences that might give an edge to 1 tool over the other. I’ve tried to tweak it as much as possible to not affect the overall outcome of the test suites.

Performance

Local runs

  • All tests are recorded headlessly on my PC, with no video recording
  • Recorded times are average over 5 runs, excluding the cold start
  • “w/ startup” means total run time after the test command is executed
  • “w/o startup” means the total test time reported by Cypress/Playwright
RunsAll 35 tests
Cypress run (w/ startup)43.5s
Cypress run (w/o startup)35.0s
Playwright (single worker, w/ startup)25.8s
Playwright (single worker, w/o startup)25.1s
Playwright (6 workers, w/ startup)6.9s
Playwright (6 workers, w/o startup)6.3s

playwright Playwright is the better option when writing tests locally


CI Runs

  • Playwright offers the ability to shard tests out of the box
  • Cypress Cloud orchestrates CI tests and can take a step further by optimizing sharded tests
    • e.g. test-runner-1 takes 2 long tests, test-runner-2 takes 4 shorter tests
    • With the more naive approach in Playwright, it’ll be cleanly divided where each runner take 3 tests

All 35 tests on Github Actions
badge
badge
badge

It seems unfair without an example using Cypress Cloud, but note that a sharded example for Playwright is also missing. Since there is already a significant difference with only 35 tests, I didn’t think it was necessary to invest effort into a sharded example.

playwright Purely on a performance angle, Playwright is the better option. The only reason to consider Cypress is if you’re a paying customer of Cypress Cloud and enjoy their SaaS features.

API differences

Beyond performance, there’s also Developer Experience (DX). I’ll compare these tools on a few select topics that I find important.

Async/await vs promise chaining

Most of my JS/TS code are written with async/await but because of Cypress, I was somewhat used to promise chaining. When I first tried Playwright, having to write await for almost every single command immediately felt like needless boilerplate code.

After spending just a bit of time with Playwright however, it clicked for me.
Now, I’m not sure if I can agree with the choice made by Cypress (reference)

Consider the following example, a snippet from network_requests. We want to:

  1. Call an API
  2. Use the response to call another API
  3. Run an assertion on the response of the 2nd API
// Cypress
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
  .its("body")
  .its("0")
  .then((user) => {
    cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
      userId: user.id,
      // ...redacted
    });
  })
  .then((response) => {
    expect(response).property("status").to.equal(201);
    // ...redacted
  });
// Playwright
const response = await request.get("https://jsonplaceholder.cypress.io/users?_limit=1");
const body = await response.json();
const user = body[0];

const response2 = await request.post("https://jsonplaceholder.cypress.io/posts", {
  data: {
    userId: user.id,
    // ...redacted
  },
});
const body2 = await response2.json();
expect(response2.status()).toBe(201);
// ...redacted

Promise chaining can get difficult to read quickly, even though you might not need to do this often in Cypress.
You’d also have to learn Cypress’s APIs such as its & wrap, instead of just doing what you’re used to in native Javascript.

playwright If you’re someone who typically writes async/await for your application code, then Playwright should appeal to you more.


Query mechanism

In my experience, both tools have the means to locate/query anything you need.
The point of contention is how intuitive or how difficult is it to use.

Consider the following, a snippet from aliasing. We want to:

  1. Get the table by class name
  2. Find the first row
  3. Find the first cell of the first row
  4. Find the button in the first cell
// Cypress
cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");

We can easily convert this to follow the same flow of logic using Playwright:

// Playwright
const firstBtn = page.locator(".as-table").locator("tbody>tr").first().locator("td").first().locator("button");

Of course, this example is contrived because it is used to demonstrate their API. The better way to achieve the same outcome is not to iterate through the table, but to find the content. This is a more resilient approach because the content is less likely to change. (what if the table is sorted or filtered?).

// Cypress
cy.get("td").contains("Row 1: Cell 1").find("button").as("firstBtn");

// Playwright
const firstButton = page.getByRole("cell", { name: "Row 1: Cell 1" }).getByRole("button");

It might be subjective at this point, but using Playwright’s APIs (getByRole, getByLabel) felt intuitive to me. It didn’t quite feel like I had to learn a new tool because I have a good understanding of the HTML markup of my application. In a way, this is also true for reading tests. Comparing the two 1-liners, I am able to understand the intent of the line from Playwright slightly easier.

This is not to say Cypress is worse on all fronts, I do appreciate small quality of life functions such as:

  • Traversing the DOM - .siblings(), .prev(), .next(), .parent()
  • Checking dropdown by value - .check("value")

My bigger issue with Cypress is the amount of APIs you have to learn:

  • Having to use aliases to workaround network requests and other problems
  • Cypress also comes with jQuery, and their examples for invoke function is littered with jQuery usage. As a dev who has no jQuery experience, this is just another API I’ll have to learn

⚖ Right now, I cannot objectively say Playwright is better, but I do enjoy writing tests in Playwright more.


Assertions

They both have their own quirks to learn and are fairly sound in my experience.

  • Example where there’s little difference

    // Cypress
    expect(response).property("body").to.contain({ title: "Cypress Test Runner" });
    
    // Playwright
    expect(body).toHaveProperty("title", "Cypress Test Runner");
  • Playwright has lesser “helpers” example 1

    // Cypress
    expect(user).property("id").to.be.a("number");
    
    // Playwright
    expect(typeof user.id).toBe("number");
  • Playwright has lesser “helpers” example 2

    // Cypress
    cy.get(".action-select-multiple")
      .select(["fr-apples", "fr-oranges", "fr-bananas"])
      .invoke("val")
      .should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
    
    // Playwright
    await expect(page.locator(".action-select-multiple")).toHaveValues(["fr-apples", "fr-oranges", "fr-bananas"]);

⚖ In my experience, I don’t find one to be better than the other. Playwright might have lesser assertions but I’ve yet to find myself missing Cypress’s assertion library; I’ve not once used deep.equal for example.


Combating flake

A flaky test means it passes/fails inconsistently, often not because of the application you’re testing.
The nature of end-to-end tests simply means there are more points of failure; e.g. network intermittence, conflicts in databases

In my experience with Cypress, I almost always used cy.wait() to wait for network requests to complete. While this alone isn’t enough to remove flake entirely, it helps decrease the likeliness of flake.

Out of the box, Playwright has the same API and a couple more features that I think helps a lot:

  • You can manually encompass blocks of logic to retry, even with exponential backoff (reference)
    • This is helpul when you’re aware of flaky portions of your test but you’re unable to directly fix that behavior
      • For example, an update to a resource separately causes it to be indexed into my search service. Since this service runs asynchronously to the update API, I cannot wait for the API response to assume it has been indexed. I used to set an arbitrary wait time with Cypress before running a search query, but manually retrying would be a much more resilient approach
    • You cannot do this in Cypress, you’d have to rely on the entire test case retrying. It seems this new API in Cypress can retry to a certain degree, but it requires you to write a custom query
  • You can wait for the page to emit the networkidle event (reference)
    • This is helpful especially coming from Cypress; I don’t want to have to write cy.wait for every API call in the page

playwright It is simply easier with Playwright


Quick, miscellaneous comparisons

  • Launching a server to test against
    • Playwright has the ability to start dev server without 3rd party support (reference)
    • Cypress requires modules such as wait-on or start-server-and-test (reference)
  • Testing multiple tabs
    • Playwright has first class support (reference)
    • Cypress claims it will never have multi-tab support (reference)
  • Plugins
    • One of the things I dislike in Cypress is how some basic functionality is kept separately in plugins: networkIdle, wait-until, drag-and-drop (all of these are built into Playwright)
    • Up until Cypress v9.3, you even had to use a plugin for uploading files: cypress-file-upload
    • Is it better than Playwright, which has no plugin support? I’d say I have only used plugins that should have been built-in anyway
  • Custom commands
    • A redeeming feature I found easier to use in Cypress are custom commands
    • You can of course write reusable code as just functions, Page Object Models, etc. But Cypress’s approach felt more intuitive so far

Conclusion

It has been a while since Playwright reached feature parity with Cypress, and I would say they are even ahead of Cypress at this point (if you exclude component testing and other Cypress SaaS services).

If end-to-end testing is all you care about, then Playwright should be the obvious choice. I’m apprehensive about Cypress’s future because most of my wishlist have been there since day 1, and it is obvious they have a different priority compared to a non-paying customer like myself.