Breaking early when iterating table rows and cells - cypress

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.

Related

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

How to .log() an element that contains element that contains a text in 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())
})
})

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

Observable make request multiple times and collect response together

In database I have 19 users.
In my API, I can get only 5 results in one call.
If I want to get all them, I need to do request 4 times, each time to get 5 users. With start query I will change from which position I want new users to get.
I'm trying to do it in RxJS together with redux-observable.
I have some idea, but maybe my approach is imperative, and RxJS is opposite ideology.
// get users from API and `pipe` them helps me to see actual data and to count length of array
function getUsers(position = 0) {
return ajax.getJSON(`${API}/users?_start=${position}&_limit=5`).
pipe(map(({data}) => ({responseLength: data.length, data})))
}
// here when I got response if array.lenght is equal to 5, I know that I need to do fetch of data again.
// Problem is encountered here: if I do recursion after doing I will get only last result, not both of them,
// if I put my previous result into array, and then recursion result again push in array it become too complicated after
// in userFetchEpic to manipulate with this data
function count(data) {
return data.pipe(
map(item => {
if (item.responseLength === 5) {
count(getUsers(5));
}
return {type: "TEST" , item}
})
)
}
function userFetchEpic(action$) {
return action$
.pipe(
ofType(USER_FETCH),
mergeMap(() => {
return count(getUsers()).pipe(
map(i => i)
)
})
);
}
My code is here just to show what was my way of thinking.
Main problem is in recursion how to save all values together, if I save values in array.
Then I need to loop through array of observables and that sounds complicated in my head. :)
Probably this problem have much easier and better solution.
19 Users with 4 concurrent calls
I've re-arranged your get-users function to generate 4 Ajax calls, run them all concurrently, then flatten the result into one array. This should get you all 19 users in a single array.
function getUsers() {
return forkJoin(
[0,5,10,15].map(position =>
ajax.getJSON(`${API}/users?_start=${position}&_limit=5`)
)
).pipe(
map(resArray => resArray.flatMap(res => res.data))
)
}
function userFetchEpic(action$) {
return action$.pipe(
ofType(USER_FETCH),
mergeMap(_ => getUsers())
);
}
Generalize: Recursively get users
This, again, will return all 19 users, but this time you don't need to know that you have 19 users ahead of time. On the other hand, this makes all its calls in sequence, so I would expect it to be slower.
You'll notice this one is done recursively. You are creating a call stack this way, but so long as you don't have millions of users, it shouldn't be a problem.
function getUsers(position = 0) {
return ajax.getJSON(`${API}/users?_start=${position}&_limit=5`).pipe(
switchMap(({data}) =>
data.length < 5 ?
of(data) :
getUsers(position + 5).pipe(
map(recursiveData => data.concat(recursiveData))
)
})
);
}

Add sum row at end DataTable dc.js/reactJS

I have been given task at work place to make sum row and since am still in process of learning dc.js/d3.js and quite stuck in progress to solve this. How can i add sum row at end of table?
https://codesandbox.io/s/dark-shape-g7o2b?file=/src/MyComponent.js
At work they are sending group as dimension, but don't know how to make it work
A previous answer described how to add a row using dc.numberDisplay and a <tfoot> row:
How to calculate the total sales and make the displayed totals dynamic?
However, if you can't modify the HTML, you can also display a total row by computing it using a fake group.
The idea is to create an object which supports .all() and .top(), the methods the data table will use to pull data. When it returns the data, it will add another entry with the totals:
return {
all: () => {
let all = orig_group.all();
const total = all.reduce(
(p, v) => {
range(ship_size_id_start, ship_size_id_end).forEach(
i => (p.value[i] = (p.value[i] || 0) + (v.value[i] || 0))
);
p.value.sum += v.value.sum;
return p;
},
{ key: "Total", value: { sum: 0 } }
);
all = all.concat([total]);
return all;
},
top: function(N) {
return this.all().slice(0, N);
}
};
This grabs the original data using orig_group.all(), then uses Array.reduce to find the sums. The Array.reduce function loops over all the ship size ids and sums the columns, and also sums the sums for a grand total.
I had to declare
const ship_size_id_start = 1,
ship_size_id_end = 8;
in order to know what to loop over.
I also had to add the translation
Total: "Total"
for the title to show up in the left column.
Fork of your code sandbox.
bolding the totals line
You can still bold the last line without editing the DataTable component; however you will need a unique selector (like a div with an #id) in order to do this safely.
So the following CSS works
table.data-table.dc-chart tbody tr:last-child {
font-weight: bold;
}
but it will style all dc data tables on the page, so be careful. I added it to the styles in my fork.

Resources