1

So I'm trying to make a custom selector with my Playwright tests, but I can't figure out how to make an empty locator. The goal of this is to pass in a string of test IDs to get a nested element, this is just to make things more readable in my tests. The goal is something like this:

export class CustomPage {
   protected readonly page: Page;
   
   constructor(page: Page) {
      this.page = page;
   }

   public getNestedElement(testIds: Array<string>) {
      let locator = //instantiate locator with this.page

      foreach(const id of testIds) {
         locator = locator.getByTestId(id);
      }

      return locator;
   }
}

Thank you for any help you can offer!

4
  • This seems unusual to want to do. An empty locator makes no sense. I'm not sure why you're doing this, but if you provide a bit more context, surely there's a better way to do whatever it is you're trying to do. The loop here is pointless--it'd just set and return the last locator in testIds. But that also makes no sense because you can't use for .. of over a string in the first place. Can you clarify the fundamental goal, requirements and code? Thanks. Commented May 16, 2023 at 15:12
  • @ggorlen It's meant to get a nested element in a more readable way. The loop is meant to appened the next locator to the last locator (ex: page.getByTestId('').getByTestId('')). As for the for loop; that's actually meant to be a string array, a typo on my part. Commented May 16, 2023 at 15:18
  • Why not query for the nested element directly, using a single locator? Is the component recursive? Test ids aren't generally considered good practice to begin with. There's probably a better way to select whatever you're trying to select. If you don't mind showing the element and providing full context, someone can probably provide a cleaner way to access it than using unusual patterns like this. Commented May 16, 2023 at 15:22
  • @ggorlen referencing the nested element directly would result in several different elements being referenced. I don't have control over whether or not I use a test id, as I'm doing this for work and this is their preferred way. Commented May 16, 2023 at 15:25

1 Answer 1

2

What you seem to be asking for is possible:

public getNestedElementByTestIds(testIds: Array<string>) {
  if (testIds.length === 0) {
    throw Error("testIds must contain at least one element");
  }

  let locator = this.page.getByTestId(testIds[0]);

  for (const testId of testIds.slice(1)) {
    locator = locator.getByTestId(testId);
  }

  return locator;
}

Runnable example:

const playwright = require("playwright"); // ^1.30.1

const html = `
<div data-testid="foo">
  ignore this
  <div data-testid="bar">
    ignore this
    <div data-testid="baz">
      ignore this
      <div data-testid="quux">
        PRINT ME
      </div>
    </div>
  </div>
</div>`;

const getNestedElementByTestIds = (page, testIds) => {
  if (testIds.length === 0) {
    throw Error("testIds must contain at least one element");
  }

  let locator = page.getByTestId(testIds[0]);

  for (const testId of testIds.slice(1)) {
    locator = locator.getByTestId(testId);
  }

  return locator;
}

let browser;
(async () => {
  browser = await playwright.chromium.launch();
  const page = await browser.newPage();
  await page.setContent(html);
  console.log((await getNestedElementByTestIds(page, [
    "foo", "bar", "baz", "quux"
  ]).textContent()).trim()); // => PRINT ME
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

Caveat emptor: This may be too clever. I would rather chain 2-3 getByTestIds in the caller, or use a more robust selection mechanism than test ids and rely on direct, user visible properties.

In general, I'm not eager to abstract Playwright calls into helpers if it's avoidable, even at the cost of some duplication across tests. They're already quite high-level. Anything more than a simple POM may become challenging to maintain for anyone but the author.

Sign up to request clarification or add additional context in comments.

1 Comment

I had thought about this solution, but I thought maybe there was a better way. There doesn't seem to be a better way, though. Thank you for the help!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.