Wait for first visible among multiple elements matching selector

Viewed 47

I have code that is at some point doing this:

await page.waitForSelector('.modal-body', {visible: true});

There are multiple .modal-body matches, but this only seems to work if the first match is the one that becomes visible.

I'm assuming the waitForSelector just finds the first match on the selector right away (regardless of whether or not it is visible) and then waits for it to become visible rather than waiting for any element to match both the selector and the option.

I could create logic to count which modal should be opening.. but I'm wondering if there is a way to just wait for any element to match the entire thing (selector + option)?

1 Answers

Yep, you're right. Basically, waitForSelector runs a querySelector, which grabs the first element, then blocks until that element is visible.

At the time of writing, I don't believe there's a built-in way to wait for the first element matching the selector to become visible, but it's possible to implement it with the general-purpose page.waitForFunction. Here's an approach that uses Puppeteer's visibility check:

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

const html = `
<!DOCTYPE html>
<html>
<head>
<style>
h1 {
  visibility: hidden;
}
</style>
</head>
<body>
<h1>foo</h1>
<h1>bar</h1>
<h1>baz</h1>
<script>
setTimeout(() => {

  //////////////////////////////////////////////////////////////////
  // pick the index of the element you want to make visible here: //
  const el = document.querySelectorAll("h1")[2];                  //
  //////////////////////////////////////////////////////////////////

  el.style.visibility = "visible";
}, 5000); // delay before visibility
</script>
</body>
</html>
`;

let browser;
(async () => {
  browser = await puppeteer.launch({headless: false});
  const [page] = await browser.pages();
  await page.setContent(html);

  await page.evaluate(() => {
    window.isVisible = element => {
      // checkWaitForOptions from Puppeteer src/common/util.ts
      // https://github.com/puppeteer/puppeteer/blob/32400954c50cbddc48468ad118c3f8a47653b9d3/src/common/util.ts#L354
      const style = window.getComputedStyle(element);
      const isVisible =
        style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
      return isVisible;
    
      function hasVisibleBoundingBox() {
        const rect = element.getBoundingClientRect();
        return !!(rect.top || rect.bottom || rect.width || rect.height);
      }
    };
  });

  const el = await page.waitForFunction(() => 
    [...document.querySelectorAll("h1")].find(isVisible)
  );
  console.log(await el.evaluate(el => el.textContent));

  // will time out unless [0] is set above
  // await page.waitForSelector("h1", {visible: true});
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close())
;

You can change the index of the first-visible element and see that when it's 0, the foo element is found; 1, the bar element; 2 gives baz. If you use page.waitForSelector("h1", {visible: true}) then it will time out for indices 1 and 2 as expected.

Yes, this is a lot of code but you can put it in a helper until (if) it becomes part of the API.

Puppeteer waitForSelector on multiple selectors is sort of related but isn't exactly specific to visibility. If you want to get all elements that first become visible on a mutation, my answer in that thread shows the idea and only needs the isVisible check thrown in.

Related