Home > database >  How to trigger click event on link from list of tabs on page with puppeteer
How to trigger click event on link from list of tabs on page with puppeteer

Time:01-05

I've been looking for a solution to this, and found a few here that focus on clicking an element, but none that allow for clicking an element based on a link.

Using puppeteer, I'm looping over an array of tabs

<div role="tablist">
    <div><a href="#one" tabindex="-1" role="tab" aria-selected="false" >One</a></div>
    <div><a href="#two" tabindex="-1" role="tab" aria-selected="false" >Two</a></div>
    <div><a href="#three" tabindex="0" role="tab" aria-selected="true" >three</a></div>
</div>

and able to grab the url or hash, but getting the error link.click() is not a function. I believe this is due to Puppeteer not being able to trigger a click the same way as JS, but unsure of the way forward:

let tabs = await page.evaluate(() => {
  var tab = [...document.querySelectorAll('[role="tablist"] a')].map(
    (el) => el.hash
  );
  return tab;
});
let components = [];
if (tabs) {
  tabs.forEach((link, index) => {
    setTimeout(() => {
      link.click();
      components.push(
        [...document.querySelectorAll(".ws-compid")]
          .map((component) => component.innerText)
          .filter((el) => el !== "")
      );
    }, 200 * index);
  });
}
console.log(components);

I believe I need an async function to be able to trigger the click event, but not sure. This should be able to click the href value of each tab, and then push values from the page into an array of components.

CodePudding user response:

I can't run your page to see what the actual behavior is, but based on the limited information provided, here's my best attempt at piecing together a working example you can adapt to your use case:

const puppeteer = require("puppeteer"); // ^19.1.0

const html = `
<div role="tablist">
  <div><a href="#one" tabindex="-1" role="tab" aria-selected="false" >One</a></div>
  <div><a href="#two" tabindex="-1" role="tab" aria-selected="false" >Two</a></div>
  <div><a href="#three" tabindex="0" role="tab" aria-selected="true" >three</a></div>
  <div ></div>
</div>
<script>
document.querySelectorAll('[role="tablist"] a').forEach(e => 
  e.addEventListener("click", () => {
    document.querySelector(".ws-compid").textContent = e.textContent;
  })
);
</script>`;

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.setContent(html);
  const components = await page.evaluate(() =>
    Promise.all(
      [...document.querySelectorAll('[role="tablist"] a')].map(
        (e, i) =>
          new Promise(resolve =>
            setTimeout(() => {
              e.click();
              resolve(
                [...document.querySelectorAll(".ws-compid")]
                  .map(component => component.innerText)
                  .filter(e => e)
              );
            }, 200 * i)
          )
      )
    )
  );
  console.log(components); // => [ [ 'One' ], [ 'Two' ], [ 'three' ] ]
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

A lot can go wrong translating browser code to Puppeteer: asynchronous loading, bot detection, iframes, shadow DOM, to name a few obstacles, so if this doesn't work, I'll need a reproducible example.

Although you claim your original code works, I don't see how that's possible. The pattern boils down to:

const tabs = [..."ABCDEF"];
let components = [];
tabs.forEach((link, index) => {
  setTimeout(() => {
    components.push(link);
  }, 200 * index);
});
console.log(components); // guaranteed to be empty

// added code
setTimeout(() => {
  console.log(components.join("")); // "ABCDEF"
}, 2000);

You can see that console.log(components) runs before the setTimeouts finish. Only after adding an artificial delay do we see components filled as expected. See the canonical thread How do I return the response from an asynchronous call?. One solution is to promisify the callbacks as I've done above.

Note also that sleeping for 200 milliseconds isn't ideal. You can surely speed this up with a waitForFunction.


In the comments, you shared a site that has similar tabs, but you don't need to click anything to access the text that's revealed after each click:

const puppeteer = require("puppeteer");

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  const url = "https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html";
  await page.goto(url, {waitUntil: "domcontentloaded"});
  const text = await page.$$eval(
    '#ex1 [role="tabpanel"]',
    els => els.map(e => e.textContent.trim())
  );
  console.log(text);
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

So there's a good chance this is an xy problem.

  • Related