Puppeteer - why doesn't element reflect DOM changes? - javascript

I have a function that waits for a selector under the scope of an element. The ElementHandle I'm passing in doesn't seem to be getting updated as the DOM updates- I log the outerHTML of this element each time my waitForFunction call fails, and changes aren't there.
async function waitForSubElSelector(page, el, subElSelector) {
// offset/rects checks for visible check (from https://stackoverflow.com/a/33456469)
await page.waitForFunction((selector, scopeEl) => {
const subEl = scopeEl.querySelector(selector)
let found = !!(subEl && (subEl.offsetWidth || subEl.offsetHeight || subEl.getClientRects().length));
if (!found) {
console.log(`waitForSubElSelector failed EL: ${scopeEl.outerHTML} SELECTOR: "${selector}", ${!subEl ? 'not found' : subEl.offsetWidth + ', ' + subEl.offsetHeight + ', ' + subEl.getClientRects().length}`)
}
return found;
}, {}, subElSelector, el
).catch(e => {
console.log(`waitForSubElSelector failed w/ selector '${subElSelector}'`)
throw e;
})
return await el.$(subElSelector);
}
I also verified this in the browser Console - I'm able to find the element and then the selector from there.
One use case is waiting for a Next button to be available, e.g.:
await waitForSubElSelector(this.browserPage, pageEl, '#nextButton:not([disabled])')
How can I get this working?
One idea I had— maybe my web framework replaces part of the DOM so the ElementHandle I have isn't the current one; is there some 'in-DOM' check in Puppeteer?

Related

Playwright - TypeError: hoverElement.hover is not a function

I am trying to run this
await hoverElement.hover();
so that the mouse will hover over the element in question
I get an error saying TypeError: hoverElement.hover is not a function
I am reading from the playwright API documentation https://playwright.dev/#version=v1.5.2&path=docs%2Fapi.md&q=elementhandlehoveroptions
What am I doing wrong?
test('Selection using hover Speak', async () => {
if (EnableTests.run_hover_speak === "true") {
const expectedResult = TestPage.Div2_Expected_Result;
const hoverElement = TestPage.Div2_css;
await test_functions._hoverSpeak(page);
await hoverElement.hover();
const highlightedText = await test_functions._hiliteSelection(page);
const actual = JSON.stringify(highlightedText); console.log("ACTUAL RESPONSE " + actual);
const expected = JSON.stringify(expectedResult); console.log("EXPECTED RESPONSE " + expected);
assert(expected === actual);
};
});
If hoverElement.hover is not a function, then it implies that hoverElement (and therefore TestPage.Div2_css) is probably not a proper Playwright elementHandle object. I can't see from your code where you get TestPage from, but it might be that Div2_css is a reference to a plain DOM node, or its CSS, rather than an elementHandle.
To fix this, you might need to assign hoverElement using something like page.$('.div2') (replace .div2 with a suitable selector to target the element you want to hover over).

How can I set event listener after dom is loaded by className

I have a card with class name contact-card when i click this card it should toggle another div's visibility. The card is laid out after getting data from a database.
function buildCard(doc) {
let ulList = document.createElement('ul');
let listItem = document.createElement('li');
let link = document.createElement('a');
let icon = document.createElement('i');
let icon2=document.createElement('i');
ulList.className = "collection";
listItem.className = "collection-item avatar card-contact";
link.className = "secondary-content";
icon.className = "material-icons";
icon2.className = "material-icons";
listItem.setAttribute('id', 'card-contact');
icon.setAttribute('id','mail');
icon2.setAttribute('id','fav');
if(doc.read==true){
icon2.textContent='drafts';
}else{
icon2.textContent='mail_outline';
}
if(doc.favourite==true){
icon.textContent = 'grade';
}else{
icon.textContent='star_border';
}
link.appendChild(icon2);
link.appendChild(icon);
listItem.appendChild(link);
ulList.appendChild(listItem);
mainContactCard.appendChild(ulList);
}
MRef = db.collection("user").doc("3454").collection('contact').orderBy("time", "desc").limit(10);
MRef.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
buildCard(doc.data());
});
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
var contactCard = document.querySelector('.card-contact');
setTimeout(ert, 2000);
function ert(){
contactCard.addEventListener('click', toggleThis);
}
function toggleThis() {
$("#chat-card").toggle("fast");
//chat-card is supposed to be the div that fades in and out
}
I tried to add a timeout of ten seconds thinking that the dom is being loaded fast and failing to get the value of the variable contactCard but this is not the case. After ten seconds I still get the error Uncaught TypeError: Cannot read property 'addEventListener' of null
What am I doing wrong. I need there to be many cards which if i click any it toggles visibility of the div. That is why I am selecting by className
(more added code)
lastMessageRef.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
buildContactCard(doc),str();
});
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
function str() {
var contactCard=document.getElementsByClassName('card-contact')
for (var i = 0; i < contactCard.length; i++) {
//Distribute(contactCard.item(i));
contactCard.item(i).addEventListener('click', function (e) {
$("#chat-card").toggle("fast");
if (e.target && e.target.nodeName == "LI") {
// List item found! Output the ID!
console.log("List item ", e.target.id.replace("", ""), " was clicked!");
}
});
}
}
The element does not exist yet when you try to access it.
Adding a 10 seconds delay after document.querySelector('.card-contact'); does not help since you try to find the element before waiting the 10 seconds.
To make this works:
var contactCard = document.querySelector('.card-contact');
setTimeout(ert, 10000);
function ert(){
contactCard.addEventListener('click', toggleThis);
}
You should change it to:
setTimeout(ert, 10000);
function ert(){
var contactCard = document.querySelector('.card-contact');
contactCard.addEventListener('click', toggleThis);
}
Note that this is still not a good solution. You have many ways to solve your issue, depending on the coupling between the creation of the element, and its use. Without more of your code, we cannot give you a proper solution.
EDIT (I just refreshed and you added some code): The above code only gets the first element with the class .card-contact. If you want to keep this solution, you should probably use querySelectorAll and iterate over the elements found instead of using querySelector. Anyway, keep on reading.
One solution could be to return your element from the function that creates it, and add the listener on the returned element.
const card = buildCard(doc.data());
card.addEventListener(/* ... */);
Another one could be using callbacks, when you call the function to create the element, you pass a callback as parameter and call the callback inside the function, with the created element as parameter. Since what you're doing is not asynchronous, I don't really see an advantage to using a callback here.
function callback(card) {
card.addEventListener(/* ... */);
}
buildCard(doc.data(), callback);
Another one could be using events, a custom emitter. When you're done creating your element, you dispatch an event to notify listeners that the element is created. You assign a listener on this event, to do all post-creation things.
myEmitter.addEventListener('card-created', /* ... */);
buildCard(doc.data()); // This function should fire 'card-created' on myEmitter
Another one... is to add a MutationObserver. I don't recommend it as it is overkill for your simple use-case. This would let you "observe" for changes in an element's child list, so you could detect when the card is added to its parent, then execute your code.
Hope this helps you.
(Note: This is my first activity on SO, so I cannot comment)

Puppeteer find array elements in page and then click

