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

Use native event dispatching instead of Simulate or SimulateNative #13023

Merged
merged 3 commits into from
Jun 13, 2018

Conversation

philipp-spiess
Copy link
Contributor

In #12629 @gaearon suggested that it would be better to drop usage of ReactTestUtils.Simulate and ReactTestUtils.SimulateNative. In this PR I’m attempting at removing it from a lot of places with only a few leftovers.

Those leftovers can be categorized into three groups:

  1. Anything that tests that SimulateNative throws. This is a property that native event dispatching doesn’t have so I can’t convert that easily. Affected test suites: EventPluginHub-test, ReactBrowserEventEmitter-test.
  2. Anything that tests ReactTestUtils directly. Affected test suites: ReactBrowserEventEmitter-test (this file has one test that reads "should have mouse enter simulated by test utils"), ReactTestUtils-test.
  3. Anything that dispatches a change event. The reason here goes a bit deeper and is rooted in the way we shim onChange. Usually when using native event dispatching, you would set the node’s .value and then dispatch the event. However inside inputValueTracking.js we install a setter on the node’s .value that will ignore the next change event (I found this article from Sophie that explains that this is to avoid onChange when updating the value via JavaScript).

All remaining usages of Simulate or SimulateNative can be avoided by mounting the containers inside the document and dispatching native events.

Here some remarks:

  1. I’m using Element#click() instead of dispatchEvent. In the jsdom changelog I read that click() now properly sets the correct values (you can also verify it does the same thing by looking at the source).
  2. I had to update jsdom in order to get TouchEvent constructors working (and while doing so also updated jest). There was one unexpected surprise: ReactScheduler-test was relying on not having window.performance available. I’ve recreated the previous environment by deleting this property from the global object.
  3. I was a bit confused that ReactTestUtils.renderIntoDocument() does not render into the document 🤷‍

In facebook#12629 @gaearon suggested that it would be better to drop usage of
`ReactTestUtils.Simulate` and `ReactTestUtils.SimulateNative`. In this
PR I’m attempting at removing it from a lot of places with only a few
leftovers.

Those leftovers can be categorized into three groups:

1. Anything that tests that `SimulateNative` throws. This is a property
   that native event dispatching doesn’t have so I can’t convert that
   easily. Affected test suites: `EventPluginHub-test`,
   `ReactBrowserEventEmitter-test`.
2. Anything that tests `ReactTestUtils` directly. Affected test suites:
   `ReactBrowserEventEmitter-test` (this file has one test that reads
    "should have mouse enter simulated by test utils"),
    `ReactTestUtils-test`.
3. Anything that dispatches a `change` event. The reason here goes a bit
   deeper and is rooted in the way we shim onChange. Usually when using
   native event dispatching, you would set the node’s `.value` and then
   dispatch the event. However inside [`inputValueTracking.js`][] we
   install a setter on the node’s `.value` that will ignore the next
   `change` event (I found [this][near-perfect-oninput-shim] article
   from Sophie that explains that this is to avoid onChange when
   updating the value via JavaScript).

All remaining usages of `Simulate` or `SimulateNative` can be avoided
by mounting the containers inside the `document` and dispatching native
events.

Here some remarks:

1. I’m using `Element#click()` instead of `dispatchEvent`. In the jsdom
   changelog I read that `click()` now properly sets the correct values
   (you can also verify it does the same thing by looking at the
   [source][jsdom-source]).
2. I had to update jsdom in order to get `TouchEvent` constructors
   working (and while doing so also updated jest). There was one
   unexpected surprise: `ReactScheduler-test` was relying on not having
   `window.performance` available. I’ve recreated the previous
   environment by deleting this property from the global object.
3. I was a bit confused that `ReactTestUtils.renderIntoDocument()` does
   not render into the document 🤷‍

[`inputValueTracking.js`]: https://github.com/facebook/react/blob/392530104c00c25074ce38e1f7e1dd363018c7ce/packages/react-dom/src/client/inputValueTracking.js#L79
[near-perfect-oninput-shim]: https://sophiebits.com/2013/06/18/a-near-perfect-oninput-shim-for-ie-8-and-9.html
[jsdom-source]: https://github.com/jsdom/jsdom/blob/45b77f5d21cef74cad278d089937d8462c29acce/lib/jsdom/living/nodes/HTMLElement-impl.js#L43-L76
/**
* Render a TestRefsComponent and ensure that the main refs are wired up.
*/
const renderTestRefsComponent = function() {
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 move this function (which is only used for one test) inside this describe() closure so it's easier to clean up the container.

@gaearon
Copy link
Collaborator

gaearon commented Jun 12, 2018

I was a bit confused that ReactTestUtils.renderIntoDocument() does not render into the document 🤷‍

You're not alone lol

expect(divRef.current.textContent).toBe('remote:2, local:2');

document.body.removeChild(container);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to be careful with stuff like this because it won't clean up if the test fails. I tend to either use try/finally (annoying) or put it in beforeEach/afterEach.

function assignEvent(e) {
event = e;
}
const node = ReactDOM.render(<div onClick={assignEvent} />, element);
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(node));
ReactDOM.findDOMNode(node).click();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can remove findDOMNode here too. Actually we should remove it in a lot of places in the tests, it's often completely unnecessary and is confusing

@philipp-spiess
Copy link
Contributor Author

You're not alone lol

This behavior seems to be around for 5 years already 🙈 Do we ever want to change that? It doesn't have to confuse people for 5 more years 🙂 (Related issue: #3789)

@philipp-spiess
Copy link
Contributor Author

philipp-spiess commented Jun 12, 2018

@gaearon I made sure all attached elements are also cleaned up even if the test fails and also removed all findDOMNode calls when we already have a DOM node. Can you take another look?

@nhunzaker
Copy link
Contributor

I've ended up writing try/catch/finally in tests that need document attachment as well. I wonder, if this is common enough, if it makes sense to write a standard test command or function that automatically cleans up, maybe something like:

it.dom('does something', (container) => {
  // Test away, knowing that container will get cleaned up
})

I feel like abstractions around testing are brutally hard though.

@philipp-spiess
Copy link
Contributor Author

@nhunzaker Yea it's hard to strike the balance between tests that are easy to understand/explicit and avoiding lots of boilerplate code. I think using beforeEach/afterEach works well for this especially if we have more than a couple of tests that require a mounted container in a module:

let container
beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});
afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

The try/finally solution is also ok I guess since it's easy to understand what's intended. Your API would reduce the boilerplate but for folks that are not familiar with our test setup it might be a bit too magical.

@gaearon
Copy link
Collaborator

gaearon commented Jun 13, 2018

Anything that dispatches a change event. The reason here goes a bit deeper and is rooted in the way we shim onChange.

We do have a way to simulate onChange AFAIK. It’s just a bit odd. You have to grab a value setter before it gets shimmed and then use that directly before dispatching an event. It’s how ChangeEventPlugin tests themselves work. It would be good to convert change callsites too.

@gaearon gaearon merged commit 036ae3c into facebook:master Jun 13, 2018
@gaearon
Copy link
Collaborator

gaearon commented Jun 13, 2018

Thanks! If you could follow up for change events that would be great.

@philipp-spiess philipp-spiess deleted the no-simulate branch June 13, 2018 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants