This is basically for a ReactSelect element (behaves like a Select2 element with multi-select enabled), on which I'm trying to select some values which are not already selected.
If an option is selected, then there'll be an element as given below in the DOM
<div class="select__multi-value__label">option1</div>
and hence that options won't be present in the list. So, any code to click() that "option" will fail.
Is there a way to check whether an element with some particular text is available in the DOM?
something like,
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
if(reactSelect.find('[class="select__multi-value__label"]').contains(option).length == 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]').contains(option).click();
}
})
})
This find().contains() part doesn't work.
How can I achieve this?
Thanks for any help.
Edit
Adding to the solution given below, can I get an exact match selector - say using a Regex?
let r = new RegExp("^" + option + "$");
...........
const selector = `div.select__multi-value__label:contains(${r})`;
This somehow doesn't work. Found a thread that recommends using filter(), but I don't know how to use it.
Is it possible?
You can do it by moving the contains() inside the selector,
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
const selector = `[class="select__multi-value__label"]:contains(${option})`
if(reactSelect.find(selector).length == 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]')
.contains(option)
.click();
}
})
})
The .find().contains() part in your code is using a different .contains() to to the Cypress .contains().
It's invoking the jQuery .contains() which has a different purpose
Description: Check to see if a DOM element is a descendant of another DOM element.
I suppose you could also select the options directly and iterate them
options = ['option1','option2','option3'];
cy.get('div.select__menu-list > div[role="option"]')
.each(option =>{
if (options.includes(option.text()) {
option.click();
}
})
Exact match
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
const matchedOptions = reactSelect
.find('[class="select__multi-value__label"]')
.filter((index, el) => el.innerText === option) // exact
if(matchedOptions.length === 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]')
.contains(option)
.click();
}
})
})
You should avoid conditionals in tests as it goes against best practice.
A solution in this case following good practices would be to mock the response that comes from the API for you to handle the request as you want, so you will know exactly when there will be specific text on the screen instead of being a random behavior and you won't have to do conditionals.
You can read more about mocks here: https://docs.cypress.io/api/commands/intercept#Stubbing-a-response
But I also advise you to read this Cypress documentation on testing with conditionals: https://docs.cypress.io/guides/core-concepts/conditional-testing#What-you-ll-learn
Related
I'd like to get an element on my page, save its text, and then later type that same text into a text input element as part of a single spec. type() only accepts strings. Is there an accepted workaround, or is this a case of not knowing the best practice?
cy.get(".navbar-text").then(($user) => {
const user = $user.text;
cy.get(".historySearch").type($user.text);
//.type(user) and .type($user.text) both cause errors on the previous line
});
});
jQuery for text extract is a function, use it with Cypress invoke command
cy.get('.navbar-text.)
.invoke('text')
.then(user => {
cy.get(".historySearch").type(user);
});
or directly like this
cy.get(".navbar-text")
.then($user => {
const user = $user.text()
cy.get(".historySearch").type(user)
});
You can also use aliases .as() to save the inner text value and use it later.
cy.get('.navbar-text').invoke('text').as('navbarText')
cy.get('#navbarText').then((text) => {
cy.get('.historySearch').type(text);
})
I need to click a dropdown list and scroll to find an item by text.
At the moment I know that the item is at the bottom of the list so I can do:
cy.get('.ng-dropdown-panel-items').scrollTo("bottom").contains(/test/i).click()
and this works, but if the item moves and is no longer at the bottom, this will break.
I tried scrollIntoView but with no luck:
cy.get('.ng-dropdown-panel-items').contains(/test/i).scrollIntoView().click()
and
cy.get('.ng-dropdown-panel-items').scrollIntoView().contains(/test/i).click()
Does anyone know how I can do this?
Update: the list of options is dynamically generated (not all options are in the DOM initially) so scrolling to the bottom is required to get all options. Once all options are available .contains() can be used to find the element.
The Angular ng-select in virtual mode is quite tricky to handle.
It's list is virtual, which means it only has some of the items in the DOM at one time, so you can't select them all and iterate over them.
You can recursively scan the options list and use .type({downarrow}) to move new options into the DOM (which is one way a user would interact with it).
it('selects an option in a virtual-scroll ng-select', () => {
cy.visit('https://ng-select.github.io/ng-select#/virtual-scroll')
cy.get('ng-select').click(); // open the dropdown panel
cy.get('.ng-option')
.should('have.length.gt', 1); // wait for the option list to populate
function searchForOption(searchFor, level = 0) {
if (level > 100) { // max options to scan
throw 'Exceeded recursion level' // only useful for 100's
} // not 1000's of options
return cy.get('ng-select input')
.then($input => {
const activeOptionId = $input.attr('aria-activedescendant') // highlighted option
const text = Cypress.$(`#${activeOptionId}`).text() // get it's text
if (!text.includes(searchFor)) { // not the one?
cy.wrap($input).type('{downarrow}') // move the list
return searchForOption(searchFor, level + 1) // try the next
}
return cy.wrap(Cypress.$(`#${activeOptionId}`))
})
}
searchForOption('ad et natus qui').click(); // select the matching option
cy.get('.ng-value')
.should('contain', 'ad et natus qui'); // confirm the value
})
Note that recursion can be hard on memory usage, and this code could be optimized a bit.
For most cases you would need cy.get().select like for example:
cy.get('.ng-dropdown-panel-items').select(/test/i)
You can use an each() to loop through the drop down elements and when you find the desired text, make an click().
cy.get('span.ng-option-label.ng-star-inserted').each(($ele) => {
if($ele.text() == 'desired text') {
cy.wrap($ele).click({force: true})
}
})
Try something like below recursive function:
function scrollUntilElementFound(scrollIndex) {
scrollIndex = scrollIndex+10;
if(!cy.get('.ng-dropdown-panel-items').contains(/test/i)){
cy.get('.ng-dropdown-panel-items').scrollTo(scrollIndex);
scrollUntilElementFound(scrollIndex);
} else {
//element found
return;
}
}
Please be patient - I am a beginner in programming. Tester for long time but programming is not my domain.
My test is:
from the backend I get some list with some element (e.g. 5 text strings)
I click some element on page which displayed those 5 elements (of course I don't know if listed elements are correct or not)
I need to check if list of elements displayed on ui is the list received from backend
Problem:
I cannot access the elements by Nightwatch api css selector, at least I could not manage (Angular app) to do it with Nightwatch
I found I could do it with .execute()
My code is (failing):
browser
.click(selector.HEADER.APPS_GRID, function () {
for (var app in appsList) {
let appShortName = appsList[app].shortName
let appLongName = appsList[app].longName
let appUrl = appsList[app].url
let appVisibility = appsList[app].visibility
browser.execute(function(app){
var appShortNameDisplayed = document.getElementsByClassName('logo-as-text')[app].innerText
var appLongNameDisplayed = document.getElementsByClassName('app-name')[app].innerText
return [appShortNameDisplayed, appLongNameDisplayed]
}, function(result){
console.log(result.value[0])
})
}
})
It fails in lines:
var appShortNameDisplayed = document.getElementsByClassName('logo-as-text')[app].innerText
var appLongNameDisplayed = document.getElementsByClassName('app-name')[app].innerText
unfortunately I have to make query with [app] - iterating by elements of object. If I skip [app].innerText I get some data like element-6066-11e4-a52e-4f735466cecf instead of text values displayed on page
I get error:
Error while running .executeScript() protocol action: TypeError: document.getElementsByClassName(...)[app] is undefined
Is it possible to pass the "app" param (counter) to the document query?
Or is it the way I have to make one query that will return as many data as necessary and then handle data returned in this block
function(result) {
console.log(result.value[0])
})
The fragment of html page is
<div _ngcontent-c8="" class="ep-app-icon mt-auto mb-auto text-center logo-as-text"> XXX </div>
... and I need to get this "XXX" text.
As your own comment suggests, there is an args argument to .execute that is an array. The array elements will be the arguments in the function passed to execute.
See https://nightwatchjs.org/api/commands/#execute
.executeAsync(function(){
var buttons=document.getElementsByTagName('button');
buttons[2].click();
return buttons;
},[],function(result){
console.log('done')
})
Try Async it works for sure
if I have a page containing:
<span data-testid="credit-balance">
10
</span>
In Cypress, how do I extract the value to a variable to use in tests?
Something along the lines of:
const creditBalance = cy.get('[data-testid="credit-balance"]').value();
Assigning return values with const, var, and let is considered an anti pattern while using Cypress.
However, when you find yourself wanting to do so, is best practice to accomplish this with closures.
it("uses closures to reference dom element", () => {
cy.get("[data-testid=credit-balance]").then(($span) => {
// $span is the object that the previous command yielded
const creditBalance = $span.text();
cy.log(creditBalance);
})
});
Another way to do this would be to use Aliases if you want to store and compare values or share values between tests using hooks.
it("aliasing the value from dom element", () => {
cy.get("[data-testid=credit-balance]").as("creditBalance")
cy.get("#creditBalance").should("contain", 10)
});
How you approach this really depends on the objective of your test. I recommend checking out more examples from the documentation: try Variables and Aliases , Best Practices, and FAQ
If you would like to retrieve the value and perform any assertions with it, a fast, efficient method would also be the use .invoke
it('Getting the value and performing an assertion', () =>{
cy.get('selector').invoke('val').should('eq',10)
})
Doc
The Cypress documentation has an example how to Compare text values of two elements
// will keep text from title element
let titleText
cy.get('.company-details')
.find('.title')
.then(($title) => {
// save text from the first element
titleText = $title.text(); //original uses normalizeText($title.text())
})
cy.get('.company-details')
.find('.identifier')
.should(($identifier) => {
// we can massage text before comparing
const idText = $identifier.text(); //original uses normalizeText($identifier.text())
// text from the title element should already be set
expect(idText, 'ID').to.equal(titleText)
})
If you have to get the value rather than text, use this. It worked for me.
<span data-testid="credit-balance" value='100'></span>
Like above
cy.get('[data-testid="credit-balance"]')
.invoke("val")
.then(($amount) => {
// $span is the object that the previous command yielded
cy.log($amount);
});
I want to have the actual idtab button aswell as a link/button within the tab able to change tabs via JavaScript.
Is this possible if so how?
Thanks
after looking through the examples again I have re-used the bulk of it and I have come up with the following
function switchTab(ActiveTab) {
var set = $('.idtabs').html();
$("a", set).removeClass("selected")
.filter("[href='" + ActiveTab + "']", set).addClass("selected");
$.each($("a", set), function (key, value) {
$($(value).attr("href")).hide();
});
$(ActiveTab).show();}
I have just stumbled through your post after a google search. In case anyone else arrives here through the same way, I'll let an advice.
Instead of...
$("a", set).removeClass("selected")
...and...
$.each($("a", set), function (key, value) {
...one should use:
$("yourMenu#IdOrHTMLTag a")
It will prevent the code from calling jQuery's .hide() and .removeClass at all links of the page, which would raise an error.
You can achieve what you want just triggering the link's click event:
function switchTab(ActiveTab) {
$("a[href'"+ActiveTab+"']").click();
}