54

I'm using 'puppeteer' for NodeJS to test a specific website. It seems to work fine in most case, but some places it reports:

Error: Node is either not visible or not an HTMLElement

The following code picks a link that in both cases is off the screen.

The first link works fine, while the second link fails.

What is the difference? Both links are off the screen.

Any help appreciated, Cheers, :)

Example code

const puppeteer = require('puppeteer');

const initialPage = 'https://website.com/path';
const selectors = [
    'div[id$="-bVMpYP"] article a',
    'div[id$="-KcazEUq"] article a'
];

(async () => {
    let selector, handles, handle;
    const width=1024, height=1600;
    const browser = await puppeteer.launch({ 
        headless: false, 
        defaultViewport: { width, height } 
    });
    const page = await browser.newPage();
    await page.setViewport({ width, height});
    page.setUserAgent('UA-TEST');

    // Load first page
    let stat = await page.goto(initialPage, { waitUntil: 'domcontentloaded'});

    // Click on selector 1 - works ok
    selector = selectors[0];
    await page.waitForSelector(selector);
    handles = await page.$$(selector);
    handle = handles[12]
    console.log('Clicking on: ', await page.evaluate(el => el.href, handle));
    await handle.click();  // OK
    
    // Click that selector 2 - fails
    selector = selectors[1];
    await page.waitForSelector(selector);
    handles = await page.$$(selector);
    handle = handles[12]
    console.log('Clicking on: ', await page.evaluate(el => el.href, handle));
    await handle.click();  // Error: Node is either not visible or not an HTMLElement

})();

I'm trying to emulate the behaviour of a real user clicking around the site, which is why I use .click(), and not .goto(), since the a tags have onclick events.

Vbakke
  • 1,393
  • 1
  • 12
  • 17
  • `document.querySelectorAll('div[id$="-KcazEUq"] article a')` returns an empty nodelist — there doesn't seem to be such collection of articles (tried in non-headless Chrome) – Vaviloff Aug 16 '18 at 06:33

5 Answers5

76

Instead of

await button.click();

do this:

await button.evaluate(b => b.click());

