automative

by

photo

Cypress vs Playwright

October 1, 2024

When I started in test automation, Cypress was the only tool I used. At first I wrote very simple tests without knowing the best practices, and over time I moved on to more complex test cases. At the same time I was learning JavaScript. Once I had the basics of JS, I started asking more questions about Cypress: what you can test with it, how it works, and so on. Cypress is known for its easy syntax and it works well for end-to-end testing. Playwright feels similar, but for someone new to testing, I think Cypress is easier to start with.

In this article, we'll compare Cypress and Playwright, two popular tools in e2e testing, by examining their key features, use cases, and performance. This comparison will help you decide which tool is the best fit for your testing needs.


Cypress

Cypress is an end-to-end testing framework designed to make testing modern web applications easier and more reliable. It allows developers and testers to write tests that interact with a web application just like a user would, entering text, navigating pages, and more. Cypress is popular for testing SPAs and supports testing front-end behavior, but it is limited to the browser environment (we will talk more about that later).

Playwright

Playwright is an open-source testing framework developed by Microsoft that allows automated testing of web applications across multiple browsers. Playwright supports all modern engines including Chromium, WebKit, and Firefox. It's super easy to install.

During my time at my previous roles, I had the chance to develop strong skills in both Cypress and Playwright. In my first role, Cypress was already integrated with a few tests, and as a beginner, I found it easy to get started. Later, at my next company, Cypress had around 2000 tests integrated. Although Playwright wasn’t fully integrated at that point, I took the initiative to learn and experiment with it, despite having limited prior experience.


Downloads Comparison

Monthly npm downloads of Cypress and Playwright, January to September 2024. Source: npm


Comparing Playwright and Cypress

Playwright and Cypress are both powerful tools for end-to-end testing, but their features and use cases differ significantly. Here's a detailed comparison of these two frameworks based on key aspects:

  1. Cross-Browser Support:

    Playwright supports Chromium, Firefox, and WebKit (Safari), offering extensive cross-browser testing capabilities.

    Cypress works with Chrome, Edge, Electron, and Firefox. WebKit (Safari) support is still experimental. Cypress runs tests in the browsers that are already installed on your machine. Playwright works differently here: it downloads its own browsers with npx playwright install, so every machine runs the tests on the same browser versions.

  2. Supported Platforms:

    Playwright can emulate mobile browsers for Android and iOS, and it runs on Windows, macOS, and Linux. It has ready device profiles, for example devices['iPhone 13'], that set the screen size, user agent, and touch support. Cypress also has ready device names, for example cy.viewport('iphone-x'), but they only change the screen size.

  3. Performance and Parallelization:

    Playwright runs tests in parallel by default, using several workers on one machine. This makes big test suites much faster. Cypress can run tests in parallel across several CI machines with Cypress Cloud. To run tests in parallel on one machine, you need a third-party plugin.

  4. Network Interception and Control:

    Playwright: Offers advanced network interception capabilities using page.route(). You can modify requests before they are sent to the server, such as changing HTTP methods, headers, or redirecting requests to mock services. Playwright also provides seamless mocking of responses directly in the request flow.
    Cypress: Allows request interception with cy.intercept() and supports pre-request modifications like altering headers, methods, or mocking responses. Cypress also enables redirecting requests to mock services. For most cases, both tools can do the same things here. The main difference is the code style: Playwright uses async/await, and Cypress uses its own command chain.

    // Playwright, example 1: change the request before it reaches the server
    await page.route('**/api/v1/users', (route) => {
      route.continue({
        headers: {
          ...route.request().headers(),
          'x-custom-header': 'PlaywrightIntercept',
        },
      });
    });
    
    // Playwright, example 2: return a mocked response
    await page.route('**/api/v1/users', (route) => {
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ success: true, message: 'Mocked Response' }),
      });
    });
    
    // Cypress, example 1: change the request before it reaches the server
    cy.intercept('GET', '**/api/v1/users', (req) => {
      req.headers['x-custom-header'] = 'CypressIntercept';
    });
    
    // Cypress, example 2: return a mocked response and check it
    cy.intercept('GET', '**/api/v1/users', {
      statusCode: 200,
      body: { success: true, message: 'Mocked Response' },
    }).as('getUsers');
    
    cy.visit('/users');
    cy.wait('@getUsers').then((interception) => {
      expect(interception.response.statusCode).to.equal(200);
      expect(interception.response.body.message).to.equal('Mocked Response');
    });
    
  5. Changing Requests Based on Conditions:

    Sometimes you want to change a request or a response only in some cases, for example based on the URL, a query parameter, or the request body. Both tools can do this. In Playwright you check the request inside page.route(). In Cypress you do the same inside the cy.intercept() handler, which gets the full request. The result is the same. It mostly comes down to which code style your team prefers.

    // Playwright: change the request only for one user
    await page.route('**/api/v1/users/**', (route, request) => {
      if (request.url().includes('specificUserId')) {
        route.continue({
          headers: { ...request.headers(), 'x-user-id': 'specialUser123' },
        });
      } else {
        route.continue();
      }
    });
    
    // Cypress: the same thing
    cy.intercept('GET', '**/api/v1/users/**', (req) => {
      if (req.url.includes('specificUserId')) {
        req.headers['x-user-id'] = 'specialUser123';
      }
      // Other requests go to the server without changes
    });
    
    // Playwright: return a different mock based on the URL
    await page.route('**/api/v1/users/**', (route) => {
      const isSpecial = route.request().url().includes('special');
      route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ message: isSpecial ? 'Special User Mock' : 'Generic Mock' }),
      });
    });
    
    // Cypress: the same thing
    cy.intercept('GET', '**/api/v1/users/**', (req) => {
      const isSpecial = req.url.includes('special');
      req.reply({
        statusCode: 200,
        body: { message: isSpecial ? 'Special User Mock' : 'Generic Mock' },
      });
    });
    
  6. Multi-Tab Handling:

    Playwright offers native support for handling multiple tabs or windows, making it ideal for complex navigation flows. Cypress, however, lacks native multi-tab support and relies on workarounds, such as forcing new tabs to open in the same window.

    // Playwright: Native Multi-Tab Handling
    
    // Open a new tab
    const [newPage] = await Promise.all([
      context.waitForEvent('page'),
      page.click('a[target="_blank"]') // Click link opening a new tab
    ]);
    
    // Interact with the new tab
    await newPage.waitForLoadState();
    await newPage.fill('#input', 'Playwright multi-tab test');
    await newPage.click('button.submit');
    
    // Switch back to the original tab
    await page.bringToFront();
    
    // Cypress: Workaround for Multi-Tab Handling
    
    // Force links to open in the same tab by removing 'target="_blank"'
    cy.get('a[target="_blank"]').invoke('removeAttr', 'target').click();
    
    // Interact with the newly loaded page
    cy.url().should('include', '/new-tab');
    cy.get('#input').type('Cypress single-tab workaround');
    cy.get('button.submit').click();
    
  7. Test Isolation:

    Both frameworks support test isolation by default. The perception of weaker isolation in Cypress often stems from tester practices rather than the tool itself.

  8. Programming Language Support:

    Cypress supports JavaScript and TypeScript, while Playwright extends its capabilities to additional languages, including Python, Java, and C#, making it a better choice for teams working in diverse language environments.

  9. Debugging Tools:

    Cypress simplifies debugging with its interactive Test Runner and time-travel snapshots. The .debug() command is particularly useful for inspecting the state during test execution.

    Playwright, on the other hand, integrates deeply with tools like VS Code, providing powerful debugging options such as breakpoints and a trace viewer.


