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

Austin France picture Austin France · Apr 23, 2018 · Viewed 12.3k times · Source

I have a situation where a button, on a form, that is animated into view, if the element.click() happens while the animation is in progress, it doesn't work.

element.click() doesn't throw an error, doesn't return a failed status (it returns undefined) it just silently doesn't work.

I have tried ensuring the element being clicked is not disabled, and is displayed (visible) but even though both those tests succeed, the click fails.

If I wait 0.4s before clicking, it works because the animation has finished.

I don't want to have to add delays (which are unreliable, and a bodge to be frank), if I can detect when a click worked, and if not automatically retry.

Is there a generic way to detect if a click() has actually been actioned so I can use a retry loop until it does?

Answer

Austin France picture Austin France · Apr 26, 2018

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).