cypress .each() .wrap() .invoke('text') takes long time - performance

I am trying to check the items shown in my table for some assertions. My way is to put all of the items in an array and then iterate over that array.
My problem: All assertions already passed but the cypress runner still takes a lot of time to finish the cy.wrap(.invoke(text)) jobs.
Since this is a very core command of my cypress tests it would be great to have a more efficient function.
My command:
cy.get('table tbody').within(() => {
cy.get('tr').each((tr) => {
cy.wrap(tr.children().eq(index)).invoke('text').then((text) => {
text = text.trim();
arrayWithValuesOfTheList.push(text);
});
})
.then(() => {
//in here all the (quickly passing) assertions are...
});
});
Thanks for any help in advance. I appreciate you all!

You can avoid wrapping the value, will give some increase in speed but it's hard to say what is the slowest part.
const arrayWithValuesOfTheList = []
cy.get('table tbody tr')
.each($tr => {
arrayWithValuesOfTheList.push($tr.children().eq(index).text())
})
.then(() => {
//in here all the (quickly passing) assertions are...
})
})

You can do something like this. It gets the tr values one by one and matches in against a regex pattern.
cy.get('table tbody tr').each(($ele) => {
cy.wrap($ele.text().trim())
.should('match', /myregexp/)
.and('not.include', 'some text')
})

If you want to assert on individual cells, using .each($cell => {...}) is fine but if you want whole-column assertions (e.g sorted, unique-values) it gets difficult with .each().
To build something adaptable for various tests, take a look at the pattern here Sorting the table.
The idea is to create helper functions using .map() to selected table rows and columns.
const { _ } = Cypress
// helpers, reusable
const getColumn = (colIndex) => {
return (rows$) => {
const children$ = _.map(rows$, 'children')
return _.map(children$, `[${colIndex}]`)
}
}
const toStrings = (cells$) => _.map(cells$, 'textContent')
const toNumbers = (texts) => _.map(text, Number)
cy.get('table tbody tr') // rows of table
.then(getColumn(1)) // extract 2nd column
.then(toStrings) // get the text value
.then(toNumbers) // convert if assertions require numeric values
// whole-column assertion example
.should(values => {
const sorted = _.sortBy(values)
expect(values, 'cells are sorted 📈').to.deep.equal(sorted)
})
// individual value assertion
.should(values => {
values.forEach(value => {
expect(value).to.be.lt(100)
})
})
Addressing performance issue
If performance is poor, you can reduce the number of process steps at the Cypress command level by using jQuery-only commands.
This will avoid adding commands to the Cypress queue which is likely to be the slowest part
const arrayWithValuesOfTheList = []
cy.get('table tbody tr td:nth-child(2)') // 2nd col of each row
.then($tds => { // only jQuery methods inside
$tds.text((index, cellText) => {
arrayWithValuesOfTheList.push(cellText.trim())
})
})
.then(() => {
//in here all the (quickly passing) assertions are...
})
})

Related

Cypress multiple arrow function parameters?

So I'm just starting to learn Typescript with Cypress and I noticed this in a page I was reading about here: https://medium.com/#NicholasBoll/cypress-io-scaling-e2e-testing-with-custom-commands-6b72b902aab
export const updateTodo = (name: string) => ($todo: JQuery) => {
cy.wrap($todo).within(() => {
cy.get('label').dblclick()
cy.get('.edit').clear().type(`${name}{enter}`)
})
So I'm still new to typescript and I understand I think that the Jquery element is the subject parameter although I'm still a little bit unsure at what would represent a "Jquery" element/parameter in Cypress.
However what confused me is the fact that there are two fat arrow parameter sections... I've not seen that before? What exactly does that mean? Especially when further down it's called as such (With just a string):
it('should update a todo', () => {
createTodo('Learn Cypress Command API')
.then(updateTodo('Learn Cypress composition'))
.then(getTodoName)
.should('equal', 'Learn Cypress composition')
})
Can anyone explain what's going on here?
Two fat arrows just means the function is returning another function.
So
export const updateTodo = (name: string) => ($todo: JQuery) => {
cy.wrap($todo).within(() => {
cy.get('label').dblclick()
cy.get('.edit').clear().type(`${name}{enter}`)
})
is equivalent to
export const updateTodo = (name: string) => { // outer function
return ($todo: JQuery) => { // inner function
cy.wrap($todo).within(() => {
cy.get('label').dblclick()
cy.get('.edit').clear().type(`${name}{enter}`)
})
Where you use the double-fat-arrow function is another shorthand,
This
.then(updateTodo('Learn Cypress composition'))
is shorthand for this
.then((name: string) => updateTodo(name)('Learn Cypress composition'))
where the double brackets updateTodo()() calls the outer function then the inner function.
Or even longer syntax:
.then((name: string) => {
const innerFn = updateTodo(name);
return innerFn('Learn Cypress composition');
})

Return value from nested wrap

I am starting out with cypress and I have a doubt about returning a value form a custom command.
I have multiple tables across my application, and in my tables I can click a row that will open a modal with more deailed information. So I want to build a command to extract the values of a specific row, so I can store them and then compare with the modal values.
I'm also trying to do this command in a way to reuse across the different tables. However I am having issues with my return value. This is my current command:
Cypress.Commands.add(
'getRowInformation',
(rowsSelector, compareValue, mainProperty, nestedSelector) => {
let rowNumber = -1
const propertiesObject = {}
/**
* get all the field in the all the rows that might contain the compareValue
*/
cy.get(`[data-testid="${mainProperty}"]`).then($elements => {
cy.wrap($elements)
.each(($elementField, index) => {
/**
* Find first match and get the row index
*/
if (rowNumber === -1 && $elementField.text() === compareValue) {
rowNumber = index + 1
}
})
.then(() => {
/**
* Access needed row
*/
rowsSelector()
.eq(rowNumber)
.within(() => {
cy.get(nestedSelector).then($property => {
cy.wrap($property)
.each($prop => {
Object.assign(propertiesObject, { [$prop.attr('data-testid')]: $prop.text() })
})
.then(() => {
/**
* Return key value map, where key in data-testid
* and value is the element's text
*/
return cy.wrap(propertiesObject)
})
})
})
})
})
},
)
And I am calling this command in my it() as:
cy.getRowInformation(myCustomSelector, 'Compare value', 'testid', 'span').then(properties => {
console.log('properties', properties)
expect(true).to.be.true
})
My custom selector:
myCustomSelector: () => cy.get('[data-testid="row"]'),
My problem is that what gets to my .then in my it() is the rowsSelector().eq(rowNumber) and what I needed is the created propertiesObject. From the docs I could not get an example as nested as this, so do you guys think this is doable?
Docs say .within() yields the same subject it was given from the previous command.
So it seems you have to move your cy.wrap(propertiesObject) out of the within callback and put it in the outer then
You can also sub in .find() which is syntactically equivalent to .within()
rowsSelector()
.eq(rowNumber)
.find(nestedSelector).each($prop => {
Object.assign(propertiesObject, { [$prop.attr('data-testid')]: $prop.text() })
})
.then(() => cy.wrap(propertiesObject))
You might also want to review this Cypress, get index of th element to use it later for it's td element for the section that finds the rowNumber.
I haven't tried it, but maybe
cy.contains(`[data-testid="${mainProperty}"]`, compareValue) // only 1st match returned
.invoke('index')
.then((rowNumber) => {
rowsSelector()
.eq(rowNumber)
.find(nestedSelector)
.each($prop => {
Object.assign(propertiesObject, { [$prop.attr('data-testid')]: $prop.text() })
})
.then(() => cy.wrap(propertiesObject))
})

Cypress: Dynamic Tests: Validate rows in table

The goal is to iterate over a table and create a test dynamically for each row to validate its status. Table example:
Aware of Cypress Examples: Dynamic tests, however, provided examples addresses iterating over a static list of base types (string, number) and not gathered children of JQuery<HTMLElement>.
Rows are grouped by the first column labeled with GUID labels. The code below collects rows label contains a specific GUID. The added pseudo injection of test it() does not work nor do I expect it would, but this is what I am trying to accomplish testing each row's status with specific GUID:
it('TEST Iterate rows matching GUID', () => {
cy.fixture('esign').then($esignStore => {
expect($esignStore).to.have.property('requestUUID').to.be.a('string').not.empty;
cy.get('table[data-qa="act_ops_log_table"]').within(() => {
cy.log('Ops Logs table found');
cy.get('tbody[data-qa="act_ops_log_table_body"]').then(() => {
cy.get('tr');
cy.get('td');
cy.get('td:nth-child(1)').each(($tableCell, $index, $list) => {
const textEnvelope = $tableCell.text();
if (textEnvelope.includes($esignStore.requestUUID)) {
// pseudo-example of dynamically added test: start
// it(`TEST ${$index}`, () => {
cy.get('td:nth-child(3)')
.eq($index)
.then($field => {
const textStatus = $field.text();
expect(textStatus, 'succeeded');
});
// });
// pseudo-example of dynamically added test: end
}
});
});
});
});
});
Approaches appreciated. Thank you
You cannot create tests dynamically.
Cypress parses the spec file to find out what tests are present before the Cypress commands even start to run.
The best you can do is add extra logging to mark each tested row.
cy.get('tbody tr', {log:false}).each(($tr, index) => {
// Log beginning of row test
cy.then(() => Cypress.log({
displayName: 'Row test',
message: `TEST ${index}`,
}))
cy.wrap($tr, {log:false}).find('td:nth-child(1)', {log:false}).then($firstCol => {
const textEnvelope = $firstCol.text();
if (textEnvelope.includes(esignStore.requestUUID)) {
cy.wrap($firstCol, {log:false}).siblings({log:false}).eq(1, {log:false})
.should('have.text', 'succeed')
} else {
cy.log(`Row ${index} - not required requestUUID`)
}
})
})
Or pre-filtering the required rows so that only those matching requestUUID are tested
cy.get('tbody tr', {log:false}).each(($tr, index) => {
.filter(`:has(td:nth-child(1):contains(${esignStore.requestUUID}))`, {log:false})
.each(($tr, index) => {
// Log beginning of row test
cy.then(() => Cypress.log({
displayName: 'Row test',
message: `TEST ${index}`,
}))
cy.wrap($tr, {log:false})
.find('td:nth-child(3)', {log:false})
.should('have.text', 'succeed')
})

Storing element text in an array and accessing it later

I am trying to get element text inside a for loop for multiple elements and wanted to store it in an array in order to use it later. below is the code I am using. I needed to access the array for the later use so that i compare this array with another array. Please let me know how to achieve this in cypress.
it('My test', () => {
let arrayOfElementText = [];
cy.get('#divEl').each(($el) => {
cy.wrap($el).click();
cy.get('#input').invoke('val')
.then(val => {
arrayOfElementText.push(val);
console.log(arrayOfElementText);//Able to access
});
console.log(arrayOfElementText); **//Not able to access**
})
let anotherArray = [];
cy.get('#divEl1').each(($el) => {
cy.get('#input1').invoke('val')
.then(val => {
anotherArray.push(val);
console.log(anotherArray);//Able to access
});
console.log(anotherArray);**//Not able to access**
})
// code to compare two arrays
//both arrays are not accessible here
});
You need to use .then() to access the values, as they are derived from asynchronous commands.
let arrayOfElementText = [];
cy.get('#divEl').each(($el) => {
cy.wrap($el).click();
cy.get('#input').invoke('val')
.then(val => {
arrayOfElementText.push(val);
})
.then(() => {
console.log(arrayOfElementText); // array is available at every step of .each()
})
}).then(() => {
console.log(arrayOfElementText); // full array is available here
cy.wrap(arrayOfElementText).as('myArray1') // alias it for later
})
/*
Next array, same as above
*/
cy.get('#myArray1').then(myArray1 => {
cy.get('#myArray2').then(myArray2 => {
// compare
})
})
Note cy.get('#input') will always get the same input

Cypress how to get a text from a div and store in a variable for later

I am new to Cypress and some of the things that I expect to work have really weird issues.
For example, I am trying to get the value of a column in a table and use the value in a search input. I have done it like this:
it('should filter', () => {
let id = 0;
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
id = $div1.text();
expect(id).not.to.eq(0);
});
//expect(id).not.to.eq(0);
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
expect(id).to.eq($div1.text());
});
});
But when I run this, I get an error stating that [data-cy=data-table-row] has a length of 25 not 1.
It turns out that the id variable I am using is not accessible outside the should method. I assume that's because it is a promise.
If I try to do this:
it('should filter', () => {
let id = 0;
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
id = $div1.text();
expect(id).not.to.eq(0);
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
expect(id).to.eq($div1.text());
});
});
});
The test goes mental and tries to get the [data-cy=table-filters-search] over and over and over again.
I am not sure why.
Is there an easier way to grab the innerText of a div and store it to compare later?
As someone gave a response, I tried this:
it('should filter', () => {
let variables = {};
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').then(($div1) => {
variables.id = $div1.text();
expect(variables.id).not.to.be.undefined;
});
console.log(variables);
expect(variables.id).not.to.be.undefined;
cy.get('[data-cy=table-filters-search]').find('input').type(variables.id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').then(($div1) => {
expect(variables.id).to.eq($div1.text());
});
});
But the test fails on the second expect(variables.id).not.to.be.undefined;
Closure problem
The problem with the first example is that the test runs in two phases. The first phase sets up the commands in the queue, and the second runs them.
During the first phase, .type(id) "captures" the current value of id (which is "0") and in the second phase that's the value that gets used.
You can fix it in a couple of ways, with an alias or moving the type(id) inside the callback, as per your second example.
This gets around the closure problem by deferring cy.get(...).find('input').type(id) until the id has actually changed.
Retry problem
The problem with the second example is that should() with an expect in the callback will retry until it succeeds or times out.
Something in the callback is continuously failing (or an error is thrown) causing a continuous retry. It should time out, not sure why that doesn't happen.
You can separate the parts of the callback into two sections, and use a .then() which does not attempt to retry.
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should(($div1) => {
id = +$div1.text(); // Note $div1.text() is always a string
// so convert with "+" to compare numbers
expect(id).not.to.eq(0) // otherwise this always succeeds
})
.then(($div1) => { // use a then which does not retry
id = $div1.text();
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should(($div1) => {
expect(id).to.eq($div1.text())
});
})
Or
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.invoke('text') // find it's text
.should('not.eq', '0') // passes on the id
.then(id => {
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should($div1 => expect(id).to.eq($div1.text()) );
})
If you want to use it within a single case - Allias is the cypress way:
cy.wrap('value').as('variable') //setting the variable
cy.get('#variable') // with this you can call it at any time in the test after aliasing
.then(variable =>{
//here you can use it
})
For a variable to use on multiple cases I recommend using an object as a Reference
let variables = {} //This would need to be declared on the scope you wish to use it in
cy.get('element')
.then($el => {
variables.elText = $el.text() //Assigning the value
})
cy.log(variables.elText) //You can call it later like this

Resources