-
Notifications
You must be signed in to change notification settings - Fork 4.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
Create @wordpress/test-utils package #18855
Conversation
@diegohaz I'm very excited about this 😍 . I think testing React components/apps based on what the user sees and actual* DOM events is a big improvement. (*As real as JSDOM is concerned) We'll get much more integration and coverage across code when your components are executed and rendered, rather than the fragmented and often heavily mocked tests from something Enzyme. Adding a collection of test utils (like your |
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.
Since I observe many of these functions proxy through to underlying testing libraries (Testing Library), and while I can see there's quite a bit of added utility here, I'm curious in your own words why not to just use Testing Library directly, than to create our own custom abstraction atop an existing abstraction?
Since with an abstraction, we'll have additional maintenance overhead and lose of any benefit of shared knowledge of an existing tool (barrier to entry for new contributors), I'd want to be clear on the benefits it brings.
I can imagine some of the scenarios where these added behaviors could be useful, but since we never tried to see how far we could get with Testing Library alone, it's unclear whether these would be common pain points in that experience.
(Note: I read through #17249 but I'm also retroactively catching up on this discussion, so apologies if I've overlooked this mentioned previously)
); | ||
} | ||
|
||
window.Element.prototype.getClientRects = function() { |
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.
Should we be modifying the global this way here? Is there even any guarantee that the DOM globals exist? In our own project, we have these globals set up, but this is managed by the Jest preset. Are other projects which consume from test-utils
expected to set this up themselves? Should this be documented?
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.
Good question! This library will require projects to setup an environment that at least simulates the browser, like jsdom
, which seems pretty common on React projects (I'm not sure about custom blocks projects though). But I'll include that in the readme.
This mock is needed here because jsdom
can't do layout and therefore there will be no value for getClientRects
, clientWidth
or clientHeight
. The isVisible
function in @wordpress/dom
would always return false
.
I'm going to experiment mocking that only when the related focus methods are called and restoring it right after that. It seems to be a safer approach.
Thanks for the review @aduth! 😊 Updated the PR description with this. Basically, I'm considering that, once it's stable, we'll recommend this library for testing projects that are already using Per this comparison, For example, even though this works in the browser, this would fail using any of the existing testing libraries out there that use import { render, fireEvent } from "@testing-library/react";
test("should call onMouseDown when button is clicked", () => {
const onMouseDown = jest.fn();
const { getByText } = render(
<button onMouseDown={onMouseDown}>button</button>
);
const button = getByText("button");
fireEvent.click(button);
expect(onMouseDown).toHaveBeenCalled(); // ❌ failure
}); The test would have to know about the component implementation details and use
The current solution for this is to write end-to-end tests instead. But they're harder to write and incredibly slow to run. Using The intention of this PR is to grab the best import { render } from "@testing-library/react";
import { click } from "@wordpress/test-utils";
test("should call onMouseDown when button is clicked", () => {
const onMouseDown = jest.fn();
const { getByText } = render(
<button onMouseDown={onMouseDown}>button</button>
);
const button = getByText("button");
click(button);
expect(onMouseDown).toHaveBeenCalled(); // ✅ success
}); Testing Library has a package called One option is to make |
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 appreciate the efforts. This is an outstanding proposal. It's evident that you put a lot of work into crafting this package. It resembles a lot the public API of Puppeteer as noted in details in my comment :)
The current solution for this is to write end-to-end tests instead. But they're harder to write and incredibly slow to run. Using jsdom we can --watch specific test files and have feedback almost instantaneously after saving our files.
Let me respond to your statement. Historically, we had more PRs opened from new contributors proposing e2e tests rather than those proposing unit tests, which goes against your point that writing e2e tests is harder. I'm sure it's very hard to set everything up and ensure they are reliable. However, there are many reasons why e2e tests are difficult to maintain like network requests, the overall complexity of the app under test, etc.
I agree that having the watch mode working out of the box for unit tests is a big advantage. I'm wondering if you could replicate the same conditions when using Jest + Puppeteer by importing the same render method used in the tested page in the actual test file and thus having this feedback wired with the watch mode.
Overall, I think you are promoting the same API I would love to see when testing UI components in isolation. I'm still not convinced that mocking DOM with jsdom
and building additional abstractions over DOM to make it behave like a regular browser is the best approach. Anyway, I like that you provided a proof of concept which can lead the discussion in the right direction.
@@ -0,0 +1,10 @@ | |||
export { default as act } from './act'; |
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 must admit that it looks very familiar to the Puppeteer API. I did quick search in their documentation to find matching methods and nearly all of them are covered:
act
– React testing helperblur
click
- https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pageclickselector-optionsfire-event
focus
– https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pagefocusselectorhover
– https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pagehoverselectorpress
– https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-elementhandlepresskey-optionsrender
– React testing helpertype
– https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pagetypeselector-text-optionswait
- https://pptr.dev/#?product=Puppeteer&version=v2.0.0&show=api-pagewaitforselectororfunctionortimeout-options-args
This makes my comment left on the parent issue even more tempting to explore:
#17249 (comment)
Thanks for taking this up @diegohaz. In my opinion, this is much needed and a great contribution. Thank you! As I expressed in my original Issue, Enzyme often encourages less optimal testing practices - although it is possible to test well, it's much easier not to! I agree with your approach which aligns nicely with that which I suggested in the Issue. Using an abstraction over RTL avoids us reinventing the wheel and provides the benefits of a well-tested library. If it ever falls out of favour or becomes unmaintained, we can simply port over the methods we require and continue to maintain this
@aduth My argument here is simplicity and utility. I've now implement x2 PRs where I've tried to follow testing "best practices" using the raw React Test Utils. Whilst it is perfectly possible to test well using this approach, it isn't easy. In general, it requires writing a lot of custom code which makes the process laborious (and somewhat tedious) - hardly a good recipe for encouraging folks to write tests. As an example, say you want to select a With RTL this is as easy as using the document.querySelector('*').find(x => x.innerText === 'Hello World'); However, this is naive and will return false positives as I'm sure someone will suggest a simple workaround to the above example, however I believe the wider point it illustrates is still valid. If we want contributors to write tests (and we surely do) then we need to make it as simple as possible. You shouldn't need to know how to implement a text content matcher in order to test a simple component using best practices. In summary, the principal reasons why I'm in favour of wrapping RTL are:
vs e2e testsI should also add that I'm not suggested we stop writing e2e tests. I believe we should recognise that they solve a different purpose. RTL should be used to quickly add test coverage to a group of related components, but not an entire application. We will always need e2e tests but in my experience having written a fair number of e2e tests in Gutenberg (although admittedly not as many as the others contributing to this discussion!) they do not provide a fast feedback loop (~30s even on my 1yr old MBP isn't quick). This slows down development and reduces the likelihood that I'll bother to write tests at all. We should also note that there is also (entirely valid) resistance to adding e2e tests for small components (or groups thereof). There is considerable overhead with each new e2e test and therefore only key interactions are deemed important enough to warrant being covered in this level of detail. This is precisely why we currently have "unit" tests for components. A simple, effective and fast way to provide some level of confidence in our code without the overhead of a full e2e test. This proposal doesn't remove the requirement for e2e tests, it merely improves the existing implementation of our component "unit" (Functional) tests. Thanks again @diegohaz. |
...also, I wonder whether we there would be value in seeing a refactor some existing component tests to use this new package. Probably in a separate PR branched from this PR. |
Thanks for the thoughtful comment @getdave! I started rewriting some existing tests in my own fork. Here's a PR for Toolbar: diegohaz#1 It includes tests for the roving tabindex behavior using the |
@diegohaz Can I do anything to help you to get this over the line? |
@getdave The code here is just waiting for an approval (and possibly more refactors on existing tests? Just let me know) 😆 There are some conflicts now. I'll resolve them asap. |
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 is a pretty incredible PR, @diegohaz! Kudos to you for the amount of work that went into it.
I've added the Needs Decision
label, as I wasn't sure if an approach had been agreed on in terms of testing components, I know there's been some discussion in the past about using an end-to-end testing framework for that purpose.
I personally like the idea of these tests being quick to run, providing a fast feedback loop, and exposing a more human-understandable API than some other react testing libraries.
Looking at the code, the most intricate part is that it simulates a lot of the fundamental browser interactions—e.g. chains of events that get triggered from a click/hover/etc.—and it looks like lots of attention to detail went into the implementation. I could also see that this part will need quite a bit of real world usage to establish that it works as expected, and it's also where maintainers of the package will need the most knowledge. Documenting and testing how this works will be important, so it's great that there are tests in this PR.
I started reviewing, but it'll take a bit of time to go through everything.
if ( | ||
! element || | ||
isBodyElement( element ) || | ||
getActiveElement( element ) !== 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.
I wonder if conditions like this should fail noisily given they indicate undesired behaviour (or dead test code).
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 reason it's not throwing is so you can call blur()
just to blur the active element, even if there isn't one (and document.activeElement
is document.body
, so isBodyElement(element)
would return true
).
This is used by focus
, for example:
gutenberg/packages/test-utils/src/focus.js
Lines 15 to 27 in e317fe4
export default function focus( element ) { | |
if ( getActiveElement( element ) === element || ! isFocusable( element ) ) { | |
return; | |
} | |
blur(); | |
act( () => { | |
element.focus(); | |
} ); | |
fireEvent.focusIn( element ); | |
} |
But I think we can throw if you explicitly pass an element
to blur(element)
.
*/ | ||
export default function blur( element ) { | ||
if ( ! element ) { | ||
element = getActiveElement(); |
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 there any benefit to passing an element into this function? It seems like it has to be the activeElement anyway (see line 22).
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.
My thoughts for adding that parameter were:
- Consistency: all the other functions receive
element
as a parameter. - Readability: you may want to explicitly pass the element to this function so your test code is more clear.
But I'm totally fine in removing it. :)
fireEvent.mouseMove( element, options ); | ||
} | ||
|
||
document.lastHovered = 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.
@diegohaz Clever ✌️😇
I've created a simpler alternative that just adds |
Also, as an alternative to this PR, we could use We don't have to maintain it ourselves as we would have to do with this PR. But the project is still not mature enough, and since it's not being maintained by the core |
I came to this PR to mention
Should we close this PR and integrate this library instead? I see new commits landed in the last days so it means it's still actively maintained. |
We also discussed it this week here: #22352 (comment). |
Yeah! I agree that we should close this and use One thing to keep in mind is that it may be necessary to wrap the user events within |
Kudos for the work on this PR regardless of the fact that it was closed :) |
Description
Closes #17249
This PR creates a new package called
@wordpress/test-utils
that provides utility methods to write integration tests with almost as much confidence as with end-to-end tests with the same developer experience as with unit tests (easy to write and run).This is not meant to be a replacement for e2e tests though. This is more like a much better alternative to unit testing components with Enzyme.
Usage examples can be found in the tests: https://github.com/diegohaz/gutenberg/tree/add/test-utils/packages/test-utils/src/test
Why
In addition to what was discussed in #17249, I'm considering that, once it's stable, we'll recommend this library for testing projects that are already using
@wordpress/*
packages, like custom blocks and apps.Per this comparison,
react-testing-library
seems to be the best option for testing single React components. That's why this package is wrapping many of their methods. But it's still not ideal compared to manual browser testing or end-to-end testing, specially because you can't reliably reproduce user interactions with their abstractions.For example, even though this works in the browser, this would fail using any of the existing testing libraries out there that use
jsdom
:The test would have to know about the component implementation details and use
fireEvent.mouseDown()
instead. We would fall into a similar problem I described here:The current solution for this is to write end-to-end tests instead. But they're harder to write and incredibly slow to run. Using
jsdom
we can--watch
specific test files and have feedback almost instantaneously after saving our files.The intention of this PR is to grab the best
react-testing-library
abstractions and fill its gaps with user interactions. This would work:How has this been tested?
npm run test-unit
Types of changes
New feature
Checklist: