-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to deal with dynamic class names à la CSS Modules or Styled Components? #1212
Comments
Interested as well in this subject. At the moment we're using a mix of unique class names (ie. "payment-button") with |
how would you prefer doing this? I used |
Wanted to say, @willklein mentioned this in his talk, basically he recommends adding classes purely for testing environment and then stripping them out for production. Perhaps he can elaborate more on how they did that for their Cypress tests. Link to video here: https://youtu.be/rICGz5qrYJU?t=1h42m28s |
yeah, one could always come up with a conventional prefix such as Forgive me the question, didn't have time to read too deep into the underlying implementation of cypress yet, but I'd assume you're using webpack? If that's the case, you could offer people to provide their own loaders. In that case I could just point For example, in my cypress test i could then just do the following: /* src/components/email-form/styles.css */
/*
* css modules will transform this class
* into sth like this: .styles__email-input-cmXvUE
*/
.email-input {
color: blue;
} /* cypress/integration/email-form.spec.js */
// path is a bit ugly but could be mitigated by using NODE_PATH
import styles from '../../src/components/email-form/styles.css'
const URL = 'http://localhost:8080'
describe('Email Form', () => {
before(() => {
cy.visit(`${URL}/`)
})
it("should have an email input", () => {
cy
// .get("[class*=email-input)") => the current way of doing things
// the same code i'm using to style my React components which would evaluate to the same .styles__email-input-cmXvUE
.get(styles.emailInput) // => the new, more exact, way of doing things
.should('have.attr', 'type', 'email')
})
}) |
We use the "data-testid"-convention inspired by BEM-naming, which I think works great for us. Example: <div class="some-class" data-testid="Login__form__container">
...
</div> And then we select test elements using a custom Cypress command that basically just wraps cy.getTestElement('Login__form__container')
// getTestElement, just the function here, but it's a Cypress command in our codebase
function getTestElement(selector) {
return cy.get(`[data-testid="${selector}"]`);
} The main benefit for us is that we have a clear convention for how to name test stuff that's not coupled to actual classes or similar. This means that we can refactor stuff and keep our tests as long as we assign the appropriate test id:s to the right elements again when refactoring. It also allows us to write tooling around that convention (a Chrome extension that lists all available testid:s on a page, with click-to-copy-selector etc), but that type of functionality can of course be implemented regardless of what naming convention you use. Anyway, just a thought! |
Sorry, I just noticed this. @zth describes the technique I prefer: custom data attributes. Using We didn't need to BEM name things, but that's also a pretty good idea. In our case, we put semantic descriptors in our form element containers along with data model specific descriptors because we had them. We had a lot of shared, pure components and with only modifying about five lines of template code, we instrumented an infinite number of fields, buttons, and links. Instrumenting other apps may require more changes, but it should still be worthwhile. How you configure your build to remove this for production will get into framework-specific tooling. If you're using React and Babel, this is what I use:
If there's any desire from the Cypress team, I'm happy to try writing up a guide on this if it fits in the documentation. |
We are all for In fact we've talked about making this the default attribute we use for the Selector Playground - which would make it super nice to automatically find these. |
@brian-mann @willklein Thumbs up 👍 for Any input on this approach for a React project with Create React App (CRA) and styled-components without ejecting? Create React App does not use Any thoughts? |
@krishnaxv that seems like a clear trade off when using something like Create React App: you're giving up control/configuration to have things managed for you. I definitely try to keep from ejecting whenever possible, but if I'm starting to spin up a lot of tooling and need my own Babel plugins (I often do), I find it's time to eject. |
@andreiashu @bahmutov do you guys know already if there is a solution for those use cases? Maybe i missed something but didn't saw anyone mentioning your questions with a solution or any workaround or this is something on the map to be added in near future in cypress maybe? |
We strongly recommend adding custom attributes for testing. Especially if they follow the convention, then they become even compatible with CSS Selector Playground. See these two resources: |
Custom attributes seems like a good workaround for selecting elements but what about when you are testing for the class name itself? Is there a way to test that the class name just contains a word? For example: |
@andrewdang17 this should work: |
@bkucera Your suggestion works perfectly in assertions, thanks. cy.get('[data-e2e="companiesFilter"]').then(($curEl) => {
if($curEl.hasClass('togglerCollapsed')) {
cy.get('[data-e2e="companiesFilterHeader"]').click();
}
}); doesn't work because class name is |
@MariiaB sure, you can get the class names with if (cy.$$('[data-e2e="companiesFilter"]').attr('class').includes('togglerCollapsed')) {
// your code here
} |
I am curious. Since custom |
@bkucera Thank you! I works. ;-) const checkClassPresence = (element, className) => {
cy
.get(element)
.invoke('attr', 'class')
.should('contain', className);
}; |
Hi @bkucera I tried your approach
but i think it would be a lot of performance hit and also it is not working :( Also the second approach of putting custom data attributes, doesn't sound too good as it would make me add attributes just to test things. In Jest, it is easier, as we can always import the component in the test framework and compare the snapshot with the rendered output. example:
courtesy: https://jestjs.io/docs/en/tutorial-react but yes Jest is specific to the frameworks like React/Vue and Angular. I hate the fact that it lacks the nice integration platform which is offered by cypress. +1 to cypress for that. |
@arshmakker see our example in http://on.cypress.io/assertions#Should-callback |
The thing about Partial matching of classnames will surely have a performance hit, so I rather not... This needs some new thinking out of the box. For example, some code post-processing plugins that would take care of this shenanigans in the background. I am not sure. It kinda makes me avoid testing these problematic scenarios which are a shame. Seeing this on Cypress landing page, it makes me 😢 |
@FredyC feel free to use data attribute or partial class name match like we show, it is up to you. Whatever you think will be maintainable in the long term and less likely to break. As far as performance - you are literally testing your code by loading the app again and gain in each test. Slightly less optimal selector would NOT even be close to loading the application over the network or affecting the speed of the test. We suggest best practices for selectors in https://on.cypress.io/best-practices#Selecting-Elements and I personally recommend using https://github.com/kentcdodds/cypress-testing-library too |
@bahmutov I've explained the problem of the data attribute, not sure what you don't understand about that. Also, I am using Emotion and there are not even remotely accessible class names to match against. |
Sure @FredyC I understand - you are using 3rd party library that does not allow adding data attributes and does not have good class names. Well, what can Cypress do besides giving you |
@FredyC might not help you in prod, but you can try this package https://github.com/lttb/reselector to automatically add data attrs from component name edit: I believe it assigns an internal id to each component |
Not only 3rd party components. Imagine a situation there is a generic
I am not saying it's a fault of Cypress (or anyones), I am mostly trying to open discussion if there isn't some better way. Writing tests shouldn't be hard, right?
Not sure how that could work considering you have only the DOM at your disposal. If it's somehow doable then it would definitely improve things a lot in my opinion. Although it would probably need to support selectors as well because only the name of the component doesn't need to be unique. Or would it be chainable with regular selectors? It's really intriguing idea :) |
If classes need to be dynamic, they should be set based on state. E.g. using CSS Modules: import React, { useState } from 'react'
import css from './Menu.module.css'
const Menu = () => {
const [menuOpen, setMenuOpen] = useState(false)
const _toggleMenu = () => {
const newMenuOpenState = !menuOpen
setMenuOpen(newMenuOpenState)
}
return (
<>
<button onClick={_toggleMenu}>Open / Close Menu</button>
<nav className={`${css.menu} ${menuOpen ? css.menuOpen : css.menuClosed}`} />
</>
)
} |
This saved my life. Thank you. |
I had stuck with this when working with Cypress, but I found this on the document of Cypress: https://docs.cypress.io/api/commands/get#Find-the-link-with-an-href-attribute-containing-the-word-questions-and-click-it |
Any pointers on how to deal with dynamic class names produced by e.g. CSS Modules or
styled-components
?The easy way out would be to just plaster my markup with
id="e2e-blabla"
but that doesn't feel particularly nice to be honest.Another way could be to look for partial matches in class names. For example the CSS Modules class
.form
produces the actual class name.styles__form--3gcRC
in the DOM. One could now use a CSS attr selector such as[class*=form]
which would work in many case but can lead to name conflicts and kinda removed all the benefits of having locally scoped and unique class names.Current behavior:
There's no documented way (at least i couldn't find one in neither issues nor docs) to make cypress play well with dynamic class names generated by commonly used tools such as CSS Modules or
styled-components
Desired behavior:
There's a well documented way of having cypress be able to look for dynamic class names in the DOM
How to reproduce:
Try to make cypress work with any React app that uses either
CSS Modules
orstyled-components
The text was updated successfully, but these errors were encountered: