pupppeteer js element not clicking even thou it is available in console - puppeteer

I have the following element in html.
<a title="Download photo" href="https://example.com/photos/GXqvtQh1N9A/download?force=true" rel="nofollow" download="" target="_blank" class="_1QwHQ _1l4Hh _1CBrG _1zIyn xLon9 _1Tfeo _2L6Ut _2Xklx"><svg class="Apljk _11dQc" version="1.1" viewBox="0 0 32 32" width="32" height="32" aria-hidden="false"></a>
From the console when Chromium is open.
I can query it like so:
document.querySelector('a[title="Download photo"]');
I can create a reference to it:
var link = document.querySelector('a[title="Download photo"]');
I then can click on it like so:
link.click();
I try the same exact thing in Puppeteer.js in code. Same page.
for (const handle of getAllElements) {
try {
await handle.click();
const downloadButton = await page.$('a[title="Download photo"]');
downloadButton.click();
await sleep.sleep(2000);
} catch (e) {
console.error(e);
}
}
The initial handle.click() works and it opens me to the page I'm discussing here.
But then downloadButton.click() doesn't function.
I've also tried page.click(downloadButton).
I've also tried:
const downloadButton = await page.$('a[title="Download photo"]');
await downloadButton.click();
To ensure I'm working with the same page I visually do it while the page is on the screen.
Any ideas what's gong on?

As you mentioned it opens a layer on top each time you click on the image. Also, a[title="Download photo"] needs to be relative to the handle not page. Here is the working code:
for (const handle of getAllElements) {
await handle.click();
await handle.$eval('a[title="Download photo"]', el => el.click());
//allow download
await page._client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: './'
});
await new Promise(resolve => setTimeout(resolve, 2000));
//click on X to close the layer
await page.click('._1NHYN');
}

Related

Please tell me what a unique selector is set on puppeteer, when elements have duplicate query selector

My Html code has Button-tags that have same id "hoge".
If you get the selector from the Chrome Dev Tool, it will be the same for both "#hoge".
<html>
<body>
<button id="hoge">Hoge</button>
<div class="shadow">
#shadow-root (open)
<button id="hoge">Hoge</button>
</div>
</body>
</html>
I want to get element of button-tag in shadow dom with puppeteer.
But, my javascript code gets element of 1st button.
const element = page.waitForSelector("pierce/#hoge");
This is not what I want.
I'm guessing it's because you didn't specify a unique selector, but i don't know what is unique selector for puppeteer.
If you know how to solve this problem, please let me know.
Long story short
I work with puppeteer a lot and wanted this knowlegde to be in my bag. One way to select a shadow Element is by accessing the parent DOM Node's shadowRoot property. The answer is based on this article.
Accessing Shadow Root property
For your html example this does the trick:
const button = document.querySelector('.shadow').shadowRoot.querySelector('#hoge')
waiting
Waiting though is a little more complicated but can be acquired using page.waitForFunction().
Working Sandbox
I wrote this full working sandbox example on how to wait for a certain shadowRoot element.
index.html (located in same directory as app.js)
<html>
<head>
<script>
// attach shadowRoot after 6 seconds for emulating waiting..
setTimeout(() => {
const btn = document.getElementById('hoge')
const container = document.getElementsByClassName('shadow')[0]
const shadowRoot = container.attachShadow({
mode: 'open'
})
shadowRoot.innerHTML = `<button id="hoge" onClick="doStuff()">hoge2</button>`
console.log('attached!.')
}, 6000)
function doStuff() {
alert('shadow button clicked!')
}
</script>
</head>
<body>
<button id="hoge">Hoge</button>
<div class="shadow">
</div>
</body>
</html>
app.js (located in same directory as index.html)
var express = require('express')
var { join } = require('path')
var puppeteer = require('puppeteer')
//utility..
const wait = (seconds) => {
console.log('waiting', seconds, 'seconds')
return new Promise((res, rej) => {
setTimeout(res, seconds * 1000)
})
}
const runPuppeteer = async() => {
const browser = await puppeteer.launch({
defaultViewport: null,
headless: false
})
const page = await browser.newPage()
await page.goto('http://127.0.0.1:5000')
await wait(3)
console.log('page opened..')
// only execute this function within a page context!.
// for example in page.evaluate() OR page.waitForFunction etc.
// don't forget to pass the selector args to the page context function!
const selectShadowElement = (containerSelector, elementSelector) => {
try {
// get the container
const container = document.querySelector(containerSelector)
// Here's the important part, select the shadow by the parentnode of the
// actual shadow root and search within the shadowroot which is like another DOM!,
return container.shadowRoot.querySelector(elementSelector)
} catch (err) {
return null
}
}
console.log('waiting for shadow elemetn now.')
const containerSelector = '.shadow'
const elementSelector = '#hoge'
const result = await page.waitForFunction(selectShadowElement, { timeout: 15 * 1000 }, containerSelector, elementSelector)
if (!result) {
console.error('Shadow element not found..')
return
}
// since waiting succeeded we can get the elemtn now.
const element = await page.evaluateHandle(selectShadowElement, containerSelector, elementSelector)
try {
// click the element.
await element.click()
console.log('clicked')
} catch (err) {
console.log('failed to click..')
}
await wait(10)
}
var app = express()
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'))
})
app.listen(5000, '127.0.0.1', () => {
console.log('listening!')
runPuppeteer()
})
Start example
$ npm i express puppeteer
$ node app.js
Make sure to use headless:false option to see what's happening.
The application does this:
start a small express server only serving index.html on /
open puppeteer after server has started and wait for the shadow root element to appear.
Once it appeared, it gets clicked and an alert() is shown. => success!
Browser Support
Tested with chrome.
Cheers ' ^^

How can I make a monitoring function to wait for an html element in puppeteer

How can I make a function that waits until a certain CSS selector loads in puppeteer?
I want to refresh the page over and over until the '.product-form__add-to-cart' is present, then I want it to continue on with the code.
The corresponding puppeteer method is page.waitForSelector.
Example:
await page.waitForSelector('.product-form__add-to-cart')
But if you need to reload the page to get the desired element (maybe because the site you are visiting can look different between visits) than you can just check if the element is present in the DOM after the page is rendered and if not: then you can try to page.reload it.
await page.goto(url, { waitUntil: 'domcontentloaded' })
const selectorExists = await page.$('.product-form__add-to-cart')
if (selectorExists !== null) {
// do something with the element
} else {
await page.reload({ waitUntil: 'domcontentloaded' })
}
You can do it in a loop as well, where you break if the selector appears, but be careful: if it never shows up you will end up in an endless loop! Set a maximum number of tries.
Edit
If you are sure about running it in a loop until the selector appears: then you can try with a while loop:
await page.goto(url, { waitUntil: 'domcontentloaded' })
let selectorExists = await page.$('.product-form__add-to-cart')
while (selectorExists === null) {
await page.reload({ waitUntil: 'domcontentloaded' })
selectorExists = await page.$('.product-form__add-to-cart')
}
// if condition meets the script will go on
As I said earlier: you can end up with an endless loop like this, so be careful.

Puppeteer - wait for generated page to be fully loaded before taking screenshot

I'm using puppeteer to convert some HTML to PNG using the screenshot method.
First, I fetch some SVG, then I create a page, and set the SVG as page content.
fetch(url)
.then(data => data.text())
.then((svgText) => {
// res.set('Content-Type', 'text/html');
const $ = cheerio.load(svgText)
return $.html()
})
.then(async (html) => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setContent(html)
const file = await page.screenshot()
res.set('Content-Type', 'image/png');
await browser.close()
res.send(file)
})
.catch((err) => {
console.log(err);
logger.log({
level: 'error', message: 'GET /product', err
})
})
})
The problem is, texts in my SVG includes a specific font. This font is loaded using the #import CSS tag. If I set my method to return the HTML, the fonts are loaded, then, after a slight delay, they get applied to my texts. Unfortunately, when using the screenshot method, my texts are not styled anymore. I suppose it is because the screenshot is taken before the fonts are loaded and applied, therefore rendering a text with a fallback font.
Is there a way to make sure that the page is completely rendered before taking the screenshot ?
I tried using the page.on('load') event listener, but this doesn't change anything the script just runs forever.

Not clicking on element after class specifically called

For some reason I'm not able to click on an element that appears on a screen with puppeteer js.
Here is the code:
const getAllElements = await page.$$('._1Nk0C');
for (let [i, link] of getAllElements.entries()) {
try {
await link.click();
await sleep.sleep(4);
await link.click('._1NHYN _3d86A Ddtb4');
} catch (e) {
console.error(e);
}
}
Here I find all elements with '._1Nk0C'
It then clicks on the element which as it enlarge in forefront. await link.click();
I then try to click the button on screen. I can confirm this is on the screen.
await link.click('._1NHYN _3d86A Ddtb4');
Nothing happens. It doesn't error out just doesn't click on element. Am I missing something?
elementHandle.click([options]) does not accept a selector as an argument. If you're trying to click on an element in the page based on its selector try:
await link.click();
await sleep.sleep(4);
await page.click(selector);

how to execute a script in every window that gets loaded in puppeteer?

I need to execute a script in every Window object created in Chrome – that is:
tabs opened through puppeteer
links opened by click()ing links in puppeteer
all the popups (e.g. window.open or "_blank")
all the iframes contained in the above
it must be executed without me evaluating it explicitly for that particular Window object...
I checked Chrome's documentation and what I should be using is Page.addScriptToEvaluateOnNewDocument.
However, it doesn't look to be possible to use through puppeteer.
Any idea? Thanks.
This searches for a target in all browser contexts.
An example of finding a target for a page opened
via window.open() or popups:
await page.evaluate(() => window.open('https://www.example.com/'))
const newWindowTarget = await browser.waitForTarget(async target => {
await page.evaluate(() => {
runTheScriptYouLike()
console.log('Hello StackOverflow!')
})
})
via browser.pages() or tabs
This script run evaluation of a script in the second tab:
const pageTab2 = (await browser.pages())[1]
const runScriptOnTab2 = await pageTab2.evaluate(() => {
runTheScriptYouLike()
console.log('Hello StackOverflow!')
})
via page.frames() or iframes
An example of getting eval from an iframe element:
const frame = page.frames().find(frame => frame.name() === 'myframe')
const result = await frame.evaluate(() => {
return Promise.resolve(8 * 7);
});
console.log(result); // prints "56"
Hope this may help you