puppeteer: waitForSelector with visible:true not returning the first visible element, causes timeout.

Steps to reproduce

Tell us about your environment:

What steps will reproduce the problem? Open Website OR use this file:

<html>
  <head>
    <meta charset="UTF-8" />
  </head>

  <body>
    <input style="display: none" type="number" class="my-input-class" />
    <input type="number" class="my-input-class" />
  </body>
</html>

Please include code that reproduces the issue.

const browser = await puppeteer.launch();

const page = await browser.newPage();
await page.goto('https://j4q389wzv3.codesandbox.io/');

try {
  const visibleInput = await page.waitForSelector('.my-input-class', {visible: true, timeout: 1000 })
  console.log('Found visible Element')
} catch (e) {
  console.log('Could NOT find a visible element ', e.message)
}

const inputs = await page.$$('.my-input-class')
console.log(`Found ${inputs.length} inputs`)

await browser.close()

What is the expected result? There are 2 elements matching the CSS Selector on the page. the first one is hidden, the second one is visible. The page.waitForSelector with {visible: true} should have found and returned the visible element on the page.

What happens instead? It Times Out since there was another HIDDEN element matching the CSS selector higher up in the DOM structure, causing it to wait until timeout even though a visible element exists on the page.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 7
  • Comments: 16 (2 by maintainers)

Commits related to this issue

Most upvoted comments

The page.waitForSelector with {visible: true} should have found and returned the visible element on the page.

@razorman8669 Ok I see what you mean here. Indeed, the behavior is somewhat confusing.

However, I disagree with the proposed approach. CSS selector is the only thing that we should use to identify the element. Adding the suggested logic adds magic to how we find elements - making it equally hard to debug.

The best solution here is to update the documentation - so that we do a better job explaining what’s going on.

A working solution (though, not ideal) is as follows. Ideally the waitForSelector with { visible:true } would behave this way… or the documentation clearly states that it doesn’t do this.

/** Internal method to determine if an elementHandle is visible on the page. */
const _isVisible = async(page, elementHandle) => await page.evaluate((el) => {
  if (!el || el.offsetParent === null) {
    return false;
  }

  const style = window.getComputedStyle(el);
  return style && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}, elementHandle);

/**
 * Checks if an element with selector exists in DOM and is visible.
 * @param {*} page
 * @param {*} selector CSS Selector.
 * @param {*} timeout amount of time to wait for existence and visible.
 */
const waitForVisible = async(page, selector, timeout=25) => {
  const startTime = new Date();
  try {
    await page.waitForSelector(selector, { timeout: timeout });
    // Keep looking for the first visible element matching selector until timeout
    while (true) {
      const els = await page.$$(selector);
      for(const el of els) {
        if (await _isVisible(page, el)) {
          console.log(`PASS Check visible : ${selector}`);
          return el;
        }
      }
      if (new Date() - startTime > timeout) {
        throw new Error(`Timeout after ${timeout}ms`);
      }
      page.waitFor(50);
    }
  } catch (e) {
    console.log(`FAIL Check visible : ${selector}`);
    return false;
  }
};