Skip to content
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

Add retry-ability to the cypress adapters for dom-testing-library #78

Merged
merged 7 commits into from
Sep 4, 2019

Conversation

Jmaharman
Copy link
Contributor

What:
Replaced the waitForElement functionality with the built in cypress retry-ability helper to:

Why:

  • Give feedback in the UI that the command is being re-run, at present you can't see any spinners
  • Cover a scenario where loading a new page stopped the text from being found

How:
I've had to update src/index.js pretty rapidly to make this possible and while it doesn't break the API it is drastic.

Checklist:

  • Documentation N/A
  • Tests
  • Ready to be merged

While I feel that the code is complete, as well as having more tests now, it is drastically different from what it was previously and so I think a key contributor / project owner will need to look over it closely.

It seems to be that if you load a new page, the retry functionality that's baked in at the moment doesn't work.
There is a built in, but undocumented, promise loop that can be run so that functions can continue to be executed time and time ago, with this enabled the previously failing test now passes.

To get it to work I have to trap the getBy exceptions when an item was not found. This means the previously useful error thrown is no longer as easily visible. I've made it accessible by clicking on the command. All getBy, getAllBy, queryBy, queryAllBy, etc, behaviours should all still work as expected.

Another side effect of this was that Chai assertion error messages are not as helpful. I've hijacked the jQuery object selector because Cypress uses that in the error message for jQuery object messages. In the selector I've placed a description of the underlying function call, which gives pretty good feedback in the UI window.

I had to adjust other tests due to message changes that came with retry-ability.
@kentcdodds
Copy link
Member

Super appreciate this PR @Jmaharman. It sounds like a great idea, and I'll try to pull it down and play around with it soon. I'd love it if other people watching this repo could do the same and provide feedback as well. Thanks!

@kentcdodds
Copy link
Member

Looks like the build is failing due to linting issues. If you run npm run validate before pushing that should run everything that the travis build runs so you can make sure you've cleared everything up. Could you do that please?

@weyert
Copy link
Contributor

weyert commented Aug 27, 2019

I will see if I can get this PR working locally at my work notebook. This looks exciting. If I understand it correctly this would make it easier to detect appearing or disappearing elements without the need of wait() left and right?

@alexkrolick
Copy link
Contributor

alexkrolick commented Aug 27, 2019

This seems like the right direction to close #23, #33, #30, #31

Leaving a few comments on the code...

src/index.js Outdated
}
catch (err) {
// Catch exceptions where it can't find the element, so we can retry
if (/Unable to find an element with the text/.test(err.message))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this regex capture all of the errors thrown by DOM Testing Library queries? There are other ones like "Unable to find an element with the label text" etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally catching any exception to getBy working as expected, when testing other functionality I realised that the retry-ability stuff would try everything even when the failure is correct. For example getBy, findBy and queryBy all throw an error when matching multiple results, and that scenario is less likely something the user wants to retry, once it fails that is probably the correct result.

If there are other error messages for getByLabelText, getByDisplayValue, getByTestId, getByRole etc, then yes we definitely need to add those or create a specific error type in dom-testing-library and catch those.

I'll defer the decision to someone with greater knowledge of dom-testing-library for that one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've since seen that getByLabelText doesn't retry, due to the error message not being as you mention. We can add the relevant messages for each of those queries, but I wonder if there is a way to catch all of those in a more succinct and deliberate way?

@kentcdodds can we create a specific type of error message for get* exceptions in dom-testing-library? That way we can test on something more concrete than the error message text, which could change over time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we add a property to the error object like error.isDTLError = true? Would that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something along those lines would be fine yes. What does DTL stand for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also create an error class in DTL, and use instanceof

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main thing we need to know is how many elements have been found. If it’s 0 then we know we need to re-run the function, if it’s more than that then we let it throw.

Either adding a property to that affect on said errors would work, whether you want to use a new class of error is up to you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(DTL is DOM Testing Library)

src/index.js Outdated

// Overriding the selector of the jquery object because it's displayed in the long message of .should('exist') failure message
// Hopefully it makes it clearer, because I find the normal response of "Expected to find element '', but never found it" confusing
result.selector = queryName + '(' + findTextOrRegex(args) + ')';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice improvement

src/index.js Outdated
})
}

