Looping inside a page.evaluate in Puppeteer - javascript

I have a loop inside a page.evaluate method. The loop iterates a query selector which catches an innerText from multiple instances of a text element in a page.
I am receiving an error Evaluation Failed: Cannot read property of 'innerText'
I tried to loop outside of page.evaluate, but my iteration variable is not accessible from within the page.evaluate function.
// Here's a rough draft of what i'm trying to achieve:
const scrapeData = [];
const data = await page.evaluate(() => {
// Iteration to capture each target text in the page
for (var i = 1; i < 9; i++) {
// Select target text
const serpDesc = document
.querySelector(
`#rso > div:nth-child(4) > div > div:nth-child(${i}) > div > div > div.s > div > span`
)
.innerText.trim();
// Build an array for the captured text
scrapeData[i] = serpDesc
return {
serpDesc
};
};
});
My goal is to scrape some link descriptions(plain text) from a page into an array. Without the iteration code, everything works fine.

Try:
const serpDesc = await page.evaluate(
() => [...document.querySelectorAll(`#rso > div:nth-child(4) > div > div:nth-child(${i}) > div > div > div.s > div > span`)].map(elem => elem.innerText)
);
You will probably need to reconstruct your selector a bit, or maybe wrap serpDesc function in a for of or forEach loop.
You could also try something like
async function elSelector(i) {
//Where i is the incremented value you pass
await page.evaluate((i) => {
let eval = $('yourSelector').toArray();
$(eval[i]).innerText
}, i)
}
for (i=0; i<9; i++) {
elSelector(i);
}

Related

Puppeteer - Accessing an IFrame within an IFrame

I am having an issue retrieving an IFrame within an IFrame I am not exactly sure how to get the result that I am looking for, I am able to retrieve the Parent IFrame just fine, however when trying to choose the child it does not find the selector of the click method. How would I go about solving this? I am quite new to Puppeteer
// Get Email
await click(contentBuilderFrame, getEmail)
await page.waitFor(200);
await click(contentBuilderFrame, '#index-entry > div > div > div.active > div > div > div.contentproperties-header > div.contentproperties-buttons > div > div > div > a');
await page.waitFor(200);
const container = await findFrame(page, "EmailAppContainer");
await page.waitFor(200);
// Will not find it
const child = await findFrame(container, "cloud/tools/SSO.aspx?legacy=1&env=default&appId=6DF293A1-4FDC-41CD-AA08-89EFC793C33C&deepLink=%2f%23%2femails");
// Timeout because cant find selector due to not finding frame
await click(child, '#sendflow-step-content')
findFrame Method:
async function findFrame(page, urlPart) {
const mainFrame = page.mainFrame();
const contentFrames = await getContentFrames(mainFrame);
let frame = null;
for (const contentFrame of contentFrames) {
if (contentFrame.url().includes(urlPart)) {
frame = contentFrame;
}
}
return frame;
}
getContentFrames:
async function getContentFrames(frame) {
const contentFrames = [];
const iframes = await frame.$$("iframe");
for (const iframe of iframes) {
const contentFrame = await iframe.contentFrame();
if (contentFrame != null) {
contentFrames.push(contentFrame);
let subContentFrames = await getContentFrames(contentFrame);
if (subContentFrames.length > 0) {
contentFrames.push(...subContentFrames);
}
}
}
//console.log(contentFrames);
return contentFrames;
}
My understanding here is that I get all the frames with the getContentFrames method - I made sure that is the case by logging the output for these, but unsure how to proceed from there on out?

Add value for DOM nodes in list

I am trying to create a list of links using DOM nodes from the data in the database. Instead of having a link for each DOM element. The whole list only have one link. I want the link to be separate because I want to add value to it so I can identify which link the user clicks.
This is the link in HTML
This is my Javascript code in which I take data from database. I want to put each corresponding in database as the value for the corresponding link in the HTML code.
window.onload = async function outsong() {
var selected = localStorage.getItem("category")
document.getElementById("result").innerHTML = selected;
var result = [];
if(selected == "Popular") {
await db.collection("Song").doc("Popular").collection("songs").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
result.push(doc.data());
});
});
console.log(result.length);
for(a = 0; a < result.length; a++) {
var node = document.createElement("li");
var textnode = document.createTextNode(result[a].song_name);
node.appendChild(textnode);
document.getElementById("songlist").appendChild(node);
var anchor = document.getElementById("songlist");
var att = document.createAttribute("value");
att.value = result[a].song_name;
anchor.setAttributeNode(att);
}
}
In the image, I want to separate the link, not one as the whole.
Within your loop, you'll need to create a new <a> element within each <li>, and add the song link to that a element. Modifying your code:
const songList = document.getElementById("songlist");
for(let a = 0; a < result.length; a++) {
var node = document.createElement("li");
var link = document.createElement('a');
link.setAttribute('href', result[a].song_link);
link.innerText = result[a].song_name;
node.appendChild(link);
songList.appendChild(node);
}
I don't see where you're getting the song_link from, so for the sake of the example, I guess it was in the result[a] object, alongside song_name.

