If you want to wait for the first of multiple selectors and get the matched element(s), you can start with waitForFunction
:
const matches = await page.waitForFunction(() => {
const matches = [...document.querySelectorAll(YOUR_SELECTOR)];
return matches.length ? matches : null;
});
waitForFunction
will return an ElementHandle but not an array of them. If you only need native DOM methods, it's not necessary to get handles. For example, to get text from this array:
const contents = await matches.evaluate(els => els.map(e => e.textContent));
In other words, matches
acts a lot like the array passed to $$eval
by Puppeteer.
On the other hand, if you do need an array of handles, the following demonstration code makes the conversion and shows the handles being used as normal:
const puppeteer = require("puppeteer"); // ^16.2.0
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
h1 {
display: none;
}
</style>
</head>
<body>
<script>
setTimeout(() => {
// add initial batch of 3 elements
for (let i = 0; i < 3; i++) {
const h1 = document.createElement("button");
h1.textContent = \`first batch #\${i + 1}\`;
h1.addEventListener("click", () => {
h1.textContent = \`#\${i + 1} clicked\`;
});
document.body.appendChild(h1);
}
// add another element 1 second later to show it won't appear in the first batch
setTimeout(() => {
const h1 = document.createElement("h1");
h1.textContent = "this won't be found in the first batch";
document.body.appendChild(h1);
}, 1000);
}, 3000); // delay before first batch of elements are added
</script>
</body>
</html>
`;
let browser;
(async () => {
browser = await puppeteer.launch({headless: true});
const [page] = await browser.pages();
await page.setContent(html);
const matches = await page.waitForFunction(() => {
const matches = [...document.querySelectorAll("button")];
return matches.length ? matches : null;
});
const length = await matches.evaluate(e => e.length);
const handles = await Promise.all([...Array(length)].map((e, i) =>
page.evaluateHandle((m, i) => m[i], matches, i)
));
await handles[1].click(); // show that the handles work
const contents = await matches.evaluate(els => els.map(e => e.textContent));
console.log(contents);
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;
Unfortunately, it's a bit verbose, but this can be made into a helper.
See also Wait for first visible among multiple elements matching selector if you're interested in integrating the {visible: true}
option.