Choosing an E2E testing framework

by Nikola Salim  on November 7, 2023

As web applications evolve and grow in complexity, it becomes more and more important to ensure that their core functionalities work as intended across different browsers and platforms, providing a consistent experience for all end users. One of the approaches that Geckotech uses to validate that the code written is production-ready is end-to-end (E2E) testing.

Let's take one of our most recent use cases, for example: a registration onboarding flow. Like most onboarding flows, this one has multiple steps, forms, validations, an integration with a payment service provider, redirections and confirmations. The flow also adapts depending on the data being entered by the user. By the end of it, we also need to confirm in the back-office dashboard whether the user has been successfully onboarded.

It's a complex flow with a clear set of steps, a beginning and an end, making E2E testing perfect for validating it.

Exploring E2E framework options

Great! But which of the many E2E frameworks available should we choose? Each one has its own features and particularities, pros and cons. We decided to explore our options. Let me walk you through our thought process.

After the initial round of research, where we considered multiple frameworks, the most important takeaway was that you need to define your own requirements for your particular project. No project is the same, so it is important to understand which features are not necessarily important, which are good-to-haves and which are essential. With this in mind, we managed to narrow it down to 2 candidates: Cypress and Playwright.

In general, both Cypress and Playwright match our requirements:

  • Cross-browser support
  • JavaScript support (the app in question is written in Angular)
  • Useful debugging tools
  • Handy tools for generating tests by recording your actions
Some differences between the two include:
  • Cypress is a more "all-in-one" type of framework.
  • Playwright is a bit more flexible and performed faster on our end.
  • Cypress runs directly within the browser and executes tests in the same JavaScript runtime as the application being tested.
  • Playwright runs tests outside the browser context using automation drivers for various browsers.
  • Playwright also supports TypeScript, Java, Python and .NET - in addition to JavaScript.

Why Playwright was chosen

While both frameworks are solid choices, Playwright stands out for this project for the following three reasons:

  1. Playwright makes it easier to create multiple browser contexts to deliver full test isolation. This is important because some of the onboarding steps require us to open parallel browser contexts, like for accessing the client's dashboard and checking if the user was successfully registered.
  2. Playwright offers better iframe support. In our case, one of the onboarding steps includes accessing an email client to activate the newly created account, and the body of the email is embedded in an iframe.
  3. Playwright offers native mobile emulation of Google Chrome for Android and Mobile Safari. Many of our clients onboard via their mobile phones, so it's important to replicate the steps as accurately as possible.

As a bonus, it's always nice to check some "stats". Although these shouldn't be taken as sole motivation to pick a framework, they're good indicators:

How frequently the project is maintained: Playwrights issue resolution averages 3 days, while Cypress' averages 15 days;

The number of GitHub stars each project has (at this moment): Playwright has 51.4k â­Â and Cypress has 43.4k â­Â – even though Cypress is a few years older than Playwright;

First impressions

Does Playwright live up to expectations? So far, the answer is yes. Setting up Playwright was easy, and its syntax feels familiar, especially with the Promise-based async/await approach. Debugging has been stress-free thanks to the built-in reporters, and the assertion API design resembles popular JS testing frameworks like Jest, making it intuitive to start writing tests:

test('has title', async ({page}) => {
    await page.goto('');
    // Expect a title "to contain" a substring.
    await expect(page).toHaveTitle(/Playwright/);

Although working with multiple browser contexts required some adjustment, we quickly developed helper functions to navigate through the possibilities. For instance, the `runOnNewPageContext` function opens a new browser context and executes an `actions` function that can perform whichever actions and assertions are needed:

export async function runOnNewPageContext(pageUrl, actions) {

    const browser = await chromium.launch();
    const context = await browser.newContext();
    const newPage = await context.newPage();
    await newPage.goto(pageUrl);

    await actions(newPage);

    await context.close();
    await browser.close();


While we continue to experiment and learn more about Playwright, it has proven to be a solid choice in our case.