Testing Simplified: Playwright or Cypress - The Definitive Comparison

Satya Swaroop Mohapatra's avatar

Satya Swaroop Mohapatra

Writing a blog comparing Playwright and Cypress is a great topic choice, as both are powerful tools in the world of software testing. Each brings its own set of advantages and capabilities, catering to the diverse needs of developers and quality assurance teams. In this blog, we will delve into a comprehensive comparison between these two popular testing frameworks, shedding light on their features, strengths, and weaknesses.

Throughout this blog, we will conduct an in-depth analysis of both Playwright and Cypress, exploring their core functionalities, browser support, ecosystem, community support, and various other aspects. By the end of this comparison, you should have a clear understanding of which testing framework aligns better with your specific testing requirements.

So, buckle up as we embark on this journey to unravel the strengths and weaknesses of Playwright and Cypress, and help you make an informed decision on which tool is best suited to supercharge your web application testing efforts. Let's dive in!

1. Introduction to Playwright and Cypress

1.1 Introducing Playwright

Playwright: The game-changer in web app testing! Effortlessly test across Chrome, Firefox, Safari, and Microsoft Edge. No flaky tests or asynchronous headaches; automatic waiting ensures reliability. Stay up-to-date with the latest browser features. Tailor Playwright to fit your specific needs. Experience blazing-fast parallel execution and embrace the support of developers worldwide. Say hello to your new testing superhero and conquer testing with Playwright!

1.2 Introducing Cypress

Meet Cypress, your reliable testing ally for dynamic web applications. As a developer seeking efficiency, rest assured that Cypress will exceed expectations. Craft seamless tests with its intuitive syntax and concentrate on building extraordinary web apps. Cypress guarantees rapid and dependable testing with Chrome, handling automatic waiting for precise results. Swiftly resolve issues using powerful debugging tools. Streamline testing and embrace newfound confidence with Cypress. Say hello to efficient and effective testing!

2. Comparison of Playwright and Cypress

2.1 System requirements

2.1.1 System Requirements for Playwright

  • Node.js 16+
  • Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
  • MacOS 12 Monterey or MacOS 13 Ventura.
  • Debian 11, Ubuntu 20.04 or Ubuntu 22.04.

2.1.2 System Requirements for Cypress

  • Node.js 14.x, Node.js 16.x, Node.js 18.x and above
  • Windows 7 and above (64-bit only).
  • MacOS 10.9 and above (Intel or Apple Silicon 64-bit (x64 or arm64)).
  • Linux Ubuntu 12.04 and above, Fedora 21 and Debian 8 (x86_64 or Arm 64-bit (x64 or arm64)).

Note: If you want to check other details like CPU, Memory and other details please check -> More details

2.2 Installation and Setup

2.2.1 Installing Playwright

Install via npm

npm init playwright@latest

Install via yarn

yarn create playwright

Install via pnpm

pnpm dlx create-playwright
While installing it will ask us to choose the following steps
  • Choose between TypeScript or JavaScript (the default is TypeScript)
  • Name of your Tests folder (you can name it as tests if we don't have any folder called tests or else we can choose e2e)
  • Add a GitHub Actions workflow to easily run tests on CI(i.e playwright.yml)
  • Install Playwright browsers (default is true) or else you can go with manual installation by running:
npx playwright install
I went with npm and after choosing the above steps, it will generate the files mentioned below:
playwright.config.ts
package.json
package-lock.json
tests/
  example.spec.ts
tests-examples/
  demo-todo-app.spec.ts

2.2.2 Installing Cypress

Install via npm

npm install cypress --save-dev

Install via yarn

yarn add cypress --dev

Direct download` -> You can check here by visiting -> Direct download details

Generating the config file and other related files with Cypress is different than Playwright

Go to the terminal and run

npx cypress open

this will open the Cypress launchpad and then follow these steps:-

  • Choose a testing type from E2E Testing or Component Testing.
  • Choose E2E Testing, and then it will open the configuration files.
  • Click Continue, and it will create a ./cypress folder in your root project directory and generate the cypress.config.ts file.
  • The next step is launching a browser, and by default, Chrome will be selected. Click on `Start E2E testing on Chrome.
  • The last and foremost step is to add a test file. We can click on Scaffold example specs, which will generate some example specs inside the ./cypress/e2e directory, or we can click on Create new empty spec, which will create an empty spec file inside the ./cypress/e2e directory.

2.3 Writing Test Cases

We will see how to write a simple test case using both Cypress and Playwright. We will interact with elements and perform actions like clicking buttons, filling out forms, verifying results, etc.

2.3.1 How to write a test case in Playwright?

Let's create a test file inside the tests directory and name it playwright-example.spec.js.

const { test } = require("@playwright/test"); // importing the test block from the module
 
test("get the page heading", async ({ page }) => {
  await page.goto("https://playwright.dev/"); // this makes sure the page is fully interactive then it will execute the next line
  page.getByRole("heading", {
    name: "Playwright enables reliable end-to-end testing for modern web apps.",
  });
});
 

There are various ways to run playwright tests, but the maximum time we go with

npx playwright test --ui

Starts the interactive UI mode.

OR

Runs the tests in a specific file.

npx playwright test playwright-example.spec.js --ui 

Other ways you can try

npx playwright test # Runs the end-to-end tests. (no UI mode runs on the terminal)
npx playwright test --project=chromium # Runs the tests only on Desktop Chrome.
npx playwright test --debug # Runs the tests in debug mode.
npx playwright codegen # Auto generate tests with Codegen.

2.3.2 How to write a test case in Cypress?

Let's create a test file inside the e2e directory and name it cypress-example.cy.js.

it("should get the page heading", () => {
  cy.visit("https://www.cypress.io/");
  /*
  we will need to manually add timeouts if the page takes some time to load.
  In our case we don't need as the page we are visiting it loads faster.
  */
  cy.wait(150) 
  cy.get("h1").contains("Test. Automate. Accelerate.");
});

To run this we can just do,

npx cypress open

Now if we compare the above two tests based on the syntax and asynchrony of the page I would mention them below:

  • The syntax in the Cypress test is chain-based, with each command separated by. (dot) notation.
  • The syntax in the Playwright test is based on await and async, making the test more structured and readable.
  • In the Cypress test, asynchronous behavior is handled through cy.wait() with an arbitrary time value, which may lead to flaky tests if the page load time varies.
  • In the Playwright test, asynchronous behavior is handled naturally with await, ensuring that each action is executed in sequence after the previous action is completed, resulting in more reliable tests. But if the page also takes time to load then it may result in flaky tests.

If I want to summarise this, I would say the Playwright test's syntax is more explicit and readable due to the use of await and async, making it easier to understand the flow of actions. Additionally, Playwright's native asynchrony handling with await eliminates the need for manual timeouts, resulting in more reliable and less error-prone tests compared to the Cypress test.

2.3.3 Interacting with the page elements and performing events

Let's write a test where we will visit the Cypress website and click on a link element and expect an element to be present on the page where we will be navigated to.

Below are the same tests but different approaches.

test("should click on `Documentation` and navigate to the docs page", async ({
  page,
}) => {
  await page.goto("https://www.cypress.io/");
  await page
    .getByRole("main")
    .getByRole("link", { name: "Documentation" })
    .click();
  await expect(page).toHaveURL(
    "https://docs.cypress.io/guides/overview/why-cypress"
  );
  await page.getByRole("heading", { name: "Why Cypress?" }).click();
});
it("should click on `Documentation` link and navigate to the docs page", () => {
  cy.visit("https://www.cypress.io/");
  cy.get("a")
    .invoke("removeAttr", "target")
    .contains("Documentation")
    .click({ force: true });
  cy.url().should(
    "include",
    "https://docs.cypress.io/guides/overview/why-cypress"
  );
  cy.get("h1").contains("Why Cypress?");
});

So from the above two tests, both are performing the same actions but in a different approach. It would be better if we break it down and compare each line of code.

  • Both test cases visit the same URL.
  • For both test cases find and click on a link with the text "Documentation" on the page. But this is not the first solution I wrote when I ran the tests, this is after a lot of googling and looking into their documentation to understand why are we writing it like that.

So the first solution I wrote for Playwright was,

await page.getByRole("link", { name: "Documentation" }).click();

But i got this error mentioned below, and Playwright also suggested what we should use instead.

locator.click: Error: strict mode violation: getByRole('link', { name: 'Documentation' }) resolved to 2 elements:
1) <a href="https://on.cypress.io" class="border borde…>…</a> aka getByRole('main').getByRole('link', { name: 'Documentation' })
2) <a target="_blank" href="https://on.cypress.io" cla…>↵Documentation↵</a> aka getByTitle('Site map').getByRole('link', { name: 'Documentation' })

Now for Cypress I wrote this line first,

cy.get("a").contains("Documentation").click();

But I got the error mentioned below, and Cypress also suggested what to do here.

// Timed out retrying after 4050ms: cy.click() failed because this element is not visible:
 
<span class="font-primary text-[16px] font-semibold leading-[24px] text-teal-500 group-hocus:text-teal-400 sm:text-teal-600 sm:group-hocus:text-teal-500">...</span>
 
/*This element <span.font-primary.text-[16px].font-semibold.leading-[24px].text-teal-500.group-hocus:text-teal-400.sm:text-teal-600.sm:group-hocus:text-teal-500> is not visible because its parent <div.z-50.h-[8px].px-[16px].transition-all.duration-300.group-hover:block.max-sm:absolute.max-sm:-right-full.max-sm:bottom-0.max-sm:left-full.max-sm:top-0.max-sm:transform.sm:mx-[-16px].sm:mt-[-8px].sm:box-content.sm:w-full.lg:absolute.lg:h-[16px].lg:w-auto.lg:max-w-[120px].max-sm:-translate-x-0.sm:hidden> has CSS property: display: none */
 
// Fix this problem, or use {force: true} to disable error checking.

So I changed it to,

cy.get("a").contains("Documentation").click({force: true});

The tests passed for this line but it failed for the next line, and it is because, on executing the above line it is redirecting to the expected link in a different tab hence making the test fail. Believe me, after a lot of googling I got this solution from stack overflow. This will perform the navigation in the same tab.

So we discussed the whole scenario for line number 3 in both playwright and cypress, I wanted you to understand what happened behind the scenes for that line. Okay let's move ahead.

  • The next line in both the test cases is to expect that we are navigated to the requested URL after clicking the link.
  • Then the last line is about checking the heading element present in the current page after navigation.

But you folks will be wondering that why a .click() in the playwright test , why are we clicking the heading element when it is not required. There is reason why we add click() in playwright test when we are visiting and navigating to page url.

There is a term called Auto awaiting in playwright, what it says is page.click() makes sure range of actionability checks.

  • element is Attached to the DOM
  • element is Visible
  • element is Stable, as in not animating or completed animation
  • element Receives Events, as in not obscured by other elements
  • element is Enabled

2.3.4 Handling API Requests in Playwright and Cypress

The emphasis in Playwright and Cypress testing scenarios involving user-triggered API calls should be on detecting behavioural changes and UI updates rather than carefully analysing individual API queries. Testers can validate user experience by focusing on expected outcomes and UI responses, enabling a more holistic testing strategy. This methodology improves adaptability and keeps tests aligned with user-centric interactions. So, if we can anticipate the behaviour and UI changes in the browser after an API call is performed, there is no purpose in explicitly validating each API request after performing an action in the website.

But suppose we are testing a signup flow on the website, and we need to ensure that every time the test runs, the user that we have previously registered is erased from the database before the signup flow steps are performed. In such situation, we must handle the API call prior to the test and perform a delete request to the database to erase the precise user details that we submitted when the test originally ran.

