Cypress - how to find by text content? - javascript

In Cypress, I want to select a button from a group of buttons based on its text-content. How can I do it? Here is my approach:
export const getCustomerButton = () => getNavigationSidenav()
.find('mat-expansion-panel-header')
.each(($el, index, $list) => {
const text = $el.find('.mat-content > mat-panel-title').text();
if (text === 'Customer') {
return $el;
}
return null;
});
The problem I have now is that I have to filter out the nulls from the element array. Is there a less complicated way?

This code will yield the DOM element with YOUR_BUTTON_CLASS which contains text 'Customer'. Is that what you're looking for?
cy.get('.YOUR_BUTTON_CLASS').contains('Customer');
Here the documentation for .contains cypress command.

Or maybe an even slicker solution is to use this:
cy.contains('YOUR_BUTTON_CLASS', 'Customer');
This can be done since contains() can hold 2 arguments. And if it gets two arguments the first one is always the element and the second the text.

Another option that's not mentioned in the previous answers here.
Use testing-library/cypress-testing-library
After the installation, just import it in cypress' commands.js:
import '#testing-library/cypress/add-commands'
And in your tests
cy.findAllByText("Jackie Chan").click();
cy.findByText("Button Text").should("exist");
cy.findByText("Non-existing Button Text").should("not.exist");
cy.findByLabelText("Label text", { timeout: 7000 }).should("exist");
cy.get("form").within(() => {
cy.findByText("Button Text").should("exist");
});
cy.get("form").then((subject) => {
cy.findByText("Button Text", { container: subject }).should("exist");
});
This is pretty straightforward and easy to use. We use this in our production site along with react testing library. Highly recommend :)

The accepted answer "can" work. However: if the element is not visible on the first try, the element cannot be found in subsequent retries.
See: https://github.com/cypress-io/cypress/issues/3745
Cypress uses "Sizzle" as selector library - so this:
cy.get('button:contains("FooBar")')
would work in retries.

There are multiple ways to do that
Syntaxes:
cy.contains(content)
cy.contains(content, options)
cy.contains(selector, content)
cy.contains(selector, content, options)
Examples:
cy.contains('button', 'Customer')
cy.contains('.buttonClass', 'Customer')
cy.get('button:contains("Customer")')
cy.contains('Customer')

The simplest way to do it would be :
cy.get('Button_Class').contains('Button_Text')
In your case, the solution would be :
cy.get('.mat-content > mat-panel-title').contains('Customer')
There is a documentation for this here.

Related

Cypress: Lodash: Cypress._.sample flaky

I have written this method but it is very flaky.
Cypress.Commands.add("selectRandomDropdownOption", function () {
cy.get(".ng-option")
.as("dropdownOptions")
.should("be.visible")
.then((options) => {
Cypress._.sample(options).click({ force: true });
cy.get("#dropdownOptions").should("not.exist");
});
});
every second test fails because cypress says Cypress._.sample(...).click is not a function which makes no sense because sometimes the method works. Can you tell me why this is? Is it because of my code or lodash itself?
One explanation might be that options is sometimes empty when the .click() is attempted, due to loading latency.
Try adding a length check
cy.get(".ng-option")
.should("be.visible")
.should('have.length','gt', 0)
.then((options) => {
Cypress._.sample(options).click();
})
cy.get(".ng-option").should("not.exist")
Converting to an array
The problem is options is an object, not an array.
If you console.log(options) you'll see it has a couple of extra properties prevObject and selector which are sometimes selected by the randon function.
jQuery.fn.init(2) [option, option, prevObject: jQuery.fn.init(1), selector: '.ng-option']
But these properties do not support the .click() function, and are not what you want to select anyway.
To fix, first convert the options object to an array
cy.get(".ng-option")
.should("be.visible")
.should('have.length','gt', 0)
.then((options) => {
const optionsArray = [...options]
Cypress._.sample(optionsArray).click();
})
cy.get(".ng-option").should("not.exist")

react-select-event doesn't trigger the onChange function to set the value during testing

It's been a couple of days that I'm struggling with a test I'm trying to write and thought this would be a good place to ask for some tips. I'm trying to fill a form that uses react-select dropdowns, and these forms are inside a map that renders 4 of them. However when trying to run the code below on the selectors of each form in a loop, after the first iteration which runs fine, the input element found in the form to perform the onChange doesn't have the onChange handler and doesn't trigger the change function to select the option I need. The package I am using for the selection is react-select-event as per the documentation here. I am still a beginner with react-testing-library and testing in general so it's quite probable that I missed something. Any tips or ideas are welcome! Thank you 🙂
const selectOptionAndAssertValue = async (indicator, field, element, option, inputForm) => {
await selectEvent.select(element, option.label, {
container: document.body,
});
await waitFor(
async () => {
expect(inputForm).toHaveFormValues({
[${indicator.short_alias}-${field}]: option.value,
});
},
{
onTimeout: error => console.log(error),
}
);
};
Note: to find the inputForm, I am using findByLabelText and it does find the element.
EDIT: Issue replicated in this repo https://github.com/ildaballiu/select-testing-demo
The issue was solved by changing the container to
container: () => document.body.querySelector('[class$=-menu]')

Validating the state of the slide toggle using Cypress

I'm just new to Cypress and I want to validate if the slide toggle button is ON or OFF.
I have this piece of code that checks whether it is ON or OFF but I have no idea how to put it in an If-Else condition.
cy.get('#slide-toggle-1')
.find('input')
.filter('#slide-toggle-1')
.should('have.attr', 'aria-checked', 'true')
//What I want to do
If(<the code above is true>) {
cy.get('#dropdown').select('value1')
}
else {
cy.get('#button').click()
}
All comments and suggestions are well appreciated. Thank you.
You can use a jQuery OR selector
cy.get('#slide-toggle-1[aria-checked="true"], #button') // "," in the selector means OR
.eq(0) // if both present take the first
.then(toggleOrButton => {
if (toggleOrButton.attr('id') === 'slide-toggle-1') {
cy.get('#dropdown').select('value1')
} else {
cy.get('#button').click()
}
})
Note this only works for static HTML. If you've just clicked the toggle and it's animating, it would pick the button before the animation completes (but the same applies to other methods using .then() or await).
You can use then, but it gets a bit messier when you have more levels of nesting
cy.get('#slide-toggle-1')
.find('input')
.filter('#slide-toggle-1')
.then((btn)=>{
if (btn.ariaChecked === 'true') {
cy.get('#dropdown').select('value1')
}
else {
cy.get('#button').click()
}
})
You should be able to use await with cypress-promise
import promisify from 'cypress-promise'
const btn = await promisify(
cy.get('#slide-toggle-1')
.find('input')
.filter('#slide-toggle-1')
);
if(btn.ariaChecked === 'true')
if(btn[0].ariaChecked === 'true') // might need [0] as per #eric99's comment
I have already done this using the following code:
cy.get('#slide-toggle-1-input').then(($toggleBtn) => {
if($toggleBtn.attr('aria-checked') === 'true') {
//command here if the toggle button is turned on
}
else {
//command here if the toggle button is turned off
}
})
Also don't use dynamic elements such as my example above, I just use that for easier understanding. Instead, use a regular expression or RegEx for the locators like below.
//example above, don't do this
cy.get('#slide-toggle-1-input')
//USE THIS for dynamic locators
cy.get("[id^='slide-toggle-'][id$='-input']").first()
//the ^ search for properties starting from
//the $ search for properties ending from
Read this for further details on the cy.get() and dynamic locators: https://docs.cypress.io/api/commands/get#Selector
Hope it helps everyone, especially who's just starting on Cypress like me! :)

Night watch change value of textContent or innerHTML

I have the following markup and I need to be able to change the values of the textContent for these items:
<div class=".react-grid-HeaderCell-sortable">Jamie</div>
<div class=".react-grid-HeaderCell-sortable">Hutber</div>
<div class=".react-grid-HeaderCell-sortable">Stackoverflow</div>
I know how to change values for input fields, but currently don't know how to do text, maybe with a custom command?
For inputs
.setValue('.someSelector', 'anewString')
Array.from(document.getElementsByClassName('react-grid-HeaderCell-sortable'))
.forEach(x => x.innerHTML = 'new text')
Explanation:
document.getElementsByClassName returns an HTMLCollection, which needs to be transformed to an array with Array.from in order to be iterable.
In order to modify text of a div, you need to set the innerHTML attribute. Please note innerHTML can be replaced by textContent, refer to W3C docs for more information on the difference.
As already noted, the only way to do this is to use .execute() and modify DOM from inside the page context.
However, depending on your particular use cases, you may want to use more complex selectors, so I am suggesting to use querySelector() for that.
In addition, you can also create a shortcut for this operation as shown below:
module.exports = {
'Test': function(client) {
client
.url('http://some.url')
.execute(setElementValue, ['table.selector > .child-selector', 'new value'])
.end();
}
};
function setElementValue(selector, value) {
document.querySelector(selector).textContent = value;
}
You might as well create a custom command for that, so you'll be able to reuse it in other tests and make your code look well:
// ./lib/custom-commands/setElementValue.js
exports.command = function(selector, value) {
this.execute(function(selector, value) {
document.querySelector(selector).textContent = value;
}, [selector, value]);
return this;
};
// Test case
client
.url('http://some.url')
.setElementValue('table.selector > .child-selector', 'new value')
.end();
Don't forget to point Nightwatch to the directory where you keep your custom commands, by setting custom_commands_path property of config file.

Select elements in HTML via cypress.get()

I'm using cypress to write some tests against an html site..
The following selects me correctly a single tr elements from a table on my HTML site.
The site contents looks like this:
<tr data-recordid="theId">
<td...><div ..>Text 1</div></td>
<td...><div ..>Text 2</div></td>
<td...><div ..>Text 3</div></td>
</tr>
The following test script snippet selects me correctly the single <tr..> part.
cy.get('tr[data-recordid="theId"]').contains('Text')
Now I want to select the text within the <div>..</div> tags..The first thing I have tried to chain a single call for the first <div>..</div> tag like this:
cy.get('tr[data-recordid="theId"]').get('div').contains('Text')
which does not work as I expected. The get() calls a chained jQuery calls (Based on the Docs of cypress). So it looks like I misunderstand how things work in JQuery.
What I'm expecting is how I can check all div elements like this (Not working):
cy.get('tr[data-recordid="theId"]')..SomeHowMagic
.get('td[alt="xyz"]".get('div').contains('Text 1')
.get('td...').get('div').contains('Text 2')
.get('td...').get('div').contains('Text 3')
Any idea how to get forward a step? Missing any information just make a comment.
Let's clarify a few things:
1) If you are just wanting to ASSERT that the div's contain the given text then this is the best possible and most precise way to do this:
cy.get('tr[data-recordid="theId"]').should(($tr) => {
const $divs = $tr.find('div') // find all the divs
expect($divs.eq(0)).to.contain('Text 1')
expect($divs.eq(1)).to.contain('Text 2')
expect($divs.eq(2)).to.contain('Text 2')
})
I can't tell if things need to be this specific. If you only want to ensure that the $tr contains text you could simplify it down to be:
cy.get('tr[data-recordid="theId"]').should(($tr) => {
expect($tr).to.contain('Text 1')
expect($tr).to.contain('Text 2')
expect($tr).to.contain('Text 2')
})
Why do it this way?
Using a .should() function will not change the subject. Your $tr will continue to be the subject going forward.
Cypress will wait until all of the assertions in the .should() callback pass, and continually retry until they do. That guarantees you the state of multiple elements is correct before proceeding.
2) However if you just care that Cypress finds the text and you don't mind the subject being changed you could do this:
cy.get('tr[data-recordid="theId"]').within(() => {
cy.contains('Text 1') // changes the subject to the <div>
cy.contains('Text 2') // changes the subject to the <div>
cy.contains('Text 3') // changes the subject to the <div>
})
This is different than the first example because instead of an explicit assertion you are simply changing the subject to whatever element the text is found in. Cypress's default assertion on cy.contains() is to retry so ultimately the behavior is the same, except you are additionally changing the subject.
If even this is too complicated you could also just do this:
cy.get('tr[data-recordid="theId"] div').contains('Text 1')
cy.get('tr[data-recordid="theId"] div').contains('Text 2')
cy.get('tr[data-recordid="theId"] div').contains('Text 3')
Your original question was also using chained cy.get() which does not drill into subjects. For that to happen use .find()
cy.get('a').get('span') // each cy.get() queries from the root
cy.get('a').find('span') // the .find() queries from the <a>
One final note: you suggested solution does not work. cy.get() does not accept a callback function, and if you look at your Command Log you will not see those 3 cy.contains from ever being invoked. In other words, they are not running. That's why its passing.
So after more experimenting I found a solution:
cy.get('tr[data-recordid="TheId"]>td> div', function() {
cy.contains('Text 1').end()
cy.contains('Text 2').end()
cy.contains('Text 3').end()
})
If someone else has a better solution please post it here.

Categories