1. Overview

End-to-end testing is one of the important factors in determining the overall working of the software product. It helps uncover the issues that may have gone unnoticed in the unit and integration testing stages and helps determine whether the software works as expected.

Performing end-to-end tests that may include multiple user steps and journeys is tedious. Therefore, a feasible approach is to perform automation testing of end-to-end test cases.

In this article, we will learn how to automate end-to-end testing with Playwright and TypeScript.

2. What Is Playwright End-to-End Testing?

Playwright end-to-end testing is the process that helps developers and testers simulate real user interactions with websites. With Playwright, we can automate tasks like clicking buttons, filling out forms, and navigating through pages to check if everything works as expected. It works with popular browsers like Chrome, Firefox, Safari, and Edge.

3. Prerequisites for Playwright End-to-End Testing

To use Playwright**, install** NodeJS version 18 or higher and TypeScript. There are two ways to install Playwright:

  • Using Command Line
  • Using VS Code

However, in this article, let’s use VS Code to install Playwright.

  1. After installing Playwright from the VS Code marketplace, let’s open the command panel and run the command Install Playwright:install playwright command
  2. Let’s install the required browsers. We will then click on OK:install browsers for playwright
  3. After installation, we will get the folder structure containing the dependencies in the package.json file:package json file

4. How to Perform End-to-End Testing With Playwright?

End-to-end testing covers the use cases that the end users ideally follow. Let’s consider the LambdaTest eCommerce Playground website for writing end-to-end tests.

We will use a cloud-based testing platform like LambdaTest to achieve greater scalability and reliability for end-to-end testing. LambdaTest is an AI-powered test execution platform that offers automation testing using Playwright across 3000+ real browsers and operating systems.

4.1. Test Scenario 1

  1. Register a new user on the LambdaTest eCommerce Playground website.
  2. Perform an assertion to check that the user has been registered successfully.

4.2. Test Scenario 2

  1. Perform assertion to check that the user is logged in.
  2. Search for a product on the home page.
  3. Select the product and add it to the cart.
  4. Perform assertion to check that the correct product is added to the cart.

4.3. Test Configuration

Let’s create a fixture file that will authenticate once per worker by overriding the storageState fixture. We can use testInfo.parallelIndex to differentiate between workers.

Further, we can use the same fixture file to configure LambdaTest capabilities. Now, let’s create a new folder named base and a new file page-object-model-fixture.ts.

The first block contains import statements for npm packages and files from other project directories. We will import expect, chromium, and test as baseTest variables and use dotenv to fetch environment variables. We then declare page object class instances directly in the fixture file and test.

The next block involves adding the LambdaTest capabilities:

const modifyCapabilities = (configName, testName) => {
    let config = configName.split("@lambdatest")[0];
    let [browserName, browserVersion, platform] = config.split(":");
    capabilities.browserName = browserName;
    capabilities.browserVersion = browserVersion;
    capabilities["LT:Options"]["platform"] =
        platform || capabilities["LT:Options"]["platform"];
    capabilities["LT:Options"]["name"] = testName;
};

We can easily generate these capabilities using the LambdaTest Capabilities Generator. The next block of lines will use the LambdaTest capabilities by customizing and creating a project name. The project name is ideally the browser, browser version, and platform name combination that could be used in the format chrome:latest:macOS Sonoma@lambdatest:

