-
Notifications
You must be signed in to change notification settings - Fork 47k
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
expose TestUtils.act()
for batching actions in tests
#14744
expose TestUtils.act()
for batching actions in tests
#14744
Conversation
cc @kentcdodds ^ |
moving it into TestUtils |
Possible follow up: warn if we detect setState on a Hook in jsdom environment? |
unstable_interact
for batching actions in testsTestUtils.interact()
for batching actions in tests
If we're going to warn for setState on Hook outside of this thing, we should do it before release. |
Noted. I'll send a followup in a bit. |
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.
We shouldn't expose private APIs to do this in the production build since we'll have to live with those for longer. The goal is to get rid of all the other exports so it's zero private exports. We only get there if we stop going the other direction. Let's just use the hack in the test utils.
We should bikeshed the name a bit more. interact
is the wrong word because not all of these are interactions, and even for things we've called interactions in the past we should rename (e.g. to "discrete events" rather than interactive events).
function batchedInteraction(callback: () => void) { | ||
batchedUpdates(callback); | ||
flushPassiveEffects(); | ||
} |
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 should move to ReactTestUtils and instead using the ReactDOM.render(null...)
trick to flush the passive effects. That way we don't have to add any unnecessary invasive APIs to the production ReactDOM
. We can add private APIs once we have the new bundle that is exclusively for testing.
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.
agreed, I felt icky doing this. will change,
I think this looks good. We'll probably just re-export the
I'm not sure I understand this. |
strong agree. it felt wrong right there. some options - |
We want to add a warning when you In fact this is already a problem in classes. |
We shouldn't warn in all jsdom environments. Only test ones, such as jest. Sometimes jsdom can be used in some other esoteric use cases - e.g. to server render webcomponents. |
@threepointone Instead of detecting if we're inside "interact()", we can detect if we're batching updates or in concurrent mode. I.e. if we're in sync mode. It's unfortunate because we won't warn if you use |
This makes sense. So correct me if I'm wrong but this basically means that if you interact with a component in any way that calls a state updater, you'll get a warning if it wasn't done within an |
This is tricky because we don't have a way to detect. We can detect Jest by We could check |
I suppose any jsdom server renderer should be using batchedUpdates anyway. |
@@ -380,6 +380,11 @@ const ReactTestUtils = { | |||
|
|||
Simulate: null, | |||
SimulateNative: {}, | |||
|
|||
interact(callback: () => void) { |
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.
Drop the inter
. Just call it act
. It's cleaner.
There is precedence in the It's also short so it's not too bothersome to write all the time in all your tests. I think it's a feature that it is non-descriptive about what actually this models. It's kind of a frame boundary but it's not always because they can also flush early. Event sequences move around a lot in various heuristics/polyfills/spec changes. So there isn't a clear semantic other than there is a bunch of work. |
|
||
interact(callback: () => void) { | ||
ReactDOM.unstable_batchedUpdates(callback); | ||
ReactDOM.render(null, document.createElement('div')); |
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 we reuse a single node? Maybe put an inline React element instead so we don't bail out.
I believe that in the nested case, these will flush at the outer |
TestUtils.interact()
for batching actions in testsTestUtils.act()
for batching actions in tests
Canonical jsdom detection jsdom/jsdom#1537 (comment) |
Question - Assuming the warning exists, how would I get this test to pass? it('lets a ticker update', () => {
function App(){
let [toggle, setToggle] = useState(0)
useEffect(() => {
let timeout = setTimeout(() => {
setToggle(1)
}, 200);
return () => clearTimeout(timeout)
})
return toggle
}
const el = document.createElement('div')
act(() => {
ReactDOM.render(<App />, el);
})
jest.advanceTimersByTime(250); // this warns!!!
expect(el.innerHTML).toBe("1")
}) for context - I wrote |
one workaround is to isolate the render call and the timer advance, so this passes the test - act(() => {
ReactDOM.render(<App />, container);
});
act(() => {
jest.advanceTimersByTime(250); // doesn't warn
})
expect(container.innerHTML).toBe('1'); bit annoying though. another alternative, also annoying - act(() => {
act(()=> {
ReactDOM.render(<App />, container);
})
jest.advanceTimersByTime(250); // this warns!!!
}); (putting them both in the same act call doesn't work, since the effect wouldn't have fired yet) |
Good point @threepointone. I can see how this complicates the testing story quite a bit. Even experienced React engineers will struggle with writing tests for effects like this 🤔 |
* expose unstable_interact for batching actions in tests * move to TestUtils * move it all into testutils * s/interact/act * warn when calling hook-like setState outside batching mode * pass tests * merge-temp * move jsdom test to callsite * mark failing tests * pass most tests (except one) * augh IE * pass fuzz tests * better warning, expose the right batchedUpdates on TestRenderer for www * move it into hooks, test for dom * expose a flag on the host config, move stuff around * rename, pass flow * pass flow... again * tweak .act() type * enable for all jest environments/renderers; pass (most) tests. * pass all tests * expose just the warning from the scheduler * don't return values * a bunch of changes. can't return values from .act don't try to await .act calls pass tests * fixes and nits * "fire events that udpates state" * nit * 🙄 * my bad * hi andrew (prettier fix)
act()
for testing react componentsReact, like other libraries, doesn't guarantee synchronous ordering and execution of it's own work. eg - calling
this.setState()
inside a class doesn't actually set the component state immediately. In fact, it might not even update the state of the component in the same 'tick'. This isn't a problem when it comes to 'users'; they interact with React surfaces asynchronously, giving react and components plenty of time to 'resolve' to particular states.However, tests are unique in that people write code, usually sequential, to interact with React components, and some assumptions they make won't hold true. Consider - with React's new
useEffect
hook, a component can run a side effect as soon as it 'starts up'. As a contrived example -Let's say you write a test for it like so -
The test would fail, which seems counterintuitive at first. But the docs explain it - "The function passed to useEffect will run after the render is committed to the screen." So while the effect has been queued into it's scheduler, it's up to React to decide when to run it. React only guarantees that it'll be run before the browser has reflected changes made to the dom to the user (ie - before the browser has 'painted' the screen)
You may be tempted to refactor this like so -
This would "work" in that your test would pass, but that's because you've explicitly using a render blocking effect where it possibly wasn't required. This is bad for a number of reasons, but in this context, it's bad because we're changing product behavior just to fix a test.
What can we do better?
Well, React could expose a helper, let's call it
act
, that guarantees the sequential execution of it's update queue. Let's rewrite the test -Note - React still doesn't synchronously execute the effect (so you still can't put your
expect
statements insideact
), but it does guarantee to execute all enqueued effects right afteract
is called.This is a nice mental model for separation of concerns when testing components - "React, here's a batch of code I'd like you to run at one go", followed by more code to test what React has actually 'done'.