if (/queryBy|queryAllBy/.test(queryName)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what makes getBy different than queryBy?

I kind of think we should just make findBy and findAllBy the default names in Cypress since they are async even in DTL, and just not implement the other styles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's the only difference at the moment. We can't retry the something where it's valid to return a null or empty array, simply because the retry-ability will keep going until it finds something.

I simply run the function to get the dom element and return early, no retryability for these. The only other option would be to keep retrying until just before the original timeout, but to me that would slow down tests unnecessarily, the developer should check the page is ready through some other query before using query*

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of think we should just make findBy and findAllBy the default names in Cypress since they are async even in DTL, and just not implement the other styles.

I'm kinda thinking this is the right way to go. What do other's think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a reasonable change. I don't think we need to support all the same queries for Cypress if they don't make sense with the Cypress ecosystem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were no tests for find* so out of curiosity I've converted all of the get* tests to find* locally to see what happens, of which they all fail.

I've only ever used the get* and query* queries, and hadn't realised the implications of find*.

I've quickly tried to fix the find* queries, however I've found that when it can't find an element it doesn't show the spinner because it's still waiting for a response from the find* queries.

The try-ability code works on the premise that it will find the value, check to see if the next assertion passes and if not try again, until the timeout expires. Due to the fact it only tries once, we don't see the spinner.

I don't mind get* or find* are removed, but either way they both need to be implemented using the get* underlying functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm, are you both saying you are happy for CTL to have the 4 queries?

  • queryBy*
  • queryAllBy*
  • findBy*
  • findAllBy*

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and if we could implement getBy* and getAllBy* to throw an error with a descriptive message that would be good. For example:

You used getByAllText which has been removed from Cypress Testing Library because it does not make sense in this context. Please use findByAllText instead.

I think that would be great :)

By the way, thank you so much for working on this so tirelessly! I'm excited for this to be released and to have a library that works much better with Cypress than ever before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at all, I'm doing very little in the grand scheme of things. We started using this library at work, and I saw the issue and realised I could do something about it.

I need to get something else done, but should be able to do the work over the weekend / early next week.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super excited to get this merged! I will see if I can use your repo at my work's project to improve the tests and let you know how it goes. Currently, I have a bunch of wait() to give the page time to render some elements.

My understanding is that with this PR I could avoid these changes. I also it sounds like it should use findByText etc instead of getByText.

How can I help getting this merged?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay guys, things have been hectic at work and at home. I will get onto it soon.

src/index.js Outdated Show resolved Hide resolved
find* now used query* implementations, but hooked up via the Cypress retry-ability so that it remains async as per the main DTL API.

get* queries are now redundant, because users expect the retry-ability from Cypress by default, we now throw an error asking the user to use find* instead.

query* queries continue to work as they did before.

I've created tests for all functions, and then the main behaviors associated to each type of query.
@Jmaharman
Copy link
Contributor Author

Here you go all, get that down and give it a test in your own projects.

@weyert
Copy link
Contributor

weyert commented Sep 3, 2019

Thanks, I will give it a try tomorrow morning at work. I hope I can remove a bunch of wait() in my tests now 💃

@kentcdodds
Copy link
Member

I resolved some merge conflicts and published this as 5.0.0-alpha.0 (you can install it via npm install @testing-library/cypress@alpha). I've tried it on my projects and upgrading was as simple as find/replace .get with .find in the cypress/ directory. 🎉 Great work here! I say this is ready to go!

@kentcdodds
Copy link
Member

Oh, and this is great 👍

image

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to go IMO.

@kentcdodds
Copy link
Member

Just FYI to whoever merges this (can be me or another maintainer), make sure to use Squash and merge for this one and follow the instructions. This one will definitely need BREAKING CHANGE: ... at the end of the commit message so we can get a major version bump.

@alexkrolick
Copy link
Contributor

LGTM, tested with a few smoke tests and just needed to update the get queries to find.

@alexkrolick alexkrolick merged commit ab5263d into testing-library:master Sep 4, 2019
@kentcdodds
Copy link
Member

🎉 This PR is included in version 5.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@Jmaharman
Copy link
Contributor Author

I've realised this morning that I'm not retrying when multiple matches are found, it fails early. I'll open another PR for it.

@kathar1223
Copy link

@Jmaharman i installed the latest version of this library. but unfortunately i don't see the command is not waiting for element to exist.
eg : cy.getByTestId('test-id') used to implicitly wait based on the defaultcommandtimeout given in cypress.json.

but after changing to cy.findByTestId('') doesn't wait and also doesn't fail even after passing wrong element to it.

I am i missing something here ?

@kathar1223
Copy link

@Jmaharman @kentcdodds can you please respond to the above comment

@Jmaharman
Copy link
Contributor Author

Jmaharman commented Oct 13, 2019 via email

@kentcdodds
Copy link
Member

Ping

@Jmaharman
Copy link
Contributor Author

@kathar1223 I've tried to replicate this issue in the Cypress testing suite for this project and found no issue. It waits for the cypress default of 4 seconds and then returns the below error:

image

I assume there is something else occurring here for the issue to arise for you. Can you explain your scenario in full? Are any of the preceeding interactions modifying the page in someway, e.g. previous command clicks a new link, which loads a new page

A reproducible example would be appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants