I wonder if there's a similar way as in Selenium to wait for text to appear for a particular element. I've tried something like this, but it doesn't seem to wait:
await page.waitForSelector('.count', {visible: true});
You can use waitForFunction. See https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforfunctionpagefunction-options-args
Including #elena's solution for completeness of the answer:
await page.waitForFunction('document.querySelector(".count").innerText.length == 7');
Apart from the method presented in the answer from nilobarp, there are two more ways to do this:
page.waitForSelector
Using the pseudo selector :empty it is possible to find elements that contain no child nodes or text. Combining this with the :not selector, we can use page.waitForSelector to query for a selector which is not empty:
await page.waitForSelector('.count:not(:empty)');
XPath expression
If you not only want to make sure that the element is not empty, but also want to check for the text it contains, you can use an XPath expression using page.waitForXPath:
await page.waitForXPath("//*[#class='count' and contains(., 'Expected text')]");
This line will only resolve after there is an element on the page which has the attribute class="count" and contains the text Expected text.
The best solution you can do using waitForFunction() (avoid weird function as string):
const selector = '.count';
await page.waitForFunction(
selector => document.querySelector(selector).value.length > 0,
{},
selector
);
Depends of the type of the text, replace value by innerText.
Check puppeteer API
page.waitFor()
You can also just simply use page.waitFor() to pass a function or CSS selector for which to wait.
Wait for Function
If the element is an input field, we can check that the .count element exists before checking that a value is present to avoid potential errors:
await page.waitFor(() => {
const count = document.querySelector('.count');
return count && count.value.length;
});
If the element is not an input field, we can check that the .count element exists before checking that innerText is present to avoid potential errors:
await page.waitFor(() => {
const count = document.querySelector('.count');
return count && count.innerText.length;
});
Wait for CSS Selector
If the element is an input field that contains a placeholder, and you want to check if a value currently exists, you can use :not(:placeholder-shown):
await page.waitFor('.count:not(:placeholder-shown)');
If the element is an input field that does not contain a placeholder, and you want to check if the value attribute contains a string, you can use :not([value=""]):
await page.waitFor('.count:not([value=""])');
If the element is not an input field that does not have any child element nodes, we can use :not(:empty) to wait for the element to contain text:
await page.waitFor('.count:not(:empty)');
page.waitForXPath()
Wait for XPath
Otherwise, you can use page.waitForXPath() to wait for an XPath expression to locate element(s) on the page.
The following XPath expressions will work even if there are additional classes present on the element other than count. In other words, it will work like .count, rather than [class="count"].
If the element is an input field, you can use the following expression to wait for the value attribute to contain a string:
await page.waitForXPath('//*[contains(concat(" ", normalize-space(#class), " "), " test ") and string-length(#value) > 0]')
If the element is not an input field, you can use the following expression to wait for the element to contain text:
await page.waitForXPath('//*[contains(concat(" ", normalize-space(#class), " "), " count ") and string-length(text()) > 0]');
await page.waitFor((name) => {
return document.querySelector('.top .name')?.textContent == name;
}, {timeout: 60000}, test_client2.name);
waitForXPath is simple and works well for finding an element with specific text.
const el = await page.waitForXPath('//*[contains(text(), "Text to check")]');
Related
The application has list of items in a table and items have [id=item[0].setupCost, id=item[1].setupCost, id=item[2].setupCost] etc.
There's a functionality to add items also, so the index keeps on increasing.
I want to get input field regardless of using magic numbers. For eg (cy.get('[id=item[some_regex].setupCost]')
The regex that applies is \[id="item\[\d+\].setupCost\].
Enclose the regex in forward slashes, not quotes.
cy.get(/\[id="item\[\d+\].setupCost\]/)
But don't use it
This syntax is undocumented - it works (in Cypress v9.5.0) but it only returns one result.
So if you want to count your items, this will fail
cy.get(/\[id="item\[\d+\].setupCost\]/)
.its('length')
.should('eq', 3) // ❌
Partial attribute selector
If you want to use partial attribute selectors, this is strongest as it includes .setupCost
Ref Find the element with id that starts with "local-" and ends with "-remote"
cy.get('[id^="item"][id$=".setupCost"]') // use "starts-with" and "ends-with" selectors
This succeeds with the count test
cy.get('[id^="item"][id$=".setupCost"]')
.its('length')
.should('eq', 3) // ✅
In case you need another answer:
You can use a regex if you add a jQuery extension.
It works because Cypress uses jQuery internally, and you can modify selector behavior by extending jQuery.
it('finds multiple ids by regex', () => {
const $ = Cypress.$
$.extend(
$.expr[':'], {
idRegex: function(a, i, m) {
const regex = new RegExp(m[3], 'i');
return regex.test(a.getAttribute('id'));
}
}
)
const regex = 'item\\[\\d+\\].setupCost'
cy.get(`input:idRegex(${regex})`)
.its('length')
.should('eq', 3) // ✅
})
Put the jQuery extension in /cypress/support/index.js to use it globally.
Note in the regex we have to double-escaped because we pass in a string not a regex.
Instead of Regex you can use ^ with your selector. This denotes the start of the text. So you can write:
cy.get('[id^="item"]')
Now if you want to access any particular element you can use eq like this:
cy.get('[id^="item"]').eq(0) //gets id=item[0].setupCost
cy.get('[id^="item"]').eq(1) //gets id=item[1].setupCost
If you want to loop over each element you can use each()
cy.get('[id^="item"]').each(($ele) => {
cy.wrap($ele)
})
You could create a custom Cypress command that would insert the index for you. Below, I'm assuming the id you're referencing is the ID of the element and is a standard CSS selector
Cypress.Commands.add('getItemById', (index) => {
return cy.get(`#item\\[${index}\\].setupCost`)
});
cy.getItemById(0)...
In my taiko test script I selected an input element with the proximity parameter.
Then I can write some text in the input element.
After that, I want to make an assertion, that the text was actually written into that element by checking the value. How do I do that?
Here is an excerpt of my test script:
await click($(`input`, below('someHeader')));
await write('abc');
The input field has no id.
How can I write a check, that the value is 'abc' ?
textBox is the selector that represents all text inputField in Taiko...
proximity selector can be used to fetch the inputfield to write and get its value like below
await write('abc',into(textBox(below('someHeader'))));
await textBox(below('someHeader')).value();
You can get the text stored by using any selectors. If there is no any ID then you can try using label name, textBox or you can try using xpath
const myText = await $("xpath_value").text();
assert.strictEqual(myText, 'abc')
I am trying to get the text from the following Xpath as a string:
//*[contains(text(), 'mission')]/following-sibling::text()[1]
I have tried
let elHandle = await page.$x("//*[contains(text(), 'mission')]/following-sibling::text()[1]")
which returns an ElementHandle<Element>[]. How can I navigate from here to get to the text string?
I am assuming your XPath is correct. So: page.$x returns an array (of matched elements: <Promise<Array<ElementHandle>>>) where you need the 1st element so you will need to add [0] after the whole element handle expression.
It can be combined with a page.evaluate to retrieve the innerText string.
const elHandleText = await page.evaluate(el => el.innerText, (await page.$x("//*[contains(text(), 'mission')]/following-sibling::text()[1]"))[0])
console.log(elHandleText)
Your question about if it can be done with CSS selectors: It is not possible, XPath's contains method is the solution if you need to find an element with specific text content.
I'm scraping from a website that has a lot of nested HTML elements, but what interests me are the abbr elements. In my case those abbr elements have data-utime attribute, so they are defined as <abbr data-utime="someValue">some other nested HTML</abbr>. So, what I want to do is that I want to get the data-utime attribute value of the last abbr element on the page.
I tried to do something like this:
const SELECTOR = 'abbr:last-child';
const result = await page.evaluate((selector) => {
return document.querySelector(selector);
}, SELECTOR);
console.log(result);
console.log(typeof(res));
console.log(result.getAttribute('data-utime'));
But the problem is that in the output that I get, result is just an empty object ({}), so typeof(res) returns object, and it of course doesn't have getAttribute function then. I believe also last-child selector is the proper way to get the last abbr element on the page. Any ideas how to achieve what I want?
evaluate is run in the page’s context; the result is serialized and returned. Use $$eval instead:
const SELECTOR = "abbr";
const result =
await page.$$eval(SELECTOR,
(elements) => elements[elements.length - 1].dataset.utime);
console.log(result);
You can also use evaluate and call document.querySelectorAll inside it, but I prefer to keep the selectors in my Puppeteer code so I can reuse them.
I want to add / setAttribute class for custom DOM nodes while writing some text in custom bbcode-like text editor. When the innerHTML of <item></item> is not empty I'm filtering through items array in order to find the value that matches. There can be unlimited amount of item nodes. (i.e 2, 5, 10)
So whenever i click on icon named item it shows in textarea as [item][/item] and in preview component as <item></item>. Once the item is written, lets say [item]id123[/item] I have in DOM <item>itemName123</item>.
Now, what I'm doing is manipulating the DOM outside React with:
const setAttributes = (el, item) =>{
el.dataset.img = item.img;
el.setAttribute('class', _.toLower(item.color))
};
const updateItems = () =>{
if(document.querySelectorAll('item')) {
document.querySelectorAll('item').forEach(el => items.find(item => {
if(_.toLower(el.innerHTML) === _.toLower(item.id))
setAttributes(el, item)
}));
}
}
The problem is, whenever I change the text, component re-renders so that removes attributes that have been set.
I tried to manipulate the text/string before it goes to dangerouslySetInnerHTML markup by splitting it, going through includes with map, filter all that godsent functions with some regex sauce from ES6 but it just smells bad.
It feels so hacky that i believe that there has to be something that I'm missing.
Edit: Forgot to add that I've also tried to put setAttributes() && updateItems() outside of class.
Edit#2: The way i'm changing from [item][/item] is via regexp text.replace(/\[item]/g, <item>).replace(/\[\/item]/g, </item>), so probably i could do something with regexp instead of setAtrribute on each re-render?And if so, i've been trying that via
new RegExp(/\[item]/+ _.toLower(item.name)+ /\[\/item]/, 'g');
and later on text.replace(<item class="${item.quality}">${_.toLower(item.name)}</item>)
but no success so far.
Thanks in advance for any tips and ideas!
Solved. Used RegExp + pattern.exec(text) and looping through text with while and replacing any matched occurances. Then used for(let..of) loop to compare the matched values and overwrite the value.