puppeteer element.click() not working and not throwing an error

The page.click() returns a promise, make sure to handle it as such, but also note that you may have issues if you are not referencing it as an xpath. That's what I had to do in order to get it working. I've tried using querySelectors and interacting with the objects that way, but I ran into issues.

page.evaluate(()=>{
await Promise.all([
    page.click("a[id=tab-default-2__item]"),
    //The page.waitFor is set to 15000 for my personal use. 
    //Feel free to play around with this.
    page.waitFor(15000)
]);
});    

I hope this helps.


Generic click with timeout function inspired by Andrea's answer. This one returns as soon as the element is clickable, so won't slow down tests.

click: async function (page, selector, timeout = 30000) {

    await page.waitForSelector(selector, { visible: true, timeout })

    let error;
    while (timeout > 0) {
        try {
            await page.click(selector);
            return;
        } catch (e) {
            await page.waitFor(100);
            timeout -= 100;
            error = e;
        }
    }
    throw error;
}

I have determined what is happening, and why I don't get an error, and how to work around the issue.

The main issue is with the way element.click() works. Using DEBUG="puppeteer:*" I was able to see what is going on internally. What element.click() actually does is:-

const box = element.boundingBox();
const x = box.x + (box.width/2);
const y = box.y + (box.height/2);
page.mouse.move(x,y);
page.mouse.down();
sleep(delay);
page.mouse.up();

The problem is that because the view (div) is animating the element's boundingBox() is changing, and between the time of asking for the box position, and completing the click() the element has moved or is not clickable.

An error isn't thrown (promise rejected) because its just a mouse click on a point in the viewport, and not linked to any element. The mouse event is sent, just that nothing responds to it.

One workaround is to add a sufficient delay to allow the animation to finish. Another is to disable animations during tests.

The solution for me was to wait for the position of the element to settle at its destination position, that is I spin on querying the boundingBox() and wait for the x,y to report the elements previously determined position.

In my case, this is as simple as adding at 10,10 to my test script just before the click, or specifically

test-id "form1.button3" at 10,10 click

And in action it works as follows, in this case, the view is being animated back in from the left.

00.571 [selector.test,61] at 8,410
test-id "main.add" info tag button displayed at -84,410 size 116,33 enabled not selected check "Add"
test-id "main.add" info tag button displayed at -11,410 size 116,33 enabled not selected check "Add"
test-id "main.add" info tag button displayed at 8,410 size 116,33 enabled not selected check "Add"
00.947 [selector.test,61] click

It wouldn't work for an element that was continually moving or for an element that is covered by something else. For those cases, try page.evaluate(el => el.click(), element).

Tags:

Puppeteer