Skip to content

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

Merged
merged 6 commits into from
Apr 25, 2019

Conversation

kentcdodds
Copy link
Member

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 > 1

Checklist:

  • Documentation added to the docs site
  • Typescript definitions updated N/A
  • Tests
  • Ready to be merged

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.

@kentcdodds
Copy link
Member Author

Note: this PR is to the next branch because it's a breaking change.

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 next branch as well.

@kentcdodds kentcdodds requested review from alexkrolick and wyze March 19, 2019 22:38
src/queries.js Outdated
@@ -136,7 +137,6 @@ function queryAllByTitle(
function queryByTitle(...args) {
return firstResultOrNull(queryAllByTitle, ...args)
Copy link
Collaborator

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?

Copy link
Member Author

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...

Copy link
Collaborator

@alexkrolick alexkrolick Mar 19, 2019

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... 🤔

Copy link
Collaborator

@alexkrolick alexkrolick Mar 20, 2019

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

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.

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.

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?

Copy link
Member Author

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*

Copy link
Collaborator

@alexkrolick alexkrolick Apr 16, 2019

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.

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 👍

@@ -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\`)).`,
Copy link
Collaborator

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

Copy link
Collaborator

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).`

@bcarroll22
Copy link

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 👍

@alexkrolick
Copy link
Collaborator

@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.

@kentcdodds
Copy link
Member Author

I can do that 👍

@kentcdodds
Copy link
Member Author

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...

@kentcdodds
Copy link
Member Author

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:

yarn add --dev dom-testing-library@alpha

Let me know what you think!

@alexkrolick
Copy link
Collaborator

@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')

@kentcdodds
Copy link
Member Author

I had considered that as well. Maybe I'll give it a try 👍 I like what you've got there.

@kentcdodds
Copy link
Member Author

I've just published an alpha of react-testing-library that upgrades dom-testing-library to the alpha as well:

yarn add --dev react-testing-library@alpha

@kentcdodds
Copy link
Member Author

I just verified that my workshop code all still works with these changes so that's great :)

@kentcdodds
Copy link
Member Author

kentcdodds commented Apr 18, 2019

Great idea on the buildQueries thing @alexkrolick 👍 I like it.

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.

@alexkrolick
Copy link
Collaborator

alexkrolick commented Apr 18, 2019

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 queryAll functions for the first time. It'll make it a lot simpler for people to build custom queries that implement all the query types.

@kentcdodds
Copy link
Member Author

That's a good point!

@bcarroll22
Copy link

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.

@bcarroll22
Copy link

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 display-value but not for placeholder. Was there a reason for that?

@kentcdodds
Copy link
Member Author

I dunno 🤷‍♂️

Kent C. Dodds and others added 2 commits April 25, 2019 12:44
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.
@kentcdodds
Copy link
Member Author

Ok, I'm ready to make this happen :)

@kentcdodds kentcdodds merged commit 12b0222 into next Apr 25, 2019
@kentcdodds kentcdodds deleted the pr/throw-get-by branch April 25, 2019 18:46
kentcdodds pushed a commit that referenced this pull request Apr 25, 2019
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants