-
Notifications
You must be signed in to change notification settings - Fork 470
fix(getBy*): throw an error if more than one element is found #229
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
Conversation
Note: this PR is to the I can't think of any other breaking changes I want to make at this time, so once this is merged, we'll go ahead and merge the |
src/queries.js
Outdated
@@ -136,7 +137,6 @@ function queryAllByTitle( | |||
function queryByTitle(...args) { | |||
return firstResultOrNull(queryAllByTitle, ...args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the query APIs would still return the first one even if there are multiple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Hmmm... Should we throw for this as well? Probably huh...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure... querySelector vs querySelectorAll would just return the first item. Almost the only use case for this is asserting null
anyways... so... 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see it throwing as well. Maybe to clarify it all we could make a table:
Proposed
One Match | Multiple Matches | No Match | Async? | |
---|---|---|---|---|
getBy | Return | Throw | Throw | No |
findBy | Return | Throw | Throw | Yes |
queryBy | Return | Throw (?) | Null | No |
getAllBy | Array | Array | Throw | No |
findAllBy | Array | Array | Throw | Yes |
queryAllBy | Array | Array | Empty Array | No |
Current
One Match | Multiple Matches | No Match | Async? | |
---|---|---|---|---|
getBy | Return | First | Throw | No |
findBy | Return | First | Throw | Yes |
queryBy | Return | First | Null | No |
getAllBy | Array | Array | Throw | No |
findAllBy | Array | Array | Throw | Yes |
queryAllBy | Array | Array | Empty Array | No |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this model does it make sense to have query
and queryAll
? Should there just be:
query
get
getAll
find
findAll
?
To me that seems pretty consistent with like, the type of behavior you'd expect from a REST API for example. like /api/users
is going to return a list of users even if there's only 1 (or 0). But if i say /api/users/1
or /api/users/1,2
i'm asking for specific things and I'll get a 404 if they're not found.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙄 duh it was the first sentence, nvm i can't read apparently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like I'd like to see query continue to never throw. I think it's nice to have a set of searches that you know aren't going to throw in any circumstance, but then again it might catch some bugs people don't know about currently... I dunno 🤔
Do you see any chance that throwing might make it more confusing for knowing when to use query vs. get since they both have the potential to throw in this model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you think there could be more than one then use queryAll*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The primary use case for queryBy
is for when you expect something not to exist and you want to check it returns null. The only I can think of it being disruptive to change it would be if you were doing something with .not
, like
await wait(() => expect(queryByText('something')).not.toBeNull())
Which, in that case, would throw inside the wait
and trigger a retry same as a failed expect. There might be some places where it's used differently/synchronously though.
The proposed error message makes it pretty clear what's going on if you see it, IMO. Could even link to the release notes for the major version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good to me, i can dig it. we can make this happen in native too 👍
src/query-helpers.js
Outdated
@@ -29,6 +29,13 @@ function getElementError(message, container) { | |||
return new Error([message, debugDOM(container)].filter(Boolean).join('\n\n')) | |||
} | |||
|
|||
function getGetByElementError(message, container) { | |||
return getElementError( | |||
`${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`getAllByText\` or \`findAllByText\`)).`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message is nice and clear and makes the breaking change pretty safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be able to enhance it even more with another parameter like query
, which contains the suffix, AllByText
. Then this could be updated to:
`${message}\n\n(If this is intentional, then use the \`get${query}\` or \`find${query}\` variant of the query).`
f3d9b06
to
af9df05
Compare
af9df05
to
09826d5
Compare
d39b4ea
to
410c6ed
Compare
I’ve pretty much got this ready in NTL, just waiting to merge/release until I know whether or not queryBy should throw as well 👍 |
@kentcdodds how about releasing a beta release with queryBy throwing, then we can test it out on some codebases and see if it breaks a lot. |
I can do that 👍 |
Sorry it's taking so long. I finally decided to look at this thing through the lens of abstraction (https://kcd.im/aha) and I think I've figured out a good way to avoid copy/paste issues and hopefully make abstraction worthwhile and make this easier to maintain. We'll see... |
Ok, feel free to look at the PR. It's kinda a big diff now, but I think it'll be a good thing for the codebase. I'd love to hear what people think. I've also published an alpha:
Let me know what you think! |
@kentcdodds the makeXQueries are an interesting abstraction - do you think you could simplify it even more to something like this: const [getByText, getAllByText, findByText, findAllByText, queryByText]
= buildQueries(queryAllByText, e => 'single error', e => 'multiple error') |
I had considered that as well. Maybe I'll give it a try 👍 I like what you've got there. |
I've just published an alpha of react-testing-library that upgrades dom-testing-library to the alpha as well:
|
I just verified that my workshop code all still works with these changes so that's great :) |
Great idea on the I'm a tiny bit worried this is over-abstraction. But I think it'll be better this way. I think we've had this copy/pasted code long enough to know what the right abstraction is here. |
I think it's fairly safe to assume we can continue abstracting the query types off of a base query- I considered it even back last year when adding the |
That's a good point! |
Man. Oh. Man. This is a good stuff. I think I need to digest it a bit more, but I believe this even helps us in native be more explicit and make the tests more closely resemble the way the platform works. To me it's a good balance, it abstracts where it makes sense, but it's also a good example of not abstracting when it makes sense not to. This is awesome, I like it a lot. |
Question. When you were doing this, how did you decide when/when not to use the abstractions vs writing a custom implementation? For example, you limit what elements are queried for |
I dunno 🤷♂️ |
Closes #202 BREAKING CHANGE: All `getBy` and `findBy` query variants now will throw an error if more than one element is returned. If this is expected, then use `getAllBy` (or `findAllBy`) instead.
a40e86d
to
78ec79c
Compare
Ok, I'm ready to make this happen :) |
Closes #202 BREAKING CHANGE: All `getBy` and `findBy` query variants now will throw an error if more than one element is returned. If this is expected, then use `getAllBy` (or `findAllBy`) instead.
What: fix(getBy*): throw an error if more than one element is found
Why: Closes #202
TL;DR: more explicit, more better.
How: Added some logic for the
getBy
queries to throw if the returned elements has length > 1Checklist:
BREAKING CHANGE: All
getBy
andfindBy
query variants now will throw an error if more than one element is returned. If this is expected, then usegetAllBy
(orfindAllBy
) instead.