Machtyn
Machtyn

Reputation: 3272

Playwright selecting element with text= or hastext with exact match

I'm using playwright nodejs. I've written myself a little dynamic selector function to select the page number button on a dataTable.

pageNumberButton(page, table_id, page_number) {
   page.locator(`[aria-controls=${table_id}]`, {hasText: page_number});
}

I've also tried:

pageNumberButton(page, table_id, page_number) {
   page.locator(`[aria-controls=${table_id}] text=${page_number}`);
}

However, I can't seem to make it do an exact match.

Suppose my dataTable has 13 pages:

dataTable button controls showing Previous, 1, 2, 3, 4, 5, ..., 13, Next button

and I wish to click on page 1. so I issue the following command: await pageNumberButton(page, "resultsTable", "1").click();

But I get a strict-mode error, since there are two results: 1 and 13.

What would be the best, or good, way to create this little selector dynamically so that I can do an exact match for the button?

Upvotes: 11

Views: 50777

Answers (5)

ggorlen
ggorlen

Reputation: 56965

Exact matching is possible with {exact: true}. See this example from the docs:

await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();

Note that this does normalize whitespace, which is probably the desired behavior in most cases, so it's not quite "exact" as advertised.

Applied to your case:

page
  .locator(`[aria-controls="${tableId}"]`)
  .getByText(pageNumber, {exact: true});

It's possible to use the text= syntax in the locator as you've attempted, as long as you separate the CSS and text= locators into two calls and use quotes inside the text="" selector. Applied to your example:

page
  .locator(`[aria-controls="${table_id}"]`)
  .locator(`text="${page_number}"`);

Here's a complete, runnable demonstration:

import {expect, test} from "@playwright/test"; // ^1.39.0

const html = `<!DOCTYPE html><html><body>
<div aria-controls="foo">1</div>
<div aria-controls="foo">13</div>
</body>
</html>`;

const pageNumberButton = (page, tableId, pageNumber) => {
  return page
    .locator(`[aria-controls="${tableId}"]`)
    .locator(`text="${pageNumber}"`);
};

test("aria-controls='foo' with text '1' is visible", async ({page}) => {
  await page.setContent(html);
  await expect(pageNumberButton(page, "foo", 1)).toBeVisible();
});

Forgetting quotes in the text="" syntax is a subtle gotcha, which is why I prefer the more explicit getByText("...", {exact: true}).

Use quotes on the [aria-controls="..."] CSS selector as well as the text="" selector.

Don't forget to return the locator from your pageNumberButton function.


Yet another approach is the :text-is("") pseudoselector which "matches the smallest element with exact text. Exact matching is case-sensitive, trims whitespace and searches for the full string" according to the docs:

page.locator(`[aria-controls="${tableId}"]:text-is("${pageNumber}")`);

Further remarks:

  • Regex is possible, but I'd avoid this if possible because it's easy to forget that regex special characters like . are treated differently and wind up with subtle bugs.
  • Generally, .filter() should be avoided when possible. The syntax is less clear than a single chain of locators.
  • CSS attributes aren't considered best practice in tests, so you may consider using a test id or, better, locating based on user-visible attributes.

Upvotes: 9

Paul Tew
Paul Tew

Reputation: 41

I recommend text-is instead of hasText.

https://playwright.dev/docs/other-locators

Upvotes: 4

Adam A
Adam A

Reputation: 14618

Playwright allows chaining locators. You can make a locator that solves your issue and also is simple and readable - no regex required. First you find the aria-controls element, then you find the correct button inside it:

function pageNumberButton(page, table_id, page_number) {
   return page.locator(`[aria-controls=${table_id}]`)
       .getByText(`${page_number}`, { exact: true });
}
await pageNumberButton(page, "resultsTable", "1").click();

should work fine after that.

Upvotes: 2

mackxu
mackxu

Reputation: 116

text body can be escaped with single or double quotes to search for a text node with exact content

    pageNumberButton(page, table_id, page_number) {
       page.locator(`[aria-controls=${table_id}]`)
           .filter({ has: page.locator(`text="${page_number}"`) });
    }

Upvotes: 10

Machtyn
Machtyn

Reputation: 3272

I found an option. Create a regex object and pass that in for hasText.

pageNumberButton(page, table_id, page_number) { 
    const regexNumber = new RegExp(`^${page_number}$`); 
    page.locator(`[aria-controls=${table_id}]`, {hasText: regexNumber});
}`  

Upvotes: 10

Related Questions