So, in both Playwright and Cypress, we will develop a test in which we will conduct an API request to delete a user if it is already present in the database before continuing to fill out the sign up form and submit it.

How to handle API requests in Playwright?

test("should test the signup flow", async ({ page, request }) => {
  const accessToken = "some-token";
  const allUsersResponse = await request.get(
    "https://example-domain/api/v2/users",
    {
      data: {
        // your request payload goes here...
      },
      headers: { Authorization: `Bearer ${accessToken}` },
    }
  );
  const allUsersData = await allUsers.json();
  const filterUser = allUsersData.find(
    (user: any) => user.email === "mytest-email@email.com"
  );
 
  // do a delete only when it is present in database
  if (filterUser) {
    await request.delete(
      `https://example-domain/api/v2/users/${filterUser.user_id}`,
      {
        headers: { Authorization: `Bearer ${accessToken}` },
      }
    );
  }
 
  await page.goto("https://my-website.com");
  await page.getByLabel("Email address").fill("mytest-email@email.com");
  await page.getByLabel("Password").click();
  await page.getByLabel("Password").fill("<your-password>");
  await page.getByRole("button", { name: "Sign Up" }).click();
  await expect(page).toHaveURL("https://my-website.com/dashboard");
});

How to handle API requests in Cypress?

it("should test the signup flow", () => {
  const accessToken = "some-token";
  cy.request({
    method: "GET",
    url: `https://example-domain/api/v2/users/`,
    body: {
      // your request payload goes here...
    },
    headers: { Authorization: `Bearer ${accessToken}` },
  }).then((response) => {
    const filterUser = response.body.find(
      (user) => user.email === "mytest-email@email.com"
    );
    // do a delete only when it is present in database
    if (filterUser) {
      cy.request({
        method: "DELETE",
        url: `https://example-domain/api/v2/users/${
          filterUser.user_id
        }`,
        headers: { Authorization: `Bearer ${accessToken}` },
      });
    }
  });
  cy.visit("https://my-website.com")
  cy.get("input[type=text]").type("mytest-email@email.com");
  cy.get("input[type=password]").type("<your-password>");
  cy.get("button").contains("Sign Up").click();
  cy.url().should(
    "include",
    "https://my-website.com/dashboard"
  );
});

Now, let us compare these two tests and list their advantages and disadvantages:

Advantages of Playwright:

  • Using the request object, Playwright allows for direct manipulation and analysis of API requests and responses. This can be useful for performing in-depth network interaction analysis.
  • Playwright enables detailed network management for complex scenarios with specialised API interactions, which is ideal for sophisticated testing requirements.

Disadvantages of Playwright:

  • Direct manipulation of API calls within test code might increase complexity and reduce code maintainability.
  • Because the test is so closely related to specific API endpoints and payloads, it is more exposed to API changes.

Advantages of Cypress:

  • Using the cy.request() command, Cypress isolates API interactions, facilitating clearer separation of UI and API interactions. This improves the readability and maintainability of the code.
  • Cypress's cy.request() function allows for simple API response faking for controlled test scenarios, effectively isolating testing from actual API endpoints.
  • Cypress maintains a clear distinction between frontend and backend interactions, which improves test suite organisation and maintainability.

Disadvantages of Cypress:

  • Cypress's cy.request() focuses on response mimicking and does not provide detailed network interaction control.

Conclusion: Forging Your Testing Future

Both Playwright and Cypress are excellent testing automation technologies, each with its own set of advantages. Playwright is a versatile tool due to its quick execution, multi-language support, and ability to handle complicated web applications. Notably, it includes automated waiting, which, like Cypress, increases dependability and simplifies test authoring.

In contrast, Cypress is praised for its developer-centric approach, real-time reloads, and a straightforward dashboard that provides a user-friendly testing experience.

The decision between the two will be heavily influenced by your project's unique requirements and the features you prioritise during the testing process. Regardless of the technology you choose, both claim to boost productivity and assist you in developing high-quality web applications.

Links and references