Is there a way to iterate and compare dom elements in nodejs?

I'm trying to get two values from a site I'm automating. I iterate through an array of elements and try to compare the value of the dropdown to what is on the page to make sure they equal one another. One of the values can only be accessed by .getValue(). The other is accessed by .getText(). I would like to save the result.value of these callback functions and compare the results.
I've tried to console.log both of these values and I get them back, but I can't return anything from this callback function. I also can't seem to save it's value in a variable and return that either. I've tried looking to do it in plain javascript with document.getElementById() but that works for client-side javascript, not serverside like nodejs. Just trying to compare two values together
for (let i = 1; i <= 20; i++) {
browser
.element('css selector', `mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value && result.value.ELEMENT) {
browser.isVisible(`mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value === true) {
browser.click(`mat-nav-list > a:nth-child(${i})`)
let chunkView = '#mat-input-0';
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`
browser.getValue(chunkView, function(result) {
chunkView = result.value
console.log(chunkView)
})
browser.getText(sideBar, function(result) {
console.log(result.value);
})
}
})
}
})
//.pause(2000)
//.pause(10000)
}
When I loop through I would expect to get the two values sideBar result.value to equal chunkView result.value. Current output can only log the two separate values.
I haven't used Nightwatch.js before, so I'm basing my answer on the assumption that browser.click, browser.getValue, and browser.getText run asynchronously, as that's fairly common with UI and UI test frameworks, and, if they did run synchronously, there would be no point in using callbacks.
You're probably going to want to get used to working with the JavaScript Promise. Since JavaScript engines are single-threaded, there is no way to spinlock/sleep while another thread handles some change (such as updating the UI after a click event). A Promise allows you to get around this by working with callbacks and handling events behind the scenes.
You can then chain promises using promise.then() which pass the returned value to the next callback.
In your case, though, I would wrap the two functions that retrieve values in promises and then use Promise.all(). This allows them to complete in any order which could improve performance.
browser.isVisible(`mat-nav-list > a:nth-child(${i})`,
function(result) {
if (result.value === true) {
browser.click(`mat-nav-list > a:nth-child(${i})`);
let chunkView = '#mat-input-0';
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`;
let valPromise = new Promise(resolve => {
browser.getValue(chunkView, resolve);
});
let textPromise = new Promise(resolve => {
browser.getText(sideBar, resolve);
});
Promise.all([valPromise, textPromise]).then(([valueResult, textResult]) => {
browser.assert.strictEqual(valueResult.value, textResult.value,
`Server-side value '${value.result}' does not match client-side value '${text.result}'`);
});
}
});
Use the perform method so that callbacks are ensured to have completed before the next command. (see https://github.com/nightwatchjs/nightwatch/wiki/Understanding-the-Command-Queue#the-perform-command)
Something like this
browser.getValue(chunkView, function(result) {
chunkView = result.value
console.log(chunkView)
}).perform(function() {
// here you have access to chunkView so you can compare it
browser.getText(sideBar, function(result) {
console.log(result.value);
if (chunkView === result.value) {
console.log('They are the same!');
}
})
});
or you could chain perform commands so that you can do the comparison at the end regarding the number of intermediate steps.
let chunkView = '#mat-input-0',
chunkViewResult;
let sideBar = `body > gps-app-root > div > div.sidebar-desktop > gps-app-sidebar-menu > div > div.product-list-wrap > mat-nav-list > a:nth-child(${i}) > div`,
sideBarResult;
browser.getValue(chunkView, function(result) {
chunkViewResult = result.value
console.log(chunkView)
}).getText(sideBar, function(result) {
sideBarResult = result.value
console.log(sideBarResult);
}).perform(function() {
if (chunkViewResult === sideBarResult) {
console.log('They are the same!')
}
})
I found if I nested .getValue() and .getText and assigned variables I was able to compare the two.
You can always use .execute() from nightwatch and run any javascript you want in the page.
client.execute(
function (data) {
// this is runned in browser
},
[what_you_pass_to_the_function_above_as_data], // multiple parameters = multiple elements in array
function (result) {
// result.status == -1 if error happend
// here you cand compare what you want... result.value is the returned things from the browser executed function.
}
);

Problem with infinite loop when manipulating DOM

I'm learning about DOM manipulation and, to practice, I'm trying to get the first 100 Twitter users who have twitted about #Javascript (see link). As, for now, Twitter doesn't allow you to use console.log() function in the browser console, I have managed to show any string visually in the HTML, in this case, under the search textbox.
This is my "custom" console.log()
function consoleLog(data) {
var searchTextBox = document.querySelector("#doc > div.topbar.js-topbar > div > div > div > div > div");
var p = document.createElement("p");
var innerText = document.createTextNode(data);
p.appendChild(innerText);
searchTextBox.appendChild(p);
}
For getting the usernames, I keep scrolling the page every 4 seconds and looking for usernames until I have 100 or more of them in my usernames variable.
var scrollPage = setInterval(function() {
window.scrollTo(0, document.body.scrollHeight);
}, 4000);
var usernames = [];
while (true) { // <------ PROBLEM
if (usernames.length < 100) {
consoleLog("Getting usernames again");
usernames = getUsernames();
}
else {
consoleLog("We have ENOUGH usernames. BREAK");
clearInterval(scrollPage);
printUsernames();
break;
}
}
function printUsernames() {
for(var user of usernames) {
consoleLog(user);
}
}
function getUsernames() {
var results = [];
var usernameNodes = document.getElementsByClassName("username u-dir u-textTruncate");
var username = usernameNodes[0].textContent;
for(var node of usernameNodes) {
results.push(node.textContent);
}
return results.filter(isUnique);
}
function isUnique(value, index, self) {
return self.indexOf(value) === index;
}
The problem is that the while loop enters in infinte loop and I don't know why. I think the logic of the code is correct. In fact, if I first copy and paste all the declared functions to the browser console, then start the scrollPage interval and, lastly, start the while loop, it works well. The problem comes when I copy and paste all the code at one time in the browser console. It is like the executions of the interval and the while loop conflict in some way. But I can't understand.
Its better to have while conditioned like this:
var usernames = [];
// This will automatically end when length is greater or equall 100
// no need to break
while (usernames.length < 100) {
consoleLog("Getting usernames again");
usernames = getUsernames();
}
consoleLog("We have ENOUGH usernames.");
clearInterval(scrollPage);
printUsernames();

WebDriver in javascript: how to check if an element exists?

How can I check if an element exists in selenium?
I have tried:
browser.driver.findElements(by.id('my-id'))
but it does not seem to work.
Use isElementPresent
browser.driver.isElementPresent(by.id('my-id'))
or isPresent
element(by.id('my-id')).isPresent()
The problem is looking for an element that does not exist throws an exception. You ar eon the right track with using findElements as this will not throw an error if it cannot find the element, the problem you have is being left with a list that contains the elements found but not comparing to make sure that there is at least 1 element in the list (e.g. it found one element)
public boolean exists(By by){
return !driver.findElements(by).isEmpty();
}
is a function that will return true if it exists, false otherwise. Its clear from this method how you could modify it to suit your need.
webdriver.js example
In this example I am waiting for a modal to exist because its a modal that fades in
See the line that reads
const doesModalFadeInExist = await this.driver.executeScript('return document.getElementsByClassName("modal fade in")');
You can put this line of code in a wait that waits for the element array to be >=1
This way you know it is there after checking inside the wait.
We can also check opacity at the same time.
Once the element array >=1 and opacity = '1' . then we exit the wait and continue the test
async waitForModalFadeIn() {
try {
let isVisible;
let opacityReturned;
let isModalFadeInFound;
const dialog = await this.driver.wait(until.elementLocated(this.baseselector.MODAL_FADE_IN));
await this.driver.wait(async () => {
const doesModalFadeInExist = await this.driver.executeScript('return document.getElementsByClassName("modal fade in")');
isModalFadeInFound = doesModalFadeInExist;
const bool = await this.isDisplayed(this.baseselector.MODAL_FADE_IN);
isVisible = bool;
const opacity = await dialog.getCssValue('opacity');
opacityReturned = opacity;
return isVisible === true && isModalFadeInFound.length > 0 && opacity === '1';
}, 4000);
return opacityReturned === '1' && isVisible === true && isModalFadeInFound.length > 0;
} catch (err) {
console.log(`Function waitForModalFadeIn: Failed to open modal${err}`);
return null;
}
}
Example Test
it('Trigger "unmetered" modals in the technical spec section', async () => {
await wHost.visit(page = '/web-hosting');
const table = await driver.wait(until.elementLocated(wHost.selector.COMPETITOR_TABLE), this.pageLoadTimeOut);
await driver.executeScript('arguments[0].scrollIntoView()', table);
const modals = await table.findElements(wHost.selector.UNLIMITED_MODAL);
for (let i = 0; i < modals.length; i++) {
await modals[i].click();
assert.isTrue(await common.waitForModalFadeIn());
assert.isTrue(await common.closeModal());
}
});
This is what you are looking for, This function takes id as parameter,
If the element is found it returns json object with status=true and element you were looking for.
async function ElementExistsID(element){
return new Promise(async (resolve)=>{
try{
let ElementResult=await driver.findElement(webdriver.By.id(element));
resolve({"element":ElementResult,"status":true});
}
catch(error){
log.warn(error);
resolve({"status":false});
}
})
}
You can use it like this
let ElementIamLookingFor=await ElementExistsID(IDOfTheElement);
if(ElementIamLookingFor.status){
console.log("Element exists");
}
else{
throw new Error("Element not found");
}
After many attempts, the following worked for me:
function doIfPresent(elementId, doIfPresent, next) {
var elem = by.id(elementId);
browser.driver.isElementPresent(elem).then(function (present){
if (present) {
doIfPresent(element(elem));
}
next();
});
}
eg.
doIfPresent('my-button', function(element){ element.click();}, function(){
// your code continues here
});
I don't get it though why we'd need to use futures here.

Categories

Resources