Using Puppeteer, I would like to get all the elements on a page with a particular class name and then loop through and click each one.
Using jQuery, I can achieve this with:
var elements = $("a.showGoals").toArray(); for (i = 0; i < elements.length; i++) { $(elements[i]).click(); }
How would I achieve this using Puppeteer?
Tried out Chridam's answer below, but I couldn't get it to work (though the answer was helpful, so thanks due there), so I tried the following and this works:
await page.evaluate(() => { let elements = $('a.showGoals').toArray(); for (i = 0; i < elements.length; i++) { $(elements[i]).click(); } });
The details on Puppeteer installation is discussed in the Chapter of Puppeteer Installation. Right-click on the folder where the node_modules folder is created, then click on the New file button. Step 2 − Enter a filename, say testcase1.
class selector selects elements with a specific class attribute. To select elements with a specific class, write a period (.) character, followed by the name of the class.
puppeteer find element by text You have to form XPath based on the text so that you can find the element. Once you form the XPath then you can use the $x method to find the element.
You can get the elements by using the class in puppeteer, but the puppeteer does not understand what is class or id; so you have to use the CSS format to make the puppeteer understand it. Use . (dot) before the class name to denote that the following is class.
@sorb999 you can get ElementHandle instances on the puppeteer-side: Sorry, something went wrong. @JoelEinbinder THANK YOU. This is the answer I have been struggling to find for days. I've been trying to get the classList of an element and did not realize that I could easily do so by wrapping what I was trying to do within a page.evaluate ().
Unlike other automation tools, getting an element in puppeteer a bit difficult and also Limited. It is not because the automation tool has a limitation, but puppeteer provides all the operations as functions that you need without requiring the element.
for
loop vs. Array.map()/Array.forEach()
As all puppeteer methods are asynchronous it doesn't matter how we iterate over them. I've made a comparison and a rating of the most commonly recommended and used options.
For this purpose, I have created a React.Js example page with a lot of React buttons here (I just call it Lot Of React Buttons). Here (1) we are able set how many buttons to be rendered on the page; (2) we can activate the black buttons to turn green by clicking on them. I consider it an identical use case as the OP's, and it is also a general case of browser automation (we expect something to happen if we do something on the page). Let's say our use case is:
Scenario outline: click all the buttons with the same selector Given I have <no.> black buttons on the page When I click on all of them Then I should have <no.> green buttons on the page
There is a conservative and a rather extreme scenario. To click no. = 132
buttons is not a huge CPU task, no. = 1320
can take a bit of time.
In general, if we only want to perform async methods like elementHandle.click
in iteration, but we don't want to return a new array: it is a bad practice to use Array.map
. Map method execution is going to finish before all the iteratees are executed completely because Array iteration methods execute the iteratees synchronously, but the puppeteer methods, the iteratees are: asynchronous.
const elHandleArray = await page.$$('button') elHandleArray.map(async el => { await el.click() }) await page.screenshot({ path: 'clicks_map.png' }) await browser.close()
Duration: 891 ms
By watching the browser in headful mode it looks like it works, but if we check when the page.screenshot
happened: we can see the clicks were still in progress. It is due to the fact the Array.map
cannot be awaited by default. It is only luck that the script had enough time to resolve all clicks on all elements until the browser was not closed.
Duration: 6868 ms
If we increase the number of elements of the same selector we will run into the following error: UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement
, because we already reached await page.screenshot()
and await browser.close()
: the async clicks are still in progress while the browser is already closed.
All the iteratees will be executed, but forEach is going to return before all of them finish execution, which is not the desirable behavior in many cases with async functions. In terms of puppeteer it is a very similar case to Array.map
, except: for Array.forEach
does not return a new array.
const elHandleArray = await page.$$('button') elHandleArray.forEach(async el => { await element.click() }) await page.screenshot({ path: 'clicks_foreach.png' }) await browser.close()
Duration: 1058 ms
By watching the browser in headful mode it looks like it works, but if we check when the page.screenshot
happened: we can see the clicks were still in progress.
Duration: 5111 ms
If we increase the number of elements with the same selector we will run into the following error: UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement
, because we already reached await page.screenshot()
and await browser.close()
: the async clicks are still in progress while the browser is already closed.
The best performing solution is a slightly modified version of bside's answer. The page.$$eval (page.$$eval(selector, pageFunction[, ...args])
) runs Array.from(document.querySelectorAll(selector))
within the page and passes it as the first argument to pageFunction
. It functions as a wrapper over forEach hence it can be awaited perfectly.
await page.$$eval('button', elHandles => elHandles.forEach(el => el.click())) await page.screenshot({ path: 'clicks_eval_foreach.png' }) await browser.close()
Duration: 711 ms
By watching the browser in headful mode we see the effect is immediate, also the screenshot is taken only after every element has been clicked, every promise has been resolved.
Duration: 3445 ms
Works just like in case of 132 buttons, extremely fast.
The simplest option, not that fast and executed in sequence. The script won't go to page.screenshot
until the loop is not finished.
const elHandleArray = await page.$$('button') for (const el of elHandleArray) { await el.click() } await page.screenshot({ path: 'clicks_for_of.png' }) await browser.close()
Duration: 2957 ms
By watching the browser in headful mode we can see the page clicks are happening in strict order, also the screenshot is taken only after every element has been clicked.
Duration: 25 396 ms
Works just like in case of 132 buttons (but it takes more time).
Array.map
if you only want to perform async events and you aren't using the returned array, use forEach or for-of instead. ❌Array.forEach
is an option, but you need to wrap it so the next async method only starts after all promises are resolved inside the forEach. ❌Array.forEach
with $$eval
for best performance if the order of async events doesn't matter inside the iteration. ✅for
/for...of
loop if speed is not vital and if the order of the async events does matter inside the iteration. ✅Use page.evaluate
to execute JS:
const puppeteer = require('puppeteer'); puppeteer.launch().then(async browser => { const page = await browser.newPage(); await page.evaluate(() => { let elements = document.getElementsByClassName('showGoals'); for (let element of elements) element.click(); }); // browser.close(); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With