This tells the browser to run the JavaScript HTMLElement.click() method on the given element, which will fire a click event on that element even if it's hidden, off-screen or covered by a different element, whereas button.click() uses Puppeteer's ElementHandle.click() which

  1. scrolls the page until the element is in view
  2. gets the bounding box of the element (this step is where the error happens) and calculates the screen x and y pixel coordinates of the middle of that box
  3. moves the virtual mouse to those coordinates and sets the mouse to "down" then back to "up", which triggers a click event on the element under the mouse
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
  • 4
    Okay, this fixed my problem too. What kind of dark magic is it? – Billy May 07 '21 at 03:52
  • 2
    @Billy [`Page.click()`](https://pptr.dev/#?product=Puppeteer&show=api-elementhandleclickoptions) clicks in a different way than clicking [with JavaScript](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click) (which is the alternative I suggested). It scrolls the element into view before clicking it but [apparently that can fail](https://github.com/puppeteer/puppeteer/issues/6462#issuecomment-717163409) and I also see that up until 18 days ago `Page.click()` possibly had a race condition ([fixed in 7097](https://github.com/puppeteer/puppeteer/pull/7097)), maybe that's related. – Boris Verkhovskiy May 07 '21 at 04:22
  • The crazy thing is that this works, but using something like "await (await page.$(selector)).click()" doesn't. I guess even the .click() function is different from the evaluate e.click version – Dr4kk0nnys Jun 11 '21 at 12:42
  • 1
    `let x = await page.$(selector); await x.click()` and `await (await page.$(selector)).click()` are the exact same thing. I explained in my comment the difference between `.click()` and `.evaluate(e => e.click())` – Boris Verkhovskiy Jun 11 '21 at 14:53
24

First and foremost, your defaultViewport object that you pass to puppeteer.launch() has no keys, only values.

You need to change this to:

'defaultViewport' : { 'width' : width, 'height' : height }

The same goes for the object you pass to page.setViewport().

You need to change this line of code to:

await page.setViewport( { 'width' : width, 'height' : height } );

Third, the function page.setUserAgent() returns a promise, so you need to await this function:

await page.setUserAgent( 'UA-TEST' );

Furthermore, you forgot to add a semicolon after handle = handles[12].

You should change this to:

handle = handles[12];

Additionally, you are not waiting for the navigation to finish (page.waitForNavigation()) after clicking the first link.

After clicking the first link, you should add:

await page.waitForNavigation();

I've noticed that the second page sometimes hangs on navigation, so you might find it useful to increase the default navigation timeout (page.setDefaultNavigationTimeout()):

page.setDefaultNavigationTimeout( 90000 );

Once again, you forgot to add a semicolon after handle = handles[12], so this needs to be changed to:

handle = handles[12];

It's important to note that you are using the wrong selector for your second link that you are clicking.

Your original selector was attempting to select elements that were only visible to xs extra small screens (mobile phones).

You need to gather an array of links that are visible to your viewport that you specified.

Therefore, you need to change the second selector to:

div[id$="-KcazEUq"] article .dfo-widget-sm a

You should wait for the navigation to finish after clicking your second link as well:

await page.waitForNavigation();

Finally, you might also want to close the browser (browser.close()) after you are done with your program:

await browser.close();

Note: You might also want to look into handling unhandledRejection errors.


Here is the final solution:

'use strict';

const puppeteer = require( 'puppeteer' );

const initialPage = 'https://statsregnskapet.dfo.no/departementer';

const selectors = [
    'div[id$="-bVMpYP"] article a',
    'div[id$="-KcazEUq"] article .dfo-widget-sm a'
];

( async () =>
{
    let selector;
    let handles;
    let handle;

    const width = 1024;
    const height = 1600;

    const browser = await puppeteer.launch(
    {
        'defaultViewport' : { 'width' : width, 'height' : height }
    });

    const page = await browser.newPage();

    page.setDefaultNavigationTimeout( 90000 );

    await page.setViewport( { 'width' : width, 'height' : height } );

    await page.setUserAgent( 'UA-TEST' );

    // Load first page

    let stat = await page.goto( initialPage, { 'waitUntil' : 'domcontentloaded' } );

    // Click on selector 1 - works ok

    selector = selectors[0];
    await page.waitForSelector( selector );
    handles = await page.$$( selector );
    handle = handles[12];
    console.log( 'Clicking on: ', await page.evaluate( el => el.href, handle ) );
    await handle.click();  // OK

    await page.waitForNavigation();

    // Click that selector 2 - fails

    selector = selectors[1];
    await page.waitForSelector( selector );
    handles = await page.$$( selector );
    handle = handles[12];
    console.log( 'Clicking on: ', await page.evaluate( el => el.href, handle ) );
    await handle.click();

    await page.waitForNavigation();

    await browser.close();
})();
Grant Miller
  • 27,532
  • 16
  • 147
  • 165
  • 4
    Thank you! The mising .dfo-widget-sm caused the hickups! And thank you for the other tips as well. It was a scaled down test script, and I obviously didn't pay enough attention it all. (However, the width and height initialisation followed the ECMA 2015 notation, and should be right. :-) – Vbakke Aug 24 '18 at 17:39
10

For anyone still having trouble this worked for me:

await page.evaluate(()=>document.querySelector('#sign-in-btn').click())

Basically just get the element in a different way, then click it.

The reason I had to do this was because I was trying to click a button in a notification window which sits outside the rest of the app (and Chrome seemed to think it was invisible even if it was not).

chrisheseltine
  • 193
  • 2
  • 13
8

I know I’m late to the party but I discovered an edge case that gave me a lot of grief, and this thread, so figured I’d post my findings.

The culprit: CSS

scroll-behavior: smooth

If you have this you will have a bad time.

The solution:

await page.addStyleTag({ content: "{scroll-behavior: auto !important;}" });

Hope this helps some of you.

Tori
  • 91
  • 1
  • 2
4

My way

async function getVisibleHandle(selector, page) {

    const elements = await page.$$(selector);

    let hasVisibleElement = false,
        visibleElement = '';

    if (!elements.length) {
        return [hasVisibleElement, visibleElement];
    }

    let i = 0;
    for (let element of elements) {
        const isVisibleHandle = await page.evaluateHandle((e) => {
            const style = window.getComputedStyle(e);
            return (style && style.display !== 'none' &&
                style.visibility !== 'hidden' && style.opacity !== '0');
        }, element);
        var visible = await isVisibleHandle.jsonValue();
        const box = await element.boxModel();
        if (visible && box) {
            hasVisibleElement = true;
            visibleElement = elements[i];
            break;
        }
        i++;
    }

    return [hasVisibleElement, visibleElement];
}

Usage

let selector = "a[href='https://example.com/']";

let visibleHandle = await getVisibleHandle(selector, page);

if (visibleHandle[1]) {

   await Promise.all([
     visibleHandle[1].click(),
     page.waitForNavigation()
   ]);
}