Correctly chaining selectors in Cypress - css-selectors

I've been working with Cypress for a few weeks and I'm starting to need to more complex selecting. Right now I am failing to understand how to chain selectors to get what I need.
Here is a simplified version of the HTML I am looking at:
<my-application-list-item test-hook="applicationListItem">
<div>
<!---->
<div>
<header>
<a test-hook="name" href="/build/applications/iVANKl9Z">E2E Test Item</a>
<my-build-application-menu>
<!---->
<my-dropdown testhook="actionsMenu">
<my-icon-button-settings testhook="openActionsMenu">
<button testhook="openActionsMenu">
The test hooks were placed when we were using Protractor, and there is inconsistency in the hyphenation. But anyway, I want to find and click the openActionsMenu that is inside the applicationListItem which contains the visible text "E2E Test Item".
I thought this would work, based on the docs for get, contains, and find:
cy.get('[test-hook="applicationListItem"]')
.contains("E2E Test Item")
.find('[testhook="openActionsMenu"]')
but this resulted in
Timed out retrying after 4000ms: Expected to find element: [testhook="openActionsMenu"], but never found it. Queried from element: <a.ellipsis.my-info-box__title-link.my-link>
even though running
cy.get('[testhook="openActionsMenu"]')
succeeds. What am I doing wrong?

Use the .contains(selector, content) form of contains().
The subject passed down is the one matching selector.
cy.contains('[test-hook="applicationListItem"]'), "E2E Test Item")
.find('[testhook="openActionsMenu"]')
Experiment a bit, use .then(console.log) to check out the subject at that point
cy.contains('[test-hook="applicationListItem"]'), "E2E Test Item")
.then(subject => console.log(subject) // <my-application-list-item>
.find('[testhook="openActionsMenu"]')
.then(subject => console.log(subject) // <my-icon-button-settings>

"contains" yields the element containing the text, in this case the <a> tag. If you use siblings(), you can force the runner to search in the next element:
cy.get('[test-hook="applicationListItem"]')
.contains("E2E Test Item")
.siblings()
.find('[testhook="openActionsMenu"]')

Related

Testing workflow with Cypress and React

I have question regarding the development and testing workflow. I am using Cypress but this topic is suitable for any end to end test.
The question is how do you selecting the elements in the browser?
1, Explicit selectors like data-cy or automation-id on each element or component.
2, Selecting the elements by visible text on the screen and then navigate to specific element by DOM hierarchy.
Every test you write will include selectors for elements. To save yourself a lot of headaches, you should write selectors that are resilient to changes.
Oftentimes we see users run into problems targeting their elements because:
Your application may use dynamic classes or ID's that change
Your selectors break from development changes to CSS styles or JS behavior
Luckily, it is possible to avoid both of these problems.
Don't target elements based on CSS attributes such as: id, class, tag
Don't target elements that may change their textContent
Add data-* attributes to make it easier to target elements
<button
id="main"
class="btn btn-large"
name="submission"
role="button"
data-cy="submit"
>
Submit
</button>
And then for example clicking to button
cy.get("[data-cy=submit]")
.should("be.visible")
.click()
You can also search for specific text in dom.
cy.get("button")
.should("be.visible")
.contains("Submit")
.click()
Custom commmands commands.js
Cypress.Commands.add("sendBtn", () => {
cy.get("[data-cy=cy_send_btn]")
.should("be.visible")
.click()
})
And in test file
it("Add test description here", function() {
.
.
.
.
cy.sendBtn()
})
Custom command shown above you will be able to use multiple times in other test files for all send buttons. Your tests will be more isolated and efficient.

What could cause "Unable to find a label with the text of" for an element that is present?

What could cause getByLabelText from testing-library to report that it can't find an element when the element is definitely there? The exact error I'm getting is
TestingLibraryElementError: Unable to find a label with the text of: Remove Item: Item C
It appeared after I ran a test that reported failure on the line
const removeItemButton = screen.getByLabelText('Remove Item: Item C');
The test output contained a chunk of page source that included
<span
class="my-company-span"
data-testid="custom-react-tag"
>
Item: Item C
<button
aria-label="Remove Item: Item C"
class="myco-chp__button"
type="button"
>
...
For further proof that the element does exist, I ended up getting it using a different function (getAllByRole) but I think getByLabelText would be cleaner.
The documentation says that aria-label attributes are valid targets for getByLabelText. Just in case there was an unusual character in there, I copy-pasted the name of the aria-label directly from the error output to the getByLabelText argument, and nothing changed. I am using the function getByLabelText() successfully on other elements in the same file, so it's not a bad import.
I've seen a lot of questions online for the similar-but-not-the-same error "Unable to find an element with the text" but the only thing I found for my situation was for someone using iframes, which doesn't apply to me.
Not sure if this is what is happening here, but I ran into a similar problem testing Material UI Textfields. What I finally end up doing was passing in the exact parameter as false. It's not evident looking at the output what extra characters might be getting added.
For your query it would look like this:
const removeItemButton = screen.getByLabelText('Remove Item: Item C', {exact:false});

Testing for text broken up by multiple elements in React Testing Library

I'm having trouble finding the correct way to test a div with text that is broken up by multiple elements - the error says 'Unable to find an element with the text: This could be because the text is broken up by multiple elements' but doesn't explain how to do so.
To explain what I'm trying to test, the element returned in the jest render() fn is as followed:
<div>
<div
class="inventory-wrapper"
>
<span>
5
</span>
item
s
left
</div>
</div>
</body>
I'm trying to test that the text inside is '5 items left', but as you can see - the text is broken up by a span tag and multiple lines.
From what I understand, I should be targeting this with getByRole() only? Does that mean I need to add a role='...' to the .inventory-wrapper div too? And how would I then test for the text '5 items left'
Any help on the correct wait to target something like this would be very appreciated
Getting by role might be preferred but isn't the only way to query the DOM https://testing-library.com/docs/queries/about/#priority.
I worked around this by adding a test id to the component, querying that and using its text content. Just make sure the test id is passed to the root element in the component.
render(<Component data-testid="component" />)
expect(screen.getByTestId('component').textContent).toBe('Text to test')

Getting wordpress posts with react shows special chars instead of apostrophe

I am getting what I am assuming is json data from a wordpress blog endpoint like so:
https://example.com/wp-json/wp/v2/posts
I am looping through and showing the tiles for now:
<div>{posts && posts.map((post) => <h1>{post.title.rendered}</h1>)}</div>
But the post titles are not displaying properly. For example the word Don't shows Don’t
I have discovered that I can use dangerouslySetInnerHTML to fix this issue but is it safe? The fact that it has the word 'dangerously' in it is worrying.
I believe dangerouslySetInnerHTML is the way to go about this - but I will go into more detail as to why "dangerously" is in "dangerouslySetInnerHTML" and hopefully that will help you make an informed decision for your situation.
What dangerouslySetInnerHTML does is render any HTML string given to it within the DOM element.
For example:
<h1 dangerouslySetInnerHTML={{__html: post.title.rendered}} />
(as an aside, note the __html key has two underscores)
Will properly render the string Don’t to Don't.
This is all pretty harmless, however, if, for example, the value of post.title.rendered could be set by an untrusted party (such as an arbitrary user), and if this arbitrary user wanted to do some damage, they could enter a string such as:
<script type="text/javascript>
// Do evil stuff
console.log('I did some evil stuff');
</script>
This code would then be executed by the browser when the page loads - because React would have generated the following DOM:
<h1>
<script type="text/javascript>
// Do evil stuff
console.log('I did some evil stuff');
</script>
</h1>
So with all that in mind, if you are sure that the value of this field is within your control (and not anyone else's) and you also know that there will not be any arbitrary code in these strings, then go ahead and use dangerouslySetInnerHTML.
However, if there is the possibility that someone besides yourself could manipulate this field, I would instead look to something like decode-html-entities - this way you can have the presentation you want, without compromising your app/users.

How to click on a deeply buried button in div classes via protractor. No id

I am trying to click a button that is buried in div classes in the code via protractor.
I am pioneering a protractor project for my work and have reached a point where I no longer know what to do. I have a button that is buried in div classes and is not allowing me to click. I have tried using mouseMove to get over to the coordinates of the button, I have tried using the className of the specific button, etc. The button does not have an id. The id is not the issue as I have tried clicking a different button, equally buried in divs, by it's id. I need to know how to get through the layers of divs in order to click the button because the rest of the tests will be dependent on it.
APPLICATION CODE:
::before
<dashboard-label>
<div class="att-topic-analysis-tabs">
<div class="att-button-group">
<button class="btn btn-default btn-lg att-close-topic ng-scope"
role="presentation" tabindex="-1"
ng-click="removeTopic(currentTopic.id)" translate>
Close Topic
</button>
</div>
</div>
PROTRACTOR TEST:
it('Closes Topic Successfully', function(){
//opens the first available topic
openTopic.click();
//checks that the URL contains 'topics' after 5 seconds
browser.wait(proExpect.urlContains('topics'), 5000);
var closeTopic = element(by.className('att-close-topic'));
//browser.wait(proExpect.elementToBeClickable(closeTopicButton), 5000);
console.log(closeTopic);
closeTopic.click();
browser.wait(proExpect.urlContains('home'), 5000);
});
As you can see, the Close Topic button is kind of buried in div classes and the standard click isn't working. Any info would be greatly appreciated
If the closeTopic locator is finding the element, but failing to click it, check to make sure there's only one matching element in the DOM, and that it's visible. My favorite way to check the DOM is just ctrl-F in Chrome inspector and paste the exact CSS that the test is using (.att-close-topic). And to check that what it's getting is visible, use
console.log(closeTopic.isDisplayed());
This can be a big gotcha in protractor, because it doesn't fail (only warns) when there are multiple matches on the page, and it defaults to the first match rather than the first visible match, which drives me nuts, because it's very rare that you want to do anything with a non-visible element on the page.
This will be partly opinion, but just to add a layer to the conversation...
Sometimes the solution to locating a troublesome element on the page is to go back to the developers and make the page more testable. I've seen testers spend hours or days crafting brilliant workarounds to access a stubborn element, and the end result was a fragile, complicated end-to-end test (and aren't they fragile enough already?).
Sometimes a 5-minute conversation with a developer can result in a quick change in the production code (e.g. add a unique ID) that avoids all that effort and yields a much better result, more stable, more simple. But this requires open conversation between the dev and test team, and a culture that values testing as a primary activity enough to make those testability changes to production code that is otherwise working just fine.
This is what you want to read to help you debug why your test doesn't work.
Also, you might want to start adopting await/async since the control flow will go away in the future.
http://www.protractortest.org/#/debugging
try this
var closebutton=element(by.css("[ng-click="removeTopic(currentTopic.id)"]"),
EC = protractor.ExpectedConditions;
Waits for the element to be clickable.checks for display and enable state of button
browser.wait(EC.elementToBeClickable(closebutton), 10000);
now use : closebutton.click();

Resources