I would like to get all the elements in my DOM with a specific css path:
var elements = await chromeless.evaluate(() => document.querySelectorAll('div a'))
console.log(elements[0].innerHTML)
console.log(elements[1].innerHTML)
but this code gives me the error "Object reference chain is too long" on the first line
This code works though:
var element = await chromeless.evaluate(() => document.querySelectorAll('div a')[0].innerHTML)
console.log(element)
and I could potentially use a loop to retrieve them all but I have no idea how many elements have this css in my DOM so I don't know how many times to loop.
What's the correct syntax to get all the desired elements?
const elements = await chromeless.evaluateHandle(() => {
const allOccurances = [...document.querySelectorAll("div a")];
const data = allOccurances.map((node) => node.innerHTML);
return data;
});
const response = await elements.jsonValue();
console.log(response);
Instead of chromeless we can use page by creating a page as per puppeteer documentation https://pptr.dev/#?product=Puppeteer&version=v13.1.3&show=api-class-page
Related
Is there a way to traverse through html elements in playwright like cy.get("abc").find("div") in cypress?
In other words, any find() equivalent method in playwright?
page.locator("abc").find() is not a valid method in playwright though :(
If you assign the parent element handle to a variable, any of the findBy* functions (or locator) will search only in the parent element. An example below where the parent is a div, the child is a button, and we use .locator() to find the elements.
test('foo', async ({ page }) => {
await page.goto('/foo');
const parent = await page.locator('div');
const child = await parent.locator('button');
});
You can just combine the selectors, this will resolve to div below abc
page.locator("abc div")
Let's consider the website https://www.example.com with the following HTML
<body style="">
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p>
More information...
</p>
</div>
</body>
As mentioned by #agoff, you can use nested locator page.locator('p').locator('a') or you can specify the nested selector directly in the locator page.locator('p >> a')
// #ts-check
const playwright = require('playwright');
(async () => {
const browser = await playwright.webkit.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://www.example.com/');
// Here res1 and res2 produces same output
const res1 = await page.locator('p').locator('a'); // nested locator
const res2 = await page.locator('p >> a'); // directly specifying the selector to check the nested elements
const out1 = await res1.innerText();
const out2 = await res2.innerText();
console.log(out1 == out2); // output: true
console.log(out1); // output: More information...
console.log(out2); // output: More information...
// Traversal
const result = await page.locator('p');
const elementList = await result.elementHandles(); // elementList will contain list of <p> tags
for (const element of elementList)
{
const out = await element.innerText()
console.log(out);
}
// The above will output both the <p> tags' innerText i.e
// This domain is for use in illustrative examples in...
// More information...
await browser.close();
})();
Since you mentioned that you need to traverse through the HTML elements, elementHandles can be used to traverse through the elements specified by the locator as mentioned in the above code.
I'm creating a page object model and one of the properties is all the users from a table. The table has a few columns so I'd like to parse that table and create a user object for each row, then return that set to then be used in tests. So far, this is what that property of my page object looks like:
users: {
get: function() {
let userRecords = [];
var users = element.all(by.repeater('user in users')).each(function(tr, index) {
let user = {
name: tr.element(by.css('td:nth-child(2)')).getText().then(text => {return text;}),
role: tr.element(by.css('td:nth-child(3)')).getText().then(text => {expect(text).toBeTruthy();}),
status: tr.element(by.css('td:nth-child(4)')).getText().then(text => {expect(text).toBeTruthy();}),
//actionsButton: tr.element(by.css('btn btn-default'))
};
userRecords += user;
}).then(userRecords => {return userRecords});
return userRecords;
}
},
Through trial and error I encounter one of two outcomes when just trying to print to screen each element of userRecords:
each element prints as undefined or
userRecords is not defined.
Just to reiterate, I'm simply trying to build an array that holds each user as an object to then be able to iterate / filter on that set in my tests.
Given the approach I'm taking, what's the ideal way to construct this user array and resolve the promises?
Edit: it's worth noting that if I do a console.log() within each of the getText().then() statements, it does print the correct data from the table. So, I do know that it's reading the table correctly.
I'd go with a method that returns json, and would make it async
users: async function() {
let userRecords = [];
var users = element.all(by.repeater('user in users'));
for (let i = 0; i < await users.count(); i++) {
let tr = users.get(i);
let user = {
name: await tr.element(by.css('td:nth-child(2)')).getText(),
role: await tr.element(by.css('td:nth-child(3)')).getText(),
status: await tr.element(by.css('td:nth-child(4)')).getText()
};
userRecords.push()
}
return userRecords;
},
and then use:
console.log(
JSON.stringify(
await constructorName.users()
)
)
should be as simple as that. Note, I didn't test the code, but I did use the approach in my experience. So it may require some minor modifications
In general, try to avoid .then - async/await is easier to use, .each - go with for loop instead. Also userRecords += user; doesn't do what you think it does (though I may be wrong here)
i am making a request to this url to translate text from english to spanish
URL: https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=es&dt=t&q=Hello
and efectivelly i´m getting translated text to spanish, so, now i want to get dinamically all innerText in body document and then put again translated text, how can i do this?
In simple words, I want to dynamically translate the website with a button click.
This is my example code to start:
let textToBeTranslate =["hello","thanks","for","help me"]
var url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=es&dt=t&q="+textToBeTranslate;
fetch(url)
.then(data => data.json()).then(data => {
//Text translated to spanish
var textTranslated = data[0][0][0].split(", ");
console.log(textTranslated)
//output: ["hola gracias por ayudarme"]
//Now i want to dinamically put translated text in body tag again
}).catch(error => {
console.error(error)
});
Try this:
const translateElement = async element => {
const
elementNode = element.childNodes[0],
sourceText = elementNode && elementNode.nodeValue;
if (sourceText)
try {
const
url = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=es&dt=t&q=' + sourceText,
resultJson = await fetch(url),
result = await resultJson.json(),
translatedText = result[0][0][0].split(', ');
elementNode.nodeValue = translatedText;
} catch (error) {
console.error(error);
}
}
}
For a single element - Just call it, like this:
(async () => await translateElement(document.body))();
For all elements in the DOM - You will need to recursively go over all elements starting from the desired parent tag (body, in your case), and call the above function for each element, like this:
(async () => {
const
parent = 'body',
selector = `${parent}, ${parent} *`,
elements = [...document.querySelectorAll(selector)],
promises = elements.map(translateElement);
await Promise.all(promises);
})();
Remarks:
I used childNodes[0].nodeValue instead of innerHtml or
innerText to keep the child elements.
Note that go over the entire DOM is not recommended and can lead to problems like changing script and style tags.
I have the following code that I am running on a website:
let details = await page.$$eval(
"#od-subtotals .a-row:not(.a-spacing-mini)",
nodes =>
nodes.map(async n => {
let heading = await n.$eval(".a-text-left span", nn => nn.innerText);
let amount = await n.$eval(".a-text-right span", nn => nn.innerText);
return { heading: heading, amount: amount };
})
);
The $$eval method works fine and if I were to run the map simply on the $$eval(sel, nodes => nodes.map(n => n.innerText), I receive an array.
Now, I am trying to separate the node even further. When I read the docs it says the following:
page.$$eval(selector, pageFunction[, ...args])
selector <string> A selector to query page for
pageFunction <function(Array<Element>)> Function to be evaluated in browser context
So, my thinking was that I loop through elements on the page on which I run the .$eval method which has the following characteristics:
elementHandle.$eval(selector, pageFunction[, ...args])
selector <string> A selector to query page for
pageFunction <function(Element)> Function to be evaluated in browser context
I do receive the aforementioned error:
Uncaught (in promise) TypeError: n.$eval is not a function
at __puppeteer_evaluation_script__:7
at Array.map (<anonymous>)
at VM163 __puppeteer_evaluation_script__:2
(anonymous) # __puppeteer_evaluation_script__:7
(anonymous) # __puppeteer_evaluation_script__:2
So, it seems like the Element returned from .$$eval is not actually an ElementHandle and as such, cannot be used with the $eval function. I was looking through the docs and there does not seem to be a way to get them converted either.
The code would need to be changed to:
let details = await page.$$eval(
"#od-subtotals .a-row:not(.a-spacing-mini)",
nodes =>
nodes.map(async n => {
let heading = n.querySelector(".a-text-left span").innerText;
let amount = n.querySelector(".a-text-right span").innerText;
return { heading: heading, amount: amount };
})
);
Presumably, there is also a way where we could use .$$ and then iterate over the Array of ElementHandle and then using $eval.
I am trying to use a client function to access the values in child elements on a page, not necessarily the ones in this example, but ones which are difficult to find with provided testcafe selectors.
In defining a page object model I want to be able to access Next, Back and Save buttons on multiple iFrame modals, they can have different locations on the DOM depending on modal view, and do not have ids (product is an old one).
They do however all follow a similar pattern, they would all be child elements of a span, and contain a Display text and title bearing their name, through chrome Dev Tools Console I can access them with something similar to the following
Array.from(document.querySelectorAll('span')).find(el => el.textContent === "Next")
However when I try to call this as a client function in testcafe I get an error, the following is an example based on my approach but against testcafe site, which gives the same error.
import { Selector } from 'testcafe';
import { ClientFunction } from 'testcafe';
fixture `Client Function`
.page `https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors/functional-style-selectors.html`;
const query = ClientFunction(() => Array.from(document.querySelectorAll('a')).find(el => el.textContent === "Filter DOM Nodes"));
test('Test query', async t => {
const queryResult = await query();
await t
.click(Selector(queryResult))
.wait(1500);
});
The error this gives is rather cryptic to me:
1) An error occurred in ClientFunction code:
ReferenceError: _from2 is not defined
Browser: Chrome 71.0.3578 / Mac OS X 10.13.6
6 | .page
`https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors/functional-style-selectors.html`;
7 |
8 |const query = ClientFunction(() =>
Array.from(document.querySelectorAll('a')).find(el => el.textContent
=== "Filter DOM Nodes"));
9 |
10 |test('Login and register user', async t => {
> 11 | const queryResult = await query();
12 | await t
13 | .click(Selector(queryResult))
14 | .wait(1500);
15 |});
16 |
at query (/Users/david/Documents/testcafe/demo/query.js:11:33)
at test (/Users/david/Documents/testcafe/demo/query.js:10:1)
at markeredfn
(/usr/local/lib/node_modules/testcafe/src/api/wrap-test-function.js:17:28)
at <anonymous>
(/usr/local/lib/node_modules/testcafe/src/api/wrap-test-function.js:7:5)
at fn
(/usr/local/lib/node_modules/testcafe/src/test-run/index.js:239:19)
at TestRun._executeTestFn
(/usr/local/lib/node_modules/testcafe/src/test-run/index.js:235:38)
at _executeTestFn
(/usr/local/lib/node_modules/testcafe/src/test-run/index.js:284:24)
1/1 failed (5s)
Does anyone know if this is a legitimate bug or an implementation error? Thanks - any pointers greatly welcome too!
You could rewrite the ClientFunction like this:
const query = ClientFunction(() => {
const results = [];
const allLinks = document.querySelectorAll('a');
allLinks.forEach(link => results.push(link));
const foundElement = results.find(el => el.textContent === "Filter DOM Nodes");
return foundElement;
});
But then you will receive another error:
ClientFunction cannot return DOM elements. Use Selector functions for this purpose.
The code inside a ClientFunction executes in the browser.
The code that calls this ClientFunction and get its result executes in a NodeJS process outside the browser.
What you are trying to achieve is called object Marshalling. You are trying to transfer a DOM object which lies in the browser process into another separate process. This can only be achieved through serialisation but DOM objects are not serialisable.
The return statement inside a ClientFunction must return a POJO (Plain Old Javascript Object).
You could achieve the same thing by using the Selector object like this:
const nextButton = Selector('span')
.find('a')
.withAttribute('title', 'NEXT')
.withText('NEXT');
await t.click(nextButton);
If you need special filtering other than attributes and textContent you could write the selector like this:
const nextButton = Selector('span')
.find('a')
.filter((link) => {
if (/* some condition processed on link element */) {
// this link element is the one to be clicked
return true;
}
return false;
});