-
Notifications
You must be signed in to change notification settings - Fork 412
feat: assert whether a string Element is in the document OR contained… #42
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
feat: assert whether a string Element is in the document OR contained… #42
Conversation
This looks interesting. I wonder though if this would be resilient to subtle changes in the string representation of the element expected to be found. Also, how resilient is this to changes of whitespace in the string representation of the element to be found. For instance, consider the following example: // <div><h1>Hello</h1> ...more stuff</div>
expect('<h1>Hello</h1>').toBeInTheDocument(); Would the above test still pass after some code is introduced that attaches, for example, a // <div><h1 data-outline="1.1">Hello</h1> ...more stuff</div>
expect('<h1>Hello</h1>').toBeInTheDocument(); The other caveat I see could happen is with whitespace: // <div><h1>
// Hello
//
// World
// </h1> ...more stuff</div>
expect('<h1>Hello World</h1>').toBeInTheDocument(); Not saying it is wrong if any of the two examples above do not pass. I just wanted to bring it up for discussion, and if that's the case, and if this is to be merged, we need to document those caveats, or better yet, see if we can mitigate some of their effects. |
I worry that this would make the tests to tied to the DOM implemention. What if I put some text in a span? Doesn't change the way it looks, but it does break the test. I personally would not use it recommend this feature. |
I understand the motivation for this change, but I think its limitations make it too brittle, as @gnapse and @kentcdodds pointed out above. Maybe if the behaviour was closer to enzyme's |
@gnapse I think you made fair points, but isn't it it the same concerns with toHaveTextContent and white spaces? |
That's true, and there are other parts of this library that I wish were more exact by default (such as Another consequence of exactness, in this case, is that the tests are verifying details about the structure of your markup that should be irrelevant to the user. I.e., as a user, I don't care if the "user name" element uses a |
@jgoz let's agree to disagree. In my use case my user directly manipulates html from a platform called contentful. Not all users are created equal so quite the opposite it seems that I'm testing what they care about. They expect their html to be inserted in the DOM. I believe context matters, and generalizing that exactness is always testing implementation details is not a statement I agree with. |
@6ewis your use case sounds like you have components that receive html as a string (provided by a user in the real use of those components in your app) and they render that html within them, and you want to assert that they did so and used that html verbatim. Is that so? Sounds interesting! It kinda puts our concerns to test, I have to admit. What we've all raised here is not to be easily dismissed though. For example, the user normally cares to see a label saying "Your message was sent successfully!". They do not care if that message internally is a It may be so that your use case is different, and we can discuss it. In your use case it may be valid, but we're worried that this new feature will be misused for use cases like the successful message notification above. |
This is a really good conversation! The concept of how contentful is being used sounds awesome and is a fun use case! At first glance, it looks like a perfect opportunity for snapshot testing. If the PR is merged, I prefer we keep the current interfaces, for consistency with the rest of the matchers, and don't overload the selectors to accept strings. Instead, I think a separate selector would be clearer: E.G.:
|
Hmmm, good point about snapshot testing. Wouldn't that work for you @6ewis? Otherwise, the |
…d in another Element
e36d36a
to
2b3bf72
Compare
@gnapse I shy away from snapshot testing in general because people tend to update large snapshots without paying much attention; I prefer more explicit assertions. More over, we cannot control what html the user will put - it's up to the users to decide whether their html is what they want or not, small or large.. We only sanitize the html. Thank you guys for your input. I'll leave it to you to decide whether it's worth it to merge it or not. I like @smacpherson64 suggestions so I updated my PR and the documentation. However, it could also be good to see if it's a requested feature in the coming months. It may be too much of a rare case. The guiding principle of react testing lib took me here, so we should never lose sight of the big picture -- either way. |
src/__tests__/to-contain-html.js
Outdated
|
||
expect(grandparent).toContainHtml(stringChildElement) | ||
expect(parent).toContainHtml(stringChildElement) | ||
expect(child).toContainHtml(stringChildElement) |
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.
It'd be nice to have .not.toContainHtml
assertions here passing as well.
Also, I'd like to have tests about what happens when you pass in a string with invalid html, such as
expect(container).toContainHtml("<div>Hello world</p>");
What happens right now? Will the test simply not pass? Or will it not pass because it throws an error because the html is not valid? Regardless, that use case should be made clear in the tests.
There are also other cases, like what happens if you pass null
or undefined
instead of a string?
src/to-contain-html.js
Outdated
|
||
export function toContainHtml(container, htmlText) { | ||
const stringToHtml = item => | ||
new DOMParser().parseFromString(item, 'text/html').body.firstChild |
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 function stringToHtml
should be declared above, outside this scope, as it does not need anything from this scope. Also prefer a function
over an arrow function in this case. It could also take care of the if htmlText is a string
check and return null.
README.md
Outdated
toContainHtml(htmlText: String) | ||
``` | ||
|
||
Assert whether a string representing a HTML element is contained in another Element: |
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.
Element
at the end need not be capitalized.
README.md
Outdated
``` | ||
|
||
> Note: Chances are you probably do not need to use this method. We encourage testing from the perspective of how the user perceives the app in a browser. That's why testing against a specific DOM structure is not advised. It might come in handy in rare cases such as if your users manipulate the DOM such as a content management tool - where users are expected to type custom HTML. please use: [`toContainElement`](#tocontainelement) | ||
|
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 came up with an alternative that perhaps got too lengthy, so feel free to just take ideas from it. Cause I still think the text above is missing some key features:
Chances are you probably do not need to use this matcher. We encourage testing from the perspective of how the user perceives the app in a browser. That's why testing against a specific DOM structure is not advised.
It could be useful in situations where the code being tested renders html that was obtained from an external source, and you want to validate that that html code was used as intended.
It should not be used to check DOM structure that you control. A good rule of thumb would be to not use it if you find yourself passing a string literal as the expected html. In such cases please obtain actual references to the elements you want to check for (using
document.querySelector
or other equivalent methods) and usetoContainElement
instead.
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 like this better as a separate method. A few comments and suggestions from me.
README.md
Outdated
### `toContainHtml` | ||
|
||
```typescript | ||
toContainHtml(htmlText: String) |
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.
String
-> string
README.md
Outdated
// ... | ||
``` | ||
|
||
> Note: Chances are you probably do not need to use this method. We encourage testing from the perspective of how the user perceives the app in a browser. That's why testing against a specific DOM structure is not advised. It might come in handy in rare cases such as if your users manipulate the DOM such as a content management tool - where users are expected to type custom HTML. please use: [`toContainElement`](#tocontainelement) |
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.
You could probably leave out "It might come in handy in rare cases such as if your users manipulate the DOM such as a content management tool - where users are expected to type custom HTML".
And capital P
on "please".
I wrote this before I saw @gnapse's feedback. I defer to his suggestion.
src/to-contain-html.js
Outdated
import {matcherHint, printReceived} from 'jest-matcher-utils' | ||
import {checkHtmlElement} from './utils' | ||
|
||
export function toContainHtml(container, htmlText) { |
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 currently does not match the capitalization in the typescript definition. I suggest renaming everything to toContainHTML
.
src/to-contain-html.js
Outdated
import {checkHtmlElement} from './utils' | ||
|
||
export function toContainHtml(container, htmlText) { | ||
const stringToHtml = item => |
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.
You could move this out of toContainHTML
and combine it with checkHtmlElement
:
function checkHtmlText(htmlText, ...args) {
const htmlElement = typeof htmlText === 'string'
? new DOMParser().parseFromString(htmlText, 'text/html').body.firstChild
: null
checkHtmlElement(htmlElement, ...args)
}
And then use it instead of checkHtmlElement
below:
checkHtmlElement(container, toContainHtml, this)
checkHtmlText(htmlText, toContainHtml, this)
@@ -0,0 +1,48 @@ | |||
import {render} from './helpers/test-utils' | |||
|
|||
/* eslint-disable max-statements */ |
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.
Is this still needed?
src/__tests__/to-contain-html.js
Outdated
|
||
expect(grandparent).toContainHtml(stringChildElement) | ||
expect(parent).toContainHtml(stringChildElement) | ||
expect(child).toContainHtml(stringChildElement) |
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.
Can you add positive test cases for .not
?
const nonExistantString = '<div data-testid="child"></div>'
expect(grandparent).not.toContainHTML(nonExistantString)
expect(parent).not.toContainHTML(nonExistantString)
expect(child).not.toContainHTML(nonExistantString)
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.
And then add the corresponding negative test cases below?
expect(() =>
expect(grandparent).toContainHTML(nonExistantString),
).toThrowError()
// etc.
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.
Looks good. Great suggestions from @jgoz. I have a couple more comments.
README.md
Outdated
``` | ||
|
||
> Chances are you probably do not need to use this matcher. We encourage testing from the perspective of how the user perceives the app in a browser. That's why testing against a specific DOM structure is not advised. | ||
|
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.
It's important to keep the >
in front of these empty lines, so it all appears as a single note. The same applies to the next empty line below.
src/__tests__/to-contain-html.js
Outdated
const nonExistantElement = queryByTestId('not-exists') | ||
const fakeElement = {thisIsNot: 'an html element'} | ||
const stringChildElement = '<span data-testid="child"></span>' | ||
const nonExistantString = '<span> Does not exists </span>' |
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.
(nit-picking alert!) should say "Does not exist" without the final "s".
Also, I still do not see any test about what happens if you pass invalid html, as in <div>hello</p>
. I'm still not clear if in that case it fails because it won't find the invalid html in the container, or simply because it will fail to parse the invalid html in the first place.
@gnapse anything else? |
README.md
Outdated
> It could be useful in situations where the code being tested renders html that was obtained from an external source, and you want to validate that that html code was used as intended. | ||
|
||
> \ |
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'm sorry for nit-picking, but why do you need the \
? Without it you get separate praragraphs, which was my intention.
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.
Nice addition, thank you 👍
@gnapse done |
🎉 This PR is included in version 1.11.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
What:
Assert whether a string representing a HTML Element is in the document:
expect("<div>Example</div>").toBeInTheDocument()
Assert whether a string representing a HTML element is contained in another Element:
expect(container).toContainElement(<div> Example </div>)
Why:
It's more intuitive to expect the methods
toContainElement()
andtoBeInTheDocument()
to expect HTML wrapped in strings. Also, there are use cases where it makes more sense. Picture a component that receives a dynamic HTML content as an input and parse it via a HTML parser - we could then test that the newly added DOM Element are in the document. It is more declarative - we are not only limited to query the DOM to check if an element is in the DOM.How:
Simply By checking the type of the argument, if it is a string then we verify that it is a valid HTML element first then we check that it is included in the html DOM.
Checklist: