How to .log() an element that contains element that contains a text in Cypress - cypress

Basically I have 4 rows
4 rows
and I want to .log() only one of them which contains a label that has a text 'Car1.2'
code
html
row code

Do you mean something like this, if you want to just print the text Car1.2
cy.contains('#label', 'Car1.2').then(($ele) => {
cy.log($ele.text())
})
If you to print the whole row where Car1.2 is printed, you first have to reach the parent element of the row and then using within scope the next commands within that specific row only and then print the row texts, something like this:
cy.contains('#label', 'Car1.2')
.parent()
.parent()
.parent()
.parent()
.within(() => {
cy.get('#label').each(($ele) => {
cy.log($ele.text())
})
})
To make sure all your commands automatically traverse shadow dom's, go to cypress.json(if cypress version < 10) or cypress.config.js(if cypress version >= 10) and write:
includeShadowDom: true

There's another way to use .contains(), you can specify the row class as the first parameter and it will return the row having the label text "Carl1.2" somewhere inside it.
Combining two selectors used here get('#chunker').find('.row') allows you to do this
cy.contains('#chunker .row', 'Carl1.2`) // simple to obtain the row
.then($row => {
cy.log($row)
})
Add includeShadowDom:true option to find the the label
cy.contains('#chunker .row', 'Carl1.2`, {includeShadowDom:true})
.then($row => {
cy.log($row)
})

You can move to the parent row by specifying .parents('.row')
See parents() command
Please note that .parents() travels multiple levels up the DOM tree as opposed to the .parent () command which travels a single level up the DOM tree.
Like this
it('finds labels', {includeShadowDom:true}, () => {
cy.contains('#chunker #label', 'Car1.2')
.parents('.row')
.then($row => {
cy.log($row.text())
})
})

Related

Cypress: Get JQuery value without needing `then` or `each`

I'm hoping someone can help, but I've posted this as a Cypress discussion as well, although it might just be my understanding that's wrong
I need to get the Cypress.Chainable<JQuery<HTMLElement>> of the cell of a table using the column header and another cell's value in the same row.
Here's a working example JQuery TS Fiddle: https://jsfiddle.net/6w1r7ha9/
My current implementation looks as follows:
static findCellByRowTextColumnHeaderText(
rowText: string,
columnName: string,
) {
const row = cy.get(`tr:contains(${rowText})`);
const column = cy.get(`th:contains(${columnName})`)
const columnIndex = ???
return row.find(`td:eq(${columnIndex})`)
}
This function is required because I want to write DRY code to find cells easily for content verification, clicking elements inside of it etc.
The only example I've seen is this https://stackoverflow.com/a/70686525/1321908, but the following doesn't work:
const columns = cy.get('th')
let columnIndex = -1
columns.each((el, index) => {
if (el.text().includes(columnName) {
columnIndex = index
}
cy.log('columnIndex', columnIndex) // Outputs 2 as expected
})
cy.log('finalColumnIndex', columnIndex) // Outputs -1
My current thinking is something like:
const columnIndex: number = column.then((el) => el.index())
This however returns a Chainable<number> How to turn it into number, I have no idea. I'm using this answer to guide my thinking in this regard.
Using .then() in a Cypress test is almost mandatory to avoid flaky tests.
To avoid problems with test code getting ahead of web page updating, Cypress uses Chainable to retry the DOM query until success, or time out.
But the Chainable interface isn't a promise, so you can't await it. You can only then() it.
It would be nice if you could substitute another keyword like unchain
const column = unchain cy.get(`th:contains(${columnName})`)
but unfortunately Javascript can't be extended with new keywords. You can only add methods like .then() onto objects like Chainable.
Having said that, there are code patterns that allow extracting a Chainable value and using it like a plain Javascript variable.
But they are limited to specific scenarios, for example assigning to a global in a before() and using it in an it().
If you give up the core feature of Cypress, the automatic retry feature, then it's just jQuery exactly as you have in the fiddle (but using Cypress.$() instead of $()).
But even Mikhail's thenify relys on the structure of the test when you add a small amount of asynchronicity
Example app
<foo>abc</foo>
<script>
setTimeout(() => {
const foo = document.querySelector('foo')
foo.innerText = 'def'
}, 1000)
</script>
Test
let a = cy.get("foo").thenify()
// expect(a.text()).to.eq('def') // fails
// cy.wrap(a.text()).should('eq', 'def') // fails
cy.wrap(a).should('have.text', 'def') // passes
let b = cy.get("foo") // no thenify
b.should('have.text', 'def') // passes
Based on your working example, you will need to get the headers first, map out the text, then find the index of the column (I've chosen 'Col B'). Afterwards you will find the row containing the other cell value, then get all the cells in row and use .eq() with the column index found earlier.
// get headers, map text, filter to Col B index
cy.get("th")
.then(($headers) => Cypress._.map($headers, "innerText"))
.then(cy.log)
.invoke("indexOf", "Col B")
.then((headerIndex) => {
// find row containing Val A
cy.contains("tbody tr", "Val A")
.find("td")
// get cell containing Val B
.eq(headerIndex)
.should("have.text", "Val B");
});
Here is the example.

Cypress number of elements in a table

Is there an easier way to count the number of elements in a table via Cypress?
I have a 3 column table and I want to count, how many times is "India" in my table.
I tried it this way:
cy.get('ag-grid-table').contains(country).its('length').as("countrylength");
cy.get('#countrylength').then((ccc) => {
cy.log(ccc)
})
but this gives me always 1, as it finds only first element.
Then I have this solution, that may probably work:
let count = 0;
cy.get('ag-grid-table div.ag-row').each(($el, index, $list) => {
if($el.text().includes(country))
{ count ++; }
}
but can't we just find that country with one line command using length()?
You could just move the contains() inside the selector, it gets around the restriction of only returning the first result.
You need to merge the country variable into the selector as well, either with a string template as below, or by concatenating strings (+ operator).
cy.get(`ag-grid-table div.ag-row:contains("${country}")`)
.its('length').as("countrylength");
cy.get('#countrylength').should('eq', 3)
You can use the filter command for this:
cy.get('ag-grid-table div.ag-row')
.filter(':contains("India")')
.should('have.length', 3)
In case you want to use the length of different tests in your project you can use Cypress.env(). As aliases as are removed after every test.
cy.get('ag-grid-table div.ag-row')
.filter(':contains("India")')
.its('length')
.then((len) => {
Cypress.env('length', len)
})
//To use it in other test
cy.get('selector').type(Cypress.env('length'))
cy.get('selector').should('have.length', Cypress.env('length'))
If you want to use the alias value later in the test, or in another test you could access it directly from the "Mocha context" by using a function callback
if('tests the ag-grid', function() { // now alias value is available on "this"
cy.get('ag-grid-table div.ag-row')
.invoke('text')
.then(text => text.match(/India/g).length) // number of occurrences in table
.as("countrylength");
cy.get('.flags').should('have.length', this.countrylength)
})

Breaking early when iterating table rows and cells

I have a table with 10 columns in each row, but I need to break the loop after performing operations with 2nd column in each row using cypress
cy.get(tablerows).find('td').each((ele,index) => {
HERE I NEED TO BREAK THE LOOP AFTER 2 COLUMNS FOR EACH ROW, PLEASE HELP ME.
})
If you break early using a single .each() you will only get the first row processed, because cy.get(tablerows).find('td') gives you one big stream of all the cells in all the rows.
Instead, use one .each() for rows and another for cells in the row
cy.get(tablerows).each($row => {
cy.wrap($row).find('td').each(($cell, index) => {
if (index > 1) {
return false
}
// test first two columns here
})
})
Or pre-filter the columns
cy.get(tablerows).each($row => {
cy.wrap($row).find('td')
.then($tds => [...$tds].slice(0,2))
.each(($cell, index) => {
// only first two columns passed here
})
})
You can exit cy.each() by returning false.
cy.get(tablerows).find('td').each((ele, index) => {
if (index > 1) {
return false
} else {
// code to execute in first two iterations
}
});
If you are only running synchronous code, you can omit the else.

Assert that a dynamic table is correctly ordered by date

Given a dynamically-loading table with a variable number of rows, how does one assert that the rows are correctly ordered by date?
This problem has two main challenges: (1) how does one compare dates within table rows using cypress; and (2) how does one handle dynamic loading in such a complex scenario?
So far, I have successfully managed to solve the first problem; however, I am unable to solve the second problem. My test works most of the time, but it will sometimes fail because the page hasn't finished loading before the assertions are hit. For example, the dates are out of order when the page is first loaded:
2023-12-23
2024-01-24
2022-02-25
2027-03-26
And then they get ordered following an XHR request:
2022-02-25
2023-12-23
2024-01-24
2027-03-26
Now, before you say anything: yes, I am already waiting for the XHR request to finish before I make any assertions. The problem is that there remains a small delay between when the request finishes, and when the actual DOM gets updated.
Normally this problem is solved automatically by Cypress. In Cypress, every call to .should() will automatically retry until the expected condition is found, or a timeout is reached. This fixes any problems related to dynamic loading.
However, .should() isn't the only way to assert something in Cypress. Alternatively, you can make direct assertions using Chai expressions, which is what Cypress uses under the hood when you call .should(). This is often required when making complex assertions such as the kind that I am making in this scenario.
Let's take a look at what I have so far:
cy.get('tbody tr').each(($row, $index, $rows) => { // foreach row in the table
if ($index > 0) { // (skipping the first row)
cy.wrap($row).within(() => { // within the current row...
cy.get('td').eq(7).then(($current_td) => { // ... get the eighth td (where the date is)
cy.wrap($rows[$index - 1]).within(() => { // within the previous row...
cy.get('td').eq(7).then(($previous_td) => { // ... get the eighth td
expect(dayjs($current_td.text().toString()).unix()) // assert that the date of the current td...
.gt(dayjs($previous_td.text().toString()).unix()) // ... is greater than the previous one.
})
})
})
})
}
})
Now, one option you have in Cypress is to replace .then() with .should(). Doing this allows the user to continue to benefit from the polling nature of .should() while also using multiple Chai expressions directly. Unfortunately, I can't seem to get this to work. Here's some of the attempts that I made:
cy.get('tbody tr').each(($row, $index, $rows) => {
if ($index > 0) {
cy.wrap($row).within(() => {
cy.get('td').eq(7).then(($current_td) => {
cy.wrap($rows[$index - 1]).within(() => {
cy.get('td').eq(7).should(($previous_td) => { // replacing with .should() here doesn't help, because it only attempts to retry on $previous_td, but we actually need to retry $current_td as well
expect(dayjs($current_td.text().toString()).unix())
.gt(dayjs($previous_td.text().toString()).unix())
})
})
})
})
}
})
cy.get('tbody tr').each(($row, $index, $rows) => {
if ($index > 0) {
cy.wrap($row).within(() => {
cy.get('td').eq(7).should(($current_td) => { // causes an infinite loop!
cy.wrap($rows[$index - 1]).within(() => {
cy.get('td').eq(7).then(($previous_td) => {
expect(dayjs($current_td.text().toString()).unix())
.gt(dayjs($previous_td.text().toString()).unix())
})
})
})
})
}
})
The only other solution I can think of is to hardcode my own polling. This is the sort of thing that I do all the time when writing tests in Selenium. However, my experience with Cypress leads me to believe that I shouldn't need to do this, ever. It's just a matter of wrangling Cypress to do what I expect it to do.
That said, I'm coming up empty handed. So, what do?
UPDATE
After learning from gleb's answer, I finally landed on this simple solution:
const dayjs = require('dayjs')
chai.use(require('chai-sorted'));
cy.get('tbody tr td:nth-of-type(8)').should($tds => {
const timestamps = Cypress._.map($tds, ($td) => dayjs($td.innerText).unix())
expect(timestamps).to.be.sorted()
})
I now feel that a core part of my problem was not understanding jQuery well enough to write a single selection statement. Furthermore, I wasn't familiar with lodash map or chai-sorted.
You need to use a single cy.get(...).should(...) callback where the callback grabs all date strings, converts into timestamps, then checks if the timestamps are sorted. Then Cypress retries the cy.get command - until the table is sorted and the should callback passes. Here is a sample code, see the full dynamic example at https://glebbahmutov.com/cypress-examples/recipes/sorted-list.html
// assuming you want to sort by the second column
cy.get('table tbody td + td').should($cells => {
const timestamps = Cypress._.map($cells, ($cell) => $cell.innerText)
.map((str) => new Date(str))
.map((d) => d.getTime())
// check if the timestamps are sorted
const sorted = Cypress._.sortBy(timestamps)
expect(timestamps, 'sorted timestamps').to.deep.equal(sorted)
})

How to query a child element, present inside a list item

I want to automate the below scenario using Xamarin.UITest:
The List has several rows and each row has different elements inside it.
I want to iterate through the list and find elements inside each row using Xamarin.
IList CellList = app.Query(thisObject => thisObject.Id("MyList_ID"));
foreach(var element in CellList)
{
//Here I want to go inside each row and find an element inside each row. So what needs to be changed in the below line of code?
bool isNTextPresent = (app.Query(thisObject => thisObject.Id("NText_ID")).Length) == 1 ? true : false;
Assert.AreEqual(isNTextPresent , true);
}

Resources