Sponsored Link
Looking for a unified way to  viewmonitor, and  debug Playwright Test Automation runs?
Try the Playwright Dashboard by today. Use the coupon code PWSOL10 for a 12 month 10% discount.

How do you create dynamic locators with Playwright?

This is a very popular question that gets asked quite often in the Playwright Discord help channel. One of the main reasons I believe this is so popular is because no one wants to write the same code over and over for the same operation with a slightly different input. There are a lot of different approaches to handling this, I'll share mine.

The example website I am using isn't as straight forward as others might be as I am going to be using a unique ID that I am grabbing from a network request, to pass into the dynamic locator. The site I am testing against is https://practicesoftwaretesting.com.

On this site the page loads from an API backend, and populates a grid of products you can interact with. I want to create a dynamic locator that allows me to pass in a parameter to dynamically select a different item based on what I pass in. The HTML for the Combination Pliers item is listed below.

<a
  class="card"
  style="text-decoration: none; color: black"
  data-test="product-01H8ABYWDYXDAYW9HM37N5FC5F"
  href="#/product/01H8ABYWDYXDAYW9HM37N5FC5F"
></a>
<div class="card-img-wrapper">
  <img class="card-img-top" src="assets/img/products/pliers01.jpeg" />
</div>
<div class="card-body">
  <h5 data-test="product-name" class="card-title">Combination Pliers</h5>
</div>
<div class="card-footer">
  <span class="float-end text-muted">
    <span data-test="product-price">$14.15</span>
  </span>
</div>

For this example we will use the data-test attribute on the link which uses product-{unique_id} as the value. We will build a dynamic locator that allows us to pass in a unique_id to interact with the items we want to interact with. The below code is listed in my homePage.ts file. I am setting productId to a dynamic locator. See the example on using it below.

// lib/pages/homePage.ts

import { Page } from "@playwright/test";

export class HomePage {
  readonly productId = (id: string) =>
    this.page.locator(`[data-test="product-${id}"]`);

  readonly addToCart = this.page.locator('[data-test="add-to-cart"]');
  readonly navCart = this.page.locator('[data-test="nav-cart"]');

  async goto() {
    await this.page.goto("/#/");
  }

  constructor(private readonly page: Page) {}
}

Implementing this in a spec, requires us to actually have the unique_id we want to pass in. await homePage.productId(productId).click(); To get the unique productId I am using page.route() in order to intercept network traffic and utilize those values in my test. You can read more about that with a simple example here.

// tests/checkout.spec.ts

import { expect } from "@playwright/test";
import { test, CheckoutPage, HomePage } from "@pages";
import { getLoginToken } from "@datafactory/login";
import { productIdRoute } from "@fixtures/productPageRoute";

test.describe("Basic UI Checks", () => {
  const username = process.env.CUSTOMER_01_USERNAME || "";
  const password = process.env.CUSTOMER_01_PASSWORD || "";
  let productId;

  test.beforeEach(async ({ page }) => {
    // Gets Login Token via API call
    const token = await getLoginToken(username, password);

    // Sets Local Storage with Login token so user is logged in
    await page.addInitScript((value) => {
      window.localStorage.setItem("auth-token", value);
    }, token);

    productId = await productIdRoute(page);
  });

  test("Add to Cart and Checkout", async ({ page }) => {
    const homePage = new HomePage(page);
    const checkoutPage = new CheckoutPage(page);

    await homePage.goto();
    await homePage.productId(productId).click();
    await homePage.addToCart.click();
    await homePage.navCart.click();

    await checkoutPage.proceed1.click();
...
  });
});

My implementation of the page.route() in this example will be utilizing an asynchronous function shown below.

// lib/fixtures/productPageRoute.ts

import { HomePage } from "@pages";

/**
 * Sets up a route to retrieve the product ID from the API response.
 * @param page - The Playwright page object.
 * @param name - Optional name of the string to get the ID for.
 * @returns The product ID.
 * @example
 * const page = await browser.newPage();
 * const productId = await productIdRoute(page); // Gets the second product ID
 *
 * const productId = await productIdRoute(page, "Pliers") // Gets the ID for the product named "Pliers"
 */
export async function productIdRoute(page: any, name?: string) {
  let productId;

  await page.route(
    "https://api.practicesoftwaretesting.com/products?between=price,1,100&page=1",
    async (route) => {
      let body;
      const response = await route.fetch();
      body = await response.json();
      if (name) {
        productId = findIdByName(body, name);
        console.log("pid: " + productId);
      } else {
        // Get the second product in the list
        productId = body.data[1].id;
      }
      route.continue();
    }
  );

  const homePage = new HomePage(page);
  await homePage.goto();

  return productId;
}

function findIdByName(json: any, name: string): string | undefined {
  const data = json.data;
  for (let i = 0; i < data.length; i++) {
    if (data[i].name === name) {
      return data[i].id;
    }
  }
  return undefined;
}

I've built the router to intercept the network traffic which returns via JSON, and returns the id. By only passing in the page to the function, it will grab the 2nd id on the page. I added a optional parameter name which if passed will be used to dynamically iterate through the json body response and if the name of the product matches (exactly, case and everything) than return that id to be used.

With all this built I can search by name, get the underlying data-id and then use that data-id to dynamically locate the element on the page. This allows for a ton of flexibility with only a few lines of code. Code for the examples can be found below.

adding dynamic locator and route to grab productId by BMayhew · Pull Request #4 · playwrightsolutions/playwright-practicesoftwaretesting.com
Example using Playwright against site https://practicesoftwaretesting.com - adding dynamic locator and route to grab productId by BMayhew · Pull Request #4 · playwrightsolutions/playwright-practicesoftwaretesting.com

Thanks for reading! If you found this helpful, reach out and let me know on LinkedIn or consider buying me a cup of coffee. If you want more content delivered to you in your inbox subscribe below.