150

Is there any method or solution to click on an element with text? I couldn't find one in the API.

For example I have the following HTML:

<div class="elements">
    <button>Button text</button>
    <a href=#>Href text</a>
    <div>Div text</div>
</div>

And I want to click on an element in which text is wrapped (click on the button inside .elements), like so:

Page.click('Button text', '.elements')
3
  • 2
    Shared the answer here: stackoverflow.com/a/47829000/6161265 Commented Jan 8, 2018 at 0:30
  • If it helps, the page I wanted to perform a click on has jQuery loaded so I was able to & used the evaluate method to execute the jQuery code. Commented Feb 26, 2018 at 4:16
  • 1
    I hate to post a "look at my answer first" comment, but the top few heavily-upvoted answers are outdated as of Puppeteer >= 18.0.0. My answer shows simpler approaches than XPath. Commented Nov 14, 2023 at 23:54

10 Answers 10

185

Short answer

This XPath expression will query a button which contains the text "Button text":

const [button] = await page.$x("//button[contains(., 'Button text')]");
if (button) {
    await button.click();
}

To also respect the <div class="elements"> surrounding the buttons, use the following code:

const [button] = await page.$x("//div[@class='elements']/button[contains(., 'Button text')]");

Explanation

To explain why using the text node (text()) is wrong in some cases, let's look at an example:

<div>
    <button>Start End</button>
    <button>Start <em>Middle</em> End</button>
</div>

First, let's check the results when using contains(text(), 'Text'):

  • //button[contains(text(), 'Start')] will return both two nodes (as expected)
  • //button[contains(text(), 'End')] will only return one nodes (the first) as text() returns a list with two texts (Start and End), but contains will only check the first one
  • //button[contains(text(), 'Middle')] will return no results as text() does not include the text of child nodes

Here are the XPath expressions for contains(., 'Text'), which works on the element itself including its child nodes:

  • //button[contains(., 'Start')] will return both two buttons
  • //button[contains(., 'End')] will again return both two buttons
  • //button[contains(., 'Middle')] will return one (the last button)

So in most cases, it makes more sense to use the . instead of text() in an XPath expression.

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

4 Comments

something that works with every type of element? i cannot know if the text is inside a button, a p, a div, a span etc.
@AndreaBisello You can use //*[...] instead.
Could this work if 'Button Text' is in an array?
Seems like this doesn't work: The string '//button[contains(., "Message")]' is not a valid XPath expression.
109

You may use a XPath selector with page.$x(expression):

const linkHandlers = await page.$x("//a[contains(text(), 'Some text')]");

if (linkHandlers.length > 0) {
  await linkHandlers[0].click();
} else {
  throw new Error("Link not found");
}

Check out clickByText in this gist for a complete example. It takes care of escaping quotes, which is a bit tricky with XPath expressions.

2 Comments

Awesome - I tried to do it for other tags, but can't make it work. (li, h1,...) How would you do that?
@RuneJeppesen Replace //a[contains with //*[contains to select any element, not just an anchor (a) element.
37

You can also use page.evaluate() to click elements obtained from document.querySelectorAll() that have been filtered by text content:

await page.evaluate(() => {
  [...document.querySelectorAll('.elements button')].find(element => element.textContent === 'Button text').click();
});

Alternatively, you can use page.evaluate() to click an element based on its text content using document.evaluate() and a corresponding XPath expression:

await page.evaluate(() => {
  const xpath = '//*[@class="elements"]//button[contains(text(), "Button text")]';
  const result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);

  result.iterateNext().click();
});

Comments

20

made quick solution to be able to use advanced css selectors like ":contains(text)"

so using this library you can just

const select = require ('puppeteer-select');

const element = await select(page).getElement('button:contains(Button text)');
await element.click()

2 Comments

Getting Evaluation failed: ReferenceError: Sizzle is not defined when tried to use this const el = await select(page).getElement('[data-testid="ContextualLayerRoot"] [role="menuitem"] div:contains("Instagram Feed")'); The issue is still open on the github
Injecting jQuery into the target site is a one-liner, so adding another dependency just to do that seems a bit excessive. Or just use a newer native Puppeteer text selector, if you don't really need jQuery in general and just need to select an element by text. If you're on an older Puppeteer version, looping over elements manually seems fine for most cases too.
20

Puppeteer 21.0.0 added locator filters which allow for text filtering, optionally alongside p-selectors:

await page
  .locator(".elements button")
  .filter(button =>
    button.textContent.trim().toLowerCase() === "button text"
  )
  .click();

Puppeteer 19.7.1 added "p" (pseudo) selectors, so text/ is deprecated in favor of ::-p-text, which selects on a substring. For example:

const el = await page.waitForSelector("::-p-text(Button text)");

Pseudoselectors can work in conjunction with CSS selectors, like

const el = await page.$(".elements button::-p-text(Button text)");

::-p-aria can also select on text content if it's part of the accessible name:

await page
  .locator('::-p-aria([name="Button text"][role="button"])')
  .click();

In Puppeteer >= 18.0.0, selectors have a text/ prefix that selects on a substring of element text:

const el = await page.waitForSelector("text/Button text");

With regards to XPath specifically, most relevant to pre-18.0.0 Puppeteer:

Since OP's use case appears to be an exact match on the target string "Button text", <button>Button text</button>, text() seems like the correct method rather than the less-precise contains().

Although Thomas makes a good argument for contains when there are sub-elements, avoiding false negatives, using text() avoids a false positive when the button is, say, <button>Button text and more stuff</button>, which seems just as likely a scenario. It's useful to have both tools on hand so you can pick the more appropriate one on a case-by-case basis.

const xp = '//*[@class="elements"]//button[text()="Button text"]';
const [el] = await page.$x(xp);
await el?.click();

Note that many other answers missed the .elements parent class requirement.

Another XPath function is [normalize-space()="Button text"] which "strips leading and trailing white-space from a string, replaces sequences of whitespace characters by a single space" and may be useful for certain cases.

Also, it's often handy to use waitForXPath (removed in v22) which waits for, then returns, the element matching the XPath or throws if it's not found within the specified timeout:

const xp = '//*[@class="elements"]//button[text()="Button text"]';
const el = await page.waitForXPath(xp);
await el.click();

Another flexible approach that works in all environments is to use browser JS to .find() or .filter() out the element(s) you want by text:

// untrusted click (ignores visibility, sometimes useful):
await page.$$eval(".elements *", els =>
  els
    .find(el => el.textContent.trim().toLowerCase() === "button text")
    .click()
);

// trusted click:
const el = await page.evaluateHandle(() =>
  [...document.querySelectorAll(".elements *")]
    .find(el => el.textContent.trim().toLowerCase() === "button text")
);
await el.click();

or:

// untrusted clicks
const els = await page.$$eval(".elements *", els =>
  els
    .filter(el => el.textContent.trim().toLowerCase() === "button text")
    .forEach(el => el.click())
);

// trusted clicks (not ideal)
const els = await page.evaluateHandle(`
  [...document.querySelectorAll(".elements *")]
    .filter(el => el.textContent.trim().toLowerCase() === "button text")
`);
const length = await els.evaluate(els => els.length);

for (let i = 0; i < length; i++) {
  const el = await els.evaluateHandle((els, i) => els[i], i);
  await el.click();
}

If you need to wait for this text, you can use waitForFunction:

const el = await page.waitForFunction(`
  [...document.querySelectorAll(".elements *")]
    .find(el => el.textContent.trim().toLowerCase() === "button text")
`);
await el.click();

Note that the above querySelectorAll strategy will include parent nodes, so <div><div>x</div></div> will find the outer <div> rather than the innermost one. To find the element with the text node immediately containing the text, you can use:

[...document.querySelectorAll("*")].find(el =>
  [...el.childNodes].find(
    el =>
      el.nodeType === Node.TEXT_NODE &&
      el.textContent.trim() === "button text"
  )
);

If you're manipulating a page that happens to have jQuery (or if you import it yourself), you can use the :contains sizzle pseudoselector syntax:

const el = await page.evaluateHandle(`
  $('.elements :contains("Button text")').first()
`);

Comments

7

The solution is

(await page.$$eval(selector, a => a
            .filter(a => a.textContent === 'target text')
))[0].click()

1 Comment

Consider replacing filter(...)[0] with find(...).
5

Here is my solution:

let selector = 'a';
    await page.$$eval(selector, anchors => {
        anchors.map(anchor => {
            if(anchor.textContent == 'target text') {
                anchor.click();
                return
            }
        })
    });

1 Comment

I'd use find or a for .. of loop rather than map here. map in this case allocates, then discards, an array of undefined for all anchors. Also, the return is miseading: map will keep going even after it finds the target. Only use map if you're going to do something with the return value.
1

There is no supported css selector syntax for text selector or a combinator option, my work around for this would be:

await page.$$eval('selector', selectorMatched => {
    for(i in selectorMatched)
      if(selectorMatched[i].textContent === 'text string'){
          selectorMatched[i].click();
          break;//Remove this line (break statement) if you want to click on all matched elements otherwise the first element only is clicked  
        }
    });

2 Comments

for(i in selectorMatched) should be for (const i in selectorMatched) to avoid creating a global variable. Also, avoid for .. in to loop arrays. That syntax is intended for objects. Instead, prefer for (const element of [...selectorMatched]).
@ggorlen Actually it'd be for (const element of selectorMatched) {}, no need for the spread. Better yet, change selectorMatched to elements and use elements.find(e => e.textContent === 'text string')?.click(), a more functional style.
1

With puppeteer 12.0.1, the following works for me:

await page.click("input[value='Opt1']"); //where value is an attribute of the element input
await page.waitForTimeout(1000);

await page.click("li[value='Nested choice 1']"); //where value is an attribute of the element li after clicking the previous option
await page.waitForTimeout(5000);

Comments

-3

You can just use the query selector.

await page.evaluate(() => {
    document.querySelector('input[type=button]').click();
});

Edits ----

You can give your button a className and use that to select the button element since you know exactly what you're trying to click:

await page.evaluate(() => {
    document.querySelector('.button]').click();
});

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.