Debugging

Debugging in Cypress and Playwright works in different ways. In Cypress, the main tools are .pause(), .debug(), and the time-travel snapshots in the Test Runner. When you use.debug(), Cypress will freeze the test and print the selector results in the Chrome console. You can then hover over the HTML element in Chrome to check if your selector is working.

However, the issue is that you can only see the value for that particular line. You don’t get access to any other states created by the test code. Adding breakpoints or using the debugger statement doesn’t help much because Cypress runs all code first and handles the rest asynchronously. If you want to inspect async code, you’d need to place the debugger inside a resolve function to make sure the code has finished. But even then, you can only see the value for that line, and you can't check the rest of the test state.

Another option is to use a third-party library that turns Cypress commands into Promises. This allows you to add debugger statements after await to inspect variables that have been awaited. The problem with this approach is that Cypress wasn’t designed to work with async/await, so you may run into issues. I tried this and faced several problems, so I switched back to using Cypress's built-in .debug() function.

Playwright, on the other hand, integrates well with Visual Studio Code and fully supports promises and async/await. You can debug your tests directly in VSCode, stop at any point, and check the state of any variable. It even highlights DOM elements in Chromium when you hover over a selector expression in VSCode (it works with multi-dot selectors too!).

Playwright also has its own inspector where you can run tests step by step while watching them in Chromium. This allows you to evaluate selectors in real-time while the test is paused.


Powerful Selectors & Nested Selectors

Both Cypress and Playwright support powerful ways to select elements on a page. While they share many similarities, the key difference lies in how selectors are managed and how each framework approaches deeply nested structures.Playwright has built-in support for CSS, XPath, text, and ARIA role selectors. Cypress has CSS selectors and text search with cy.contains() built in. For role and label selectors, you can add the open-source Testing Library plugin. It needs a little setup, but then it works in a similar way.

Powerful Selectors

// Playwright Selector Examples

// CSS Selector
await page.click('button.submit-btn');

// Text Selector
await page.click('text="Submit"');

// XPath Selector
await page.locator('//button[text()="Submit"]').click();

// ARIA Role Selector
await page.getByRole('button', { name: 'Submit' }).click();
    
// Cypress with Testing Library Selector Example

// Using Testing Library's findByRole
cy.findByRole('button', { name: /submit/i }).click();
    
// Using Testing Library's findByText
cy.findByText('Submit').click();

// Built-in Cypress text search (no plugin needed)
cy.contains('button', 'Submit').click();

// Basic Cypress CSS Selector
cy.get('button.submit-btn').click();

In short, Playwright has CSS, text, XPath, and ARIA role selectors built in. Cypress has CSS and text built in, and needs Testing Library for role and label selectors. After that setup, both work in a similar way.


Conclusion

Both tools are powerful, and the choice often depends on specific project needs. Cypress is ideal for simple to moderately complex web apps with a focus on ease of use and a strong developer experience. Playwright is better suited for more complex scenarios, especially if cross-browser testing, multi-page interactions, or deep network control are required.