projects: [
    {
        name: "chrome:latest:macOS Sonoma@lambdatest",
        use: {
            viewport: {
                width: 1920,
                height: 1080,
            },
        },
    },
    {
        name: "chrome:latest:Windows 10@lambdatest",
        use: {
            viewport: {
                width: 1280,
                height: 720,
            },
        },
    },

The next block of code has been divided into two parts. In the first part, the testPages constant variable is declared and has been assigned to baseTest extends the pages type declared initially in the fixture file as well as the workerStorageState:

const testPages = baseTest.extend<pages, { workerStorageState: string; }>({
    page: async ({}, use, testInfo) => {
        if (testInfo.project.name.match(/lambdatest/)) {
            modifyCapabilities(testInfo.project.name, `${testInfo.title}`);
            const browser =
                await chromium.connect(
                    `wss://cdp.lambdatest.com/playwright?capabilities=
                    ${encodeURIComponent(JSON.stringify(capabilities))}`
                );
            const context = await browser.newContext(testInfo.project.use);
            const ltPage = await context.newPage();
            await use(ltPage);

            const testStatus = {
                action: "setTestStatus",
                arguments: {
                    status: testInfo.status,
                    remark: getErrorMessage(testInfo, ["error", "message"]),
                },
            };
            await ltPage.evaluate(() => {},
                `lambdatest_action: ${JSON.stringify(testStatus)}`
            );
            await ltPage.close();
            await context.close();
            await browser.close();
        } else {
            const browser = await chromium.launch();
            const context = await browser.newContext();
            const page = await context.newPage();
            await use(page);
        }
    },

    homePage: async ({ page }, use) => {
        await use(new HomePage(page));
    },
    registrationPage: async ({ page }, use) => {
        await use(new RegistrationPage(page));
    },
});

In the second part of the block, the workerStorageState is set where each parallel worker is authenticated once. All tests use the same authentication state a worker runs:

storageState: ({ workerStorageState }, use) =>
    use(workerStorageState),
    
workerStorageState: [
    async ({ browser }, use) => {
        const id = test.info().parallelIndex;
        const fileName = path.resolve(
            test.info().project.outputDir,
            `.auth/${id}.json`
        );
    },
],

The authentication will be done once per worker with a worker-scoped fixture. We need to ensure we authenticate in a clean environment by unsetting the storage state:

const page = await browser.newPage({ storageState: undefined });

The authentication process should be updated in the fixture file next. It includes the user registration steps, as discussed in test scenario 1.

4.4. Implementation: Test Scenario 1

First, we will create two-page object classes to hold locators and functions for interacting with each page’s elements. Let’s create a new folder named pageobjects in the tests folder. The first-page object class will be for the homepage:

import { Page, Locator } from "@playwright/test";
import { SearchResultPage } from "./search-result-page";

export class HomePage {
    readonly myAccountLink: Locator;
    readonly registerLink: Locator;
    readonly searchProductField: Locator;
    readonly searchBtn: Locator;
    readonly logoutLink: Locator;
    readonly page: Page;

    constructor(page: Page) {
        this.page = page;
        this.myAccountLink = page.getByRole("button", { name: " My account" });
        this.registerLink = page.getByRole("link", { name: "Register" });
        this.logoutLink = page.getByRole("link", { name: " Logout" });
        this.searchProductField = page.getByPlaceholder("Search For Products");
        this.searchBtn = page.getByRole("button", { name: "Search" });
    }

    async hoverMyAccountLink(): Promise<void> {
        await this.myAccountLink.hover({ force: true });
    }

    async navigateToRegistrationPage(): Promise<void> {
        await this.hoverMyAccountLink();
        await this.registerLink.click();
    }
}

On the homepage, we first need to hover over the “My account” link to open the menu dropdown and click the register link to open the registration page:

click role button

In the Chrome “DevTools” window, the “My account” WebElement role is a button. Hence, let’s locate this link using the following code:

this.myAccountLink = page.getByRole("button", { name: " My account" });

Let’s hover the mouse over the MyAccount**Link to open the dropdown to view and click on the register link:

async hoverMyAccountLink(): Promise<void> {
    await this.myAccountLink.hover({ force: true });
}

The register link must be located and clicked to open the registration page. We can notice the registerLink locator in the Chrome DevTools; the role of this WebElement is that of a link:

register button span

The following function will hover over the MyAccountLink, and when the dropdown opens, it will locate and click on the registerLink:

async navigateToRegistrationPage(): Promise<void> {
    await this.hoverMyAccountLink();
    await this.registerLink.click();
}

Let’s create the second-page object class for the registration page, which will hold all the fields and functions for performing interactions:

async registerUser(
    firstName: string,
    lastName: string,
    email: string,
    telephoneNumber: string,
    password: string
): Promise<MyAccountPage> {
    await this.firstNameField.fill(firstName);
    await this.lastNameField.fill(lastName);
    await this.emailField.fill(email);
    await this.telephoneField.fill(telephoneNumber);
    await this.passwordField.fill(password);
    await this.confirmPassword.fill(password);
    await this.agreePolicy.click();
    await this.continueBtn.click();

    return new MyAccountPage(this.page);
}

We can use the getByLabel() function to locate fields and then create the registerUser() function to interact and complete registration.

Let’s create my-account-page.ts for header assertions and update the fixture file for the registration scenario. We will use navigateToRegistrationPage() to visit the registration page and assert the Register Account title. Then, we will call registerUser() from the RegistrationPage class with data from register-user-data.json.

After the registration, we will assert to check that the page header “Your Account Has Been Created! is visible on the My Account page.

4.5. Implementation: Test Scenario 2

We will add a product in the second test scenario and verify that the cart details show the correct values.

The first assertion checks that the user is logged in. It does so by hovering over MyAccountLink with a mouse and checking that the Logout link is visible in the menu.

Now, we will search for a product using the search box from the home page.

We will search for an iPhone by typing in the value in the search box and clicking the search button. The searchForProduct() function will help us search the product and return a new instance of the SearchResultPage:

const searchResultPage = await homePage.searchForProduct("iPhone");
await searchResultPage.addProductToCart();

The search results will appear on the searchResultPage. The addProductToCart() function will mouse hover over the first product on the page retrieved in the search result. It will click the Add to Cart button when the mouse hovers over the product.

A notification pop-up will appear, displaying a confirmation text:

await expect(searchResultPage.successMessage).toContainText(
    "Success: You have added iPhone to your shopping cart!"
);
const shoppingCart = await searchResultPage.viewCart();

To confirm that the cart has the product, first assert the confirmation text on the pop-up, then click the viewCart button to navigate to the shopping cart page.

An assertion finally verifies that the product name iPhone in the shopping cart confirms the addition of the searched product:

await expect(shoppingCart.productName).toContainText("iPhone");

4.6. Test Execution

The following command will run the tests on the Google Chrome browser on the local machine:

$ npx playwright test --project=\"Google Chrome\"

The following command will run the tests on the latest Google Chrome version on macOS Sonoma on the LambdaTest cloud grid:

$ npx playwright test --project=\"chrome:latest:macOS Sonoma@lambdatest\"

Let’s update the respective commands in the scripts block in the package.json file:

"scripts": {
    "test_local": "npx playwright test --project=\"Google Chrome\"",
    "test_cloud": "npx playwright test --project=\"chrome:latest:macOS Sonoma@lambdatest\""
}

So, if we want to run the tests locally, run the command:

$ npm run test_local

To run the tests over the LambdaTest cloud grid, we can run the command:

$ npm run test_cloud

After the test execution, we can view the test results on the LambdaTest Web Automation Dashboard and in the build details window:

test details lambdatest

The build details screen provides information such as the platform, browser name and its respective versions, video, logs, commands executed, and time taken to run the tests.

5. Conclusion

Playwright is a lightweight and easy-to-use test automation framework. Developers and testers can configure it easily with multiple programming languages.

Using Playwright with TypeScript is much more flexible and simple, as we don’t have to write too much boilerplate code for configuration and setup. We need to run a simple command for installation and, right away, start writing the tests.

The source code used in this article is available over on GitHub.