Cypress: Get all elements containing a given string / regex - cypress

I'm trying to test a pagination bar with cypress.
I want to assert the number of buttons containing a number only in this bar, and ignore the other buttons (previous page, next page...)
The buttons are looking like this:
<button class="...">33</button>
I first tried this test:
cy.get('.pagination')
.find('button')
.contains(/\d+/)
.should('have.length.gte', 2)
But this gave me a warning about the fact that contains will only return one element, making the "length" test useless.
I also tried many combinations based on filter, the ":contains" jquery keyword, but none worked:
.filter(`:contains('/\d+\')`)
// >> finds nothing
.filter((elt) => { return elt.contains(rx) })
// >> throws 'elt.contains is not a function'
.filter((elt) => { return rx.test(elt.text()) })
// >> throws 'elt.text is not a function'
.filter(() => { return rx.test(Cypress.$(this).text()) })
// filter everything and return nothing, even the buttons containing the text '1'

.filter() with a callback has parameters (index, elt) => {} which means you can use it like this
cy.get('.pagination')
.find('button')
.filter((index, elt) => { return elt.innerText.match(/\d+/) })
.should('have.length.gte', 2)

nextAll() might work in this situation:
cy
.get('.pagination')
.find('button')
.contains(/\d+/)
.nextAll()
.should('have.length.gte', 2);
Another solution might be to distinguish the pagination buttons by something else, like a class, or some html attribute that is unique to them.

You can use an loop through the elements and match the element text and then increment a count variable and then later validate it, something like:
var count =0
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
count++
}
})
expect(count).to.be.greaterThan(2)
You can do other things as well like:
Assertions
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
expect(+$ele.text().trim()).to.be.a('number')
}
})
Perform Click
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
cy.wrap($ele).click()
}
})
Validate Inner text
cy.get('.pagination').find('button').each(($ele) => {
if(/\d+/.test($ele.text()){
cy.wrap($ele).should('have.text', 'sometext')
}
})

nextAll() fails if there's element wrapping the buttons, but you can count the wrappers.
cy.get('.pagination')
.find('button') // presume this is 'Prev' button
.parent()
.nextAll(':not(:contains(Next))')
.should('have.length.gte', 2)
or .nextUntil()
cy.get('.pagination')
.find('button') // presume this is 'Prev' button
.parent()
.nextUntil(':contains(Next)')
.should('have.length.gte', 2)
or .children()
cy.get('.pagination')
.children(':not(:contains(Prev)):not(:contains(Next))')
.should('have.length.gte', 2)
Overall, .filter() is better as it does not assume the HTML structure.

Related

Traverse through a list, perform actions and return back to the next item in list

Is there a way I can traverse through the list, perform click again and then return to the same page again for the next item in list.
cy.get('#collaborators').next().children().each((items) => {
// Here I have to write code to access list element
cy.log(cy.wrap(items))
}
Log gives me a structure like this and am not sure how to access it. Please help as I am new to cypress.
cy.get('#collaborators').next().children().each( (items,index)=>{
cy.wrap(items[index]).click()
}
)
Having a code written like this, is causing DOM element to be detached although it goes to the next page.
var itemsCount = cy.get('#collaborators').next().children().its('length')
Not sure if I can loop over to get to each of the elements this way.
cy.children() enables you to select child elements and use the selector to filter them. In your case, to get the a tag element, you can do something like:
cy.wrap(items).children('a');
I am also new to cypress, but I believe you can access the href attribute with the invoke() command:
invoke() - https://docs.cypress.io/api/commands/invoke
Try something like this:
cy.wrap(items).children('a').invoke('attr', 'href')
.then((url) => {
cy.visit(url);
});
If you evaluate the href attribute before starting the loop, you'll avoid the detached from DOM error.
Essentially, iterate over a string array not an element array.
cy.get('#collaborators').next()
.find('a') // find all <a> within the <ul>
.then($els => [...$els].map((a) => a.href)) // extract the href
.each(href => { // now iterate list of URL strings
cy.visit(href)
cy.pause() // substitute whatever test you need
cy.go('back')
})
Clicking the link
If you prefer to click the link, extract the last part of the href and use it to find the link element inside the loop
cy.get('#collaborators').next()
.find('a')
.then($els => [...$els].map((a) => a.href))
.each(href => {
const slug = href.split('/')[3]
cy.get('#collaborators').next().find(`a[href="/${slug}"]`).click()
const title = slug.replace('~', '')
cy.contains('h2', title)
cy.go('back')
})

Cypress: Async function using for loop giving same results

I'm new to cypress framework and trying to achieve the below functionality using cypress.
I have a page with table rows and a dropdown menu on page header. On selecting the option, the dropdown menu gets closed and the body content gets changed/loaded up according to the selected menu options.
Problem: Getting the same length for the table rows for all the menu options selected sequentially, although the table rows count is different for the options.
Here is my code:
it.only('Validate table Row changed length on menu option selection', {defaultCommandTimeout: 10000}, () => {
// opening the dropdown menu
page.openDropdownMenu();
// getting the dropdown options and calculating the length
cy.get('dropdownOptions').then($options => {
// calculating the length
const menuOptionCount = $options.length;
// closing the dropdown menu
page.closeDropdownMenu();
for (let i = 0; i < menuOptionCount; i++) {
// opening the dropdown menu
page.openDropdownMenu();
// clicking the menu option
$options[i].click();
// closing the dropdown menu
page.closeDropdownMenu();
cy.get("body").then($body => {
// always getting the same length for the table rows for all selected options
const rowsLength = $body.find('.table.rows').length;
cy.log('****************Rows length************', rowsLength);
});
}
});
});
Is there any way to write the asynchronous statement to synchronous like (await async in promises) without using any external utility in cypress. As in my previous assignment using Protractor the same thing could be handled using async await as below.
const elementCount = await element(
by.css('[title="Locked By"] .med-filter-header-button div')
).count();
After click() the app rewrites the table, but Cypress does not know that happens and gets the row count before the change occurs.
TLDR - You need to give Cypress more information test correctly. Generally, your test data should be known (not "discovered" by the test code).
Problem #1
You need some way to wait for the row change to finish. Either some text element changes (maybe the first row text), or by adding a .should() on the actual row count.
Something like
const expectedRowCount = [5, 4, 3, 2]
cy.get('dropdownOptions').each(($option, index) => {
page.openDropdownMenu()
$option.click()
page.closeDropdownMenu()
cy.get('.table.rows')
.should('have.length', expectedRowCount[index]) // this will retry until rowsLength changes
.then(rowsLength => {
cy.log('****************Rows length************', rowsLength)
})
})
Problem #2
If "the body content gets changed/loaded" means that the dropdown also gets rewritten with every click, then the loop will fail because $options gets refreshed each time.
You might use the expectedRowCount to loop instead
const expectedRowCount = [5, 4, 3, 2]
expectedRowCount.forEach((expectedCount, index) => {
page.openDropdownMenu()
cy.get('dropdownOptions').eq(index).click()
page.closeDropdownMenu()
cy.get('.table.rows')
.should('have.length', expectedCount) // retries until rowsLength changes
.then(rowsLength => {
cy.log('****************Rows length************', rowsLength)
})
})
The above strategies do not really give you the most solid test.
If you can, check some text that changes upon each iteration,
page.openDropdownMenu()
cy.get('dropdownOptions').then($options => {
let firstRowText = ''; // to control the loop, waiting for this to change
const menuOptionCount = $options.length;
page.closeDropdownMenu();
for (let i = 0; i < menuOptionCount; i++) {
page.openDropdownMenu();
cy.get('dropdownOptions').eq(i).click(); // fresh query each time through the loop
page.closeDropdownMenu();
cy.get('.table.rows').first().invoke('text')
.should('not.eq', firstRowText); // retry until text has changed
.then(newText => firstRowText = newText); // save for next loop
cy.get('.table.rows').then($rows => {
const rowsLength = $rows.length;
cy.log('****************Rows length************', rowsLength);
});
}
})
You can condense your code to something like this. Instead of using a for loop, use each which is a cypress inbuilt method for looping.
it.only(
'Validate table Row changed length on menu option selection',
{defaultCommandTimeout: 10000},
() => {
page.openDropdownMenu()
cy.get('dropdownOptions').each(($options, index) => {
cy.wrap($options).eq(index).click()
page.closeDropdownMenu()
cy.get('.table.rows')
.its('length')
.then((rowsLength) => {
cy.log('****************Rows length************', rowsLength)
})
page.openDropdownMenu()
})
page.closeDropdownMenu()
}
)

Cypress: How to scroll a dropdown to find item

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;
}
}

Iterate through elements

I have a page with a few Results panels, each panel has its own delete button.
I wrote a Cypress test to test the delete process, the test works as expected, the panel gets deleted:
cy.get('div[data-test="Results"]')
.first()
.within(() => {
cy.get('p[data-test="Contact ID"]').then($match => {
contactID = $match.html();
cy.get('button[data-test="Delete Contact"]')
.click()
.get('div[data-test="Delete Record Modal"]')
.should('be.visible')
.get('button[data-test="Confirm Deletion"]')
.click();
});
});
Next I'm trying to detect if the correct panel got deleted.
How can I iterate through all the <p />s of all panels and make sure none of them has a contactID equal to the one that was deleted?
I tried this:
cy.get('p[data-test="ContactID"]').then($match2 => {
expect($match2.text()).not.to.eq(contactID);
});
But in $match2 I get all contacts ids all together for example: 12345678 instead of 1234 and 5678
You can use each:
cy.get('p[data-test="ContactID"]').each(($match) => {
cy.wrap($match).invoke('text').should('not.eq', contactID)
})
invoke calls a function on the subject, in this case, .text()
the chained .should makes an assertion on that text
this will retry the assertion until it passes or times out (see retry-ability) due to the cy.wrap

How do you check the equality of the inner text of a element using cypress?

I have a div that has another div inside of it and I want to check the equality of the inner text of the div. I have figured out how to do it using the invoke('text') function, but i am wondering if that is the best way. So my question is: how do you check the equality of the inner text of a element using cypress?
it('the channel name should contain be Anakin Skywaler', () => {
//This works but can we be more specific with our selector
cy.get("[data-test-id='Skywalker,Anakin']").should('contain', 'Skywalker,Anakin');
})
it('the channel name should equal Skywalker,Anakin', () => {
cy.get("[data-test-id='Skywalker,Anakin']").find('.channel-name').invoke('text').then((text) => {
expect(text.trim()).equal('Skywalker,Anakin')
});
});
Please ignore the Star War Reference!
I think you can simplify this.
Assuming you have HTML that looks like this:
<div data-test-id="Skywalker,Anakin">
<div class=".channel-name">Skywalker,Anakin</div>
</div>
You can write your assert like this:
cy.get('[data-test-id="Skywalker,Anakin"]').should(
"have.text",
"Skywalker,Anakin"
);
This passed for me and if I modified the HTML to Skywalker,Anakin 1 it failed as you would expect. Cypress uses the have.text to look at what is rendered out so it will not worry about any markup and just see what the result is.
This did not work for trimming. you would need to add a callback to do the trimming.
cy.get('[data-test-id="Skywalker,Anakin"]').should(($div) => {
expect($div.text().trim()).equal("Skywalker,Anakin");
});
You can check if a string is contained somewhere inside the div:
cy.get("[data-test-id='Skywalker,Anakin']").contains('Skywalker,Anakin');
Or, if you need to make sure the div contains only the specified text and nothing else, you can tag on this extra assertion:
cy.get("[data-test-id='Skywalker,Anakin']").contains('Skywalker,Anakin').should((elem) => {
expect(elem.text()).to.equal('Skywalker,Anakin');
});
Explanation:
// Get the data
cy.get("[data-test-id='Skywalker,Anakin']")
// Get the child or children of the previous command that
// contain the text - this command fails if no child
// element is found containing the given text
.contains('Skywalker,Anakin');
// These two lines are explained above
cy.get("[data-test-id='Skywalker,Anakin']")
.contains('Skywalker,Anakin')
// Like a .then(), except the contents are retried until
// all contained assertions are successful or until the
// command times out
.should((elem) => {
// Expect the element's text to exactly equal the
// string, not just contain it
expect(elem.text()).to.equal('Skywalker,Anakin');
});
I think currently this is the best option, because it does not check for contains. I was hoping for a shorter piece of code to do this.
it('the channel name should equal Skywalker,Anakin', () => {
cy.get("[data-test-id='Skywalker,Anakin']").find('.channel-name').invoke('text').then((text) => {
expect(text.trim()).equal('Skywalker,Anakin')
});
});
Following is how you can check exact or partial match for a string in an element:
//matches exact text of result string
cy.get("[data-test-id='Skywalker,Anakin']").should('have.text', 'Skywalker,Anakin');
//matches partial text of result string
cy.get("[data-test-id='Skywalker,Anakin']")
.text()
.then(value => {
cy.log("Text value is :", value);
expect(value).to.include('Anakin');
});
where text() is defined in command.js file as following:
Cypress.Commands.add("text", { prevSubject: true }, (subject, options) => {
return subject.text();
});
Can't believe I didn't see one of the magical cypress .should matches. Also I use typescript cypress which gives great lookups/intellisense.
using should. However, these are exact text matches and may have a lot of spaces
cy.get("[data-test-id='Skywalker,Anakin']")
.should("have.text", "Skywalker,Anakin")
.should("have.attr", "data-test-id","Skywalker,Anakin'")
adding a new command would be better such as shouldHaveTrimmedText
I got it from https://github.com/cypress-io/cypress/issues/3887#issuecomment-522962482
but below is the setup to get it working also typescript with type
lookup
commands.ts
Cypress.Commands.add(
'shouldHaveTrimmedText',
{
prevSubject: true,
},
(subject, equalTo) => {
if (isNaN(equalTo)) {
expect(subject.text().trim().replace(/ +/g, ' ')).to.eq(equalTo);
} else {
expect(parseInt(subject.text())).to.eq(equalTo);
}
return subject;
},
);
cypress/support/index.d.ts
Cypress.Commands.add(
'shouldHaveTrimmedText',
{
prevSubject: true,
},
(subject, equalTo) => {
if (isNaN(equalTo)) {
// removes double spaces and ending spaces, does not remove special characters such as tabs or \n
expect(subject.text().trim().replace(/ +/g, ' ')).to.eq(equalTo);
} else {
expect(parseInt(subject.text())).to.eq(equalTo);
}
return subject;
},
);
tsconfig
{
"types": ["cypress","./cypress/support"]
// or "typeRoots": ... but not both
}
cy.get("[data-test-id='Skywalker,Anakin']")
.shouldHaveTrimmedText("Skywalker,Anakin")
Simple exact matching
cy.get('[data-test-id=Skywalker,Anakin]')
.invoke('text')
.should('equal', 'Skywalker,Anakin')

Resources