4

I am trying to click a number of elements in in a page, but only if they are visible. This was quite easy using selenium (using is_displayed), but I can't seem to find a way in puppeteer. I was trying to use something like

try {
    await page
      .waitForSelector(id, visible=true, timeout=0)
      .then(() => {
        element.click()
      });
...

But this does not working if it is a simple element like :

<a class="cookie-close" href="#">
OK
</a>

I also can't seem to see a way to do it using the element.click method in puppeteer.

RexFuzzle
  • 1,412
  • 2
  • 17
  • 30
  • Have you tried applying standard Javascript method for checking element visibility, such as https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom ? – Dejan Toteff Nov 08 '17 at 17:34

6 Answers6

5

Short Answer

const element = await page.waitForSelector('a.cookie-close', { visible: true });
await element.click();

This uses the page.waitForSelector function to select a visible element with the selector a.cookie-close. After the selector is queried, the code uses elementHandle.click to click on it.

Explanation

Only the functions page.waitForSelector and page.waitForXPath have an option built in that checks if an element is not only present but also visible. When used, puppeteer will check if the style attribute visibility is not hidden and if the element has a visible bounding box.

Making sure the element is not empty

Even if the element is visible, it might be empty (e.g. <span></span>). If you also want the element not to be empty too, you can use the following query instead:

const element = await page.waitForSelector('SELECTOR:not(:empty)', { visible: true });

This will in addition use the pseudo selectors :empty and :not to make sure that the element contains a child node or text. If you want to query for a specific text inside the element, you might want to check out this answer.

Thomas Dondorf
  • 23,416
  • 6
  • 84
  • 105
0

Similar to Selenium, the correct answer is to also use Puppeteer's waitForSelector which can test for DOM element presence and visibility.

try {
  // Will throw err if element is not present and visible.
  await chromePage.waitForSelector("div.hello", {
    visible: true
  });
  await chromePage.click("div.hello");
} catch(err) {
  console.log(err);
}
peter bray
  • 1,325
  • 1
  • 12
  • 22
0

Here's a more idiomatic way wait for an element to be "displayed" before clicking. The result should be the same as to what @joquarky explained:

click() {
    let retries = 5;
    const hoverAndClick = () => {
        return this._element!.hover() // this._element is ElementHandle
            .then(() => {
                return this._element!.click();
            })
            .catch(err => {
                if (retries <= 0) {
                    throw err;
                }
                retries -= 1;
                sleep(1000).then(hoverAndClick);
            });
    };

    return hoverAndClick();
}

The slight different here is that we retry a few times in case the element is waiting for a transition, due to any of its parents hiding it.

lmerino
  • 173
  • 2
  • 11
-1

You should use page.click() for effecting a click.

See page.click(selector, [options]) in Puppeteer's API docs: https://github.com/GoogleChrome/puppeteer/blob/HEAD/docs/api.md#pageclickselector-options

gjegadesh
  • 144
  • 10
-1

Not sure if it's the most effective way, but you could try something like this:

// returns null if element not present
let element = await page.evaluate(() => {
  return document.querySelector(id);
  });

// use jQuery to check if element is not hidden
if (element && !$(element).is(':hidden') {
  await page.click(id);
}
kaiak
  • 1,759
  • 1
  • 13
  • 11
-1

I had the same problem, and came up with this solution:

await page.waitForSelector('#clickable');
await page.evaluate(() => {
  let el = document.querySelector('#clickable');
  if (isVisible(el)) {
    el.click();
    return true;
  }
  return false;

  function isVisible(el) {
    if (el.offsetParent === null && el !== document.body) {
      return false; // not connected to document
    }
    if (el.offsetWidth <= 0 || el.offsetHeight <= 0) {
      return false; // no width or height
    }
    let style = el.style;
    if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
      return false; // has a 'hidey' css attribute
    }
    if (el === document.body) {
      return true;
    }
    return isVisible(el.offsetParent);
  }
});

This checks that the element is actually attached to the document, that it has at least some width and height, and that the element nor any of its ancestors are hidden by CSS styles display:none, visibility:hidden, or opacity:0.

Note that this probably won't work if the element is hidden by unconventional means, like a huge negative margin/padding.

joquarky
  • 64
  • 5
  • I had to pull the `(el === document.body)` condition up to check first for this to work reliably, since occasionally `body` had an `offsetHeight` of `0`. – Martin Mar 09 '18 at 15:30
  • This was the answer that worked for me! In my case, the container div had a display: none, and it was 'shown' so `page.waitForSelector('div', { visible: true })` effectively returned true when toggled BUT, it also had some transition cycle so waiting for it to be "displayed" is what worked. I figured a more idiomatic way of checking if an element is displayed and ready to be clicked: waiting for `element.hover()` to be successful, retrying every second until it is, otherwise throwing... See my answer below. – lmerino Oct 10 '19 at 14:18