Hello I have site that url are rendered by javascript.
I want to find all script tags in my site, then math script src and return only valid ones.
Next find parent of script and finally click on link.
This is what i have:
const scripts = await page.$$('script').then(scripts => {
return scripts.map(script => {
if(script.src.indexOf('aaa')>0){
return script
}
});
});
scripts.forEach(script => {
let link = script.parentElement.querySelector('a');
link.click();
});
My problem is that i have script.src is undefined.
When i remove that condition i move to forEach loop but i get querySelector is undefined. I can write that code in js inside of console of debug mode but i cant move it to Puppeteer API.
from console i get results as expected
let scripts = document.querySelectorAll('script');
scripts.forEach(script=>{
let el = script.parentElement.querySelector('a');
console.log(el)
})
When you use $$ or $, it will return a JSHandle, which is not same as a HTML Node or NodeList that returns when you run querySelector inside evaluate. So script.src will always return undefined.
You can use the following instead, $$eval will evaluate a selector and map over the NodeList/Array of Nodes for you.
page.$$eval('script', script => {
const valid = script.getAttribute('src').indexOf('aaa') > 0 // do some checks
const link = valid && script.parentElement.querySelector('a') // return the nearby anchor element if the check passed;
if (link) link.click(); // click if it exists
})
There are other ways to achieve this, but I merged all into one. Ie, If it works on browser, then you can also use .evaluate and run the exact code and get exact desired result.
page.evaluate(() => {
let scripts = document.querySelectorAll('script');
scripts.forEach(script => {
let el = script.parentElement.querySelector('a');
console.log(el) // it won't show on your node console, but on your actual browser when it is running;
el.click();
})
})

Nightwatch how can I make element selector visible within a command?

I am writing a test in Nightwatch to test pagination for a grid. I am using Page Objects and element selectors to make maintenance of the test suite easier. It seems, however, that I have run into a limitation with using element selectors within commands. The following code executes without error:
pagination() {
var lastPageNum;
var currentPageNum;
var newPageNum
return this
.getText('#pageNum1', function(result) {
currentPageNum = parseInt(result.value);
console.log('Current Page Number = ' + currentPageNum);
})
.getText('#pageNum2', function(result) {
lastPageNum = parseInt(result.value);
console.log('last Page Number = ' + lastPageNum);
if (lastPageNum >= 2) {
this.useXpath()
.waitForElementVisible('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]', 3000)
.click('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]')
.getText('//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[1]', function(result) {
newPageNum = parseInt(result.value);
console.log('New Page = ' + newPageNum);
this.assert.equal(newPageNum, currentPageNum + 1, "Assert pagination to Next page passed.");
})
} else {
console.log('Not enough rows exist in grid to test pagination, pages = ' + lastPageNum)
}
})
},
Note that I am using element selectors for the .getText commands. Here are the selectors I am using:
pageNum1: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[1]',
locateStrategy: 'xpath'
},
pageNum2: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/span[2]',
locateStrategy: 'xpath'
},
So far, so good. The issue I am running into is when I attempt to replace the xpath elements inside the .getText command with element selectors as well. Here is the code I am attempting to replace the above with:
pagination() {
var lastPageNum;
var currentPageNum;
var newPageNum
return this
.getText('#pageNum1', function(result) {
currentPageNum = parseInt(result.value);
console.log('Current Page Number = ' + currentPageNum);
})
.getText('#pageNum2', function(result) {
lastPageNum = parseInt(result.value);
console.log('last Page Number = ' + lastPageNum);
if (lastPageNum >= 2) {
this.useXpath()
.waitForElementVisible('#nextPageButton', 3000)
.click('#nextPageButton')
.getText('#pageNum1', function(result) {
newPageNum = parseInt(result.value);
console.log('New Page = ' + newPageNum);
this.assert.equal(newPageNum, currentPageNum + 1, "Assert pagination to Next page passed.");
})
} else {
console.log('Not enough rows exist in grid to test pagination, pages = ' + lastPageNum)
}
})
},
And here is the additional element selector I am using:
nextPageButton: {
selector: '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]',
locateStrategy: 'xpath'
},
When I try to run the test after replacing the xpaths in the .getText command with element selectors, the test fails with the following error:
Timed out while waiting for element <#NextPageButton> to be present for 3000 milliseconds. - expected "visible" but got: "not found"
Is there a way to get the element selector to be visible within the .getText command function?
Since there does not appear to be a way to actually use the selectors within a command, I will post the workaround I have been using recently. It is fairly simple: in the page object, I am declaring a constant that contains the web element I want to use as a selector. This needs to be at the top of the page object before declaring the constants or variables you are using to hold the page object methods. The original snippets I posted above only contain the method, not the entire page object, but the declaration needs to happen well before that. Here is an example from the page object that contains the above method:
const nPageButton = '//*[#id="borderLayout_eRootPanel"]/div[2]/div/div/span[2]/button[3]';
const gridComands = {
//page object methods go here
}
Once you have declared the web element as a constant, you can use that constant when constructing your element selector:
nextPageButton: {
selector: nPageButton,
locateStrategy: 'xpath'
},
This will allow for easier maintenance, while sacrificing a bit of the functionality that selectors provide. You can use the constant within the page object when the selector does not work, you will just lose the utility of having the locate strategy defined, so you will need to rely on .useXpath() in the method when using xpaths for your web elements.

jquery calling null element that actually exists

My code works on localhost, but when I implement it on my site, it doesnt.
The error log says it's calling for an element that doesn't exist. I've reach to the conclusion that it can't see the element because the element is loaded dynamically.
The element is the class .newsitem_text, it's a div that contains a blog post. I believe that the jquery is calling for the class before the class is being loaded by the page.
Here is one example fiddle:
http://jsfiddle.net/ku6L240c
The error:
Uncaught TypeError: Cannot read property 'html' of null
47-ganhe-dinheiro-atraves-de-downloads:1093 Uncaught SyntaxError: Unexpected token ILLEGAL
The code:
<javascript>
var wordList = $(".newsitem_text").html().split(' ');
var newHtml = '';
$.each(wordList, function(index, word){
newHtml += ' ' + word;
if (index == 50) {
newHtml += '<div>Some HTML</div>'
}
})
;
$(".newsitem_text").html(newHtml);
</javascript>
How can I make the script wait until the class is loaded by the page, then it gets executed or something?
Seems you are doing dynamic HTML within JS code and immediately trying to get the just added tags. If that is the case, your code will have to wait and keep checking until browser rendered your new HTML and add the nodes to DOM, then you can query them or do something about.
The only way I found works is usint setTimeOut and keep checking then execute your last statement. I create a function below that checks wait and check for certain condition then execute a call back function.
//A function to wait until either times up, or until a pass in "funct" returns "true", which ever occured first.
//funct - callback function, to be returned true or false.
//done - an optional callback function for notify when waiting is over.
//timeout - the amount of time in million-second to wait.
//caller - optional string of caller for monitoring if multiple waiting session.
function waitToSync(funct, done, timeout, caller) {
//This is a hack synchronize to wait until funct() returns true or timeout becomes < 0.
caller = caller || '';
if ((funct === undefined) || typeof (funct) != 'function') return;
function waiting() {
if (!funct()) {
var dt = new Date();
console.log(caller + " waiting: " + dt.format('yyyy-mm-dd h:MM:ss'));
if ((timeout - 1000) > 0)
setTimeout(waiting, 1000); //1 second.
else {
console.log(caller + ': waitToSync timed out!!!');
document.body.style.cursor = 'default';
}
timeout -= 1000;
}
else {
if (done !== undefined && (typeof done === 'function'))
done();
}
}
waiting();
}
Do all you dynamic or anything to want to wait. Then call the WaitToSync
$.each(wordList, function(index, word){
newHtml += ' ' + word;
if (index == 50) {
newHtml += '<div>Some HTML</div>'
}
});
waitToSync(
function wait() { return document.getElementsByClassName("newsitem_text").length > 0; },
function dosomething() { $(".newsitem_text").html(newHtml); },
10000, //wait up to 10 seconds.
'dynamic add HTML');
You can try to execute this function at window load or at document ready
just put ur function inside this:
$(window).load(function(){
//your function here
});
or here
$(document).ready(function(){
//your function here
});
It is way to better to put the code in the end of the body tag after all your content so javascript can run after everything is loaded !

Categories