-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Add createComponentMock option to test renderer #8982
Add createComponentMock option to test renderer #8982
Conversation
@@ -220,6 +232,9 @@ var defaultTestOptions = { | |||
createNodeMock: function() { | |||
return null; | |||
}, | |||
createComponentMock: function(component: Fiber) { |
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.
Something I was thinking about when doing the initial test renderer implementation is if the createNodeMock
had to be part of the HostConfig
long-term. I think it did at the time for backwards-compatibility, but ideally could be done without extending the HostConfig
. Since this is a new API can you think of a way to have this done entirely in renderer-land?
@@ -100,6 +100,8 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = { | |||
resetAfterCommit() : void, | |||
|
|||
useSyncScheduling ?: boolean, | |||
|
|||
mockComponent ?: 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.
I was guided towards making the HostConfig
shape strict in a prior change. May be applicable here as well.
See #8628 (comment)
Don't have a strong preference other than consistency with
Seems fine to me.
In general I'm not a fan of leaking the Fiber to the public interface. Seems like it's not necessary for the typical use case; if there are use cases that come up in the future we can reconsider then. The answer might be obvious, but could you explain why it's important to distinguish between function and class mocks? Why are the mocks different? If it is necessary to distinguish between the two, we should make sure we don't call Is the intention to support this in production mode, too? Are there use cases where mocking in production is necessary? Ideally it'd be nice to strip out any mocking code when not in DEV. |
@@ -450,7 +457,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>( | |||
'An indeterminate component should never have mounted. This error is ' + | |||
'likely caused by a bug in React. Please file an issue.' | |||
); | |||
var fn = workInProgress.type; | |||
var fn = hasMockingBehavior | |||
? mockComponent(workInProgress, hostContext.getRootHostContainer()) |
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.
Would it not be simpler to mock the component just once, on mount?
workInProgress.type = mockComponent(...);
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.
That way this would be the only place we'd ever have to branch on hasMockingBehavior
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.
@acdlite one problem with this option is that then the resulting data structure in toTree()
would have type
be set to the mock, rather than the original value
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.
toTree()
is part of the test renderer, so you could store the original type
on the mocked type
.
Agreed 👍 |
I wonder if this could also be helpful for hot reloading.. |
@gaearon Ooh yeah, should be! |
@acdlite re:
They are different because the place in which i'm mocking them, they've already turned into either a |
Ok, so if that's the only reason, then that's why I think it would be simpler to override the type with the mocked type, and to store the original type on the mock itself. How does that sound? |
@acdlite yeah, I think that's probably the right approach. Let me take another pass at this! 👍 |
@lelandrichardson Awesome, thanks! |
ee70416
to
154cc6b
Compare
@acdlite @trueadm I finally got some time to work on this... I addressed a couple of things:
|
154cc6b
to
ba99f9e
Compare
ba99f9e
to
e35416f
Compare
@lelandrichardson For point |
@trueadm The problem with using a fiber as a key in a Map is that there's two fibers, and potentially more. We could rely on there being just two Fibers (currently true) and storing each in a Map, or look up both. However, we might want more of them or throw them away, so it might be even more invasive than just another field. |
@@ -217,6 +220,29 @@ var TestRenderer = ReactFiberReconciler({ | |||
setTimeout(fn, 0, {timeRemaining: Infinity}); | |||
}, | |||
|
|||
mockComponent(component: Fiber, rootContainer: Container) { |
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 should not be exposing the Fiber data structure to renderers. I have some refactoring ideas where this will break. Instead, pass the individual pieces as arguments.
); | ||
if (mockedFn !== component.type) { | ||
component._unmockedType = component.type; | ||
component.type = mockedFn; |
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 should never mutate the Fiber here. We should instead return the mock and let the algorithm initialize it in the appropriate place.
component.type = mockedFn; | ||
// force the fiber to be indeterminate so that users can mock a class component | ||
// into a functional component and vice versa | ||
component.tag = IndeterminateComponent; |
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 indicates to me that we're not overriding it soon enough. Maybe we need to do the override earlier?
This also doesn't allow you to mock it to a string
to render a host node for example.
}; | ||
case HostComponent: // 5 | ||
return { | ||
nodeType: 'host', | ||
type: node.type, | ||
props: {...node.memoizedProps}, | ||
instance: null, // TODO: use createNodeMock here somehow? | ||
rendered: nodeAndSiblingsArray(node.child).map(toTree), | ||
rendered: flatten(nodeAndSiblingsArray(node.child).map(toTree)), |
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 make this faster? This is super inefficient atm. We have plenty of examples in the codebase of linked list tree traversal without the intermediate arrays.
But why are only host components flattened and not other types?
@lelandrichardson It is kind of weird to be able to mock based on props, since props can change over the life-time of an instance. What if it changes between a functional and class component? What if it changes between two different classes with different state types? If so, it should probably throw away the state as if it was a different type. Would it be enough to mock based on the |
As I think about this more, I think this is correct. It also maybe makes sense to ensure that |
@sebmarkbage are we okay with relying on |
As long as we only depend on WeakMap in this renderer and not every renderer, I'm fine with just assuming it is there. |
In case anyone's interested- building on Leland's idea, I started working on a shallow renderer in PR #9426. |
Seems like we didn't really go anywhere with this. Thanks for PR though! |
This PR is a followup to #8931, and adds a
createComponentMock
option to the test renderer when using fiber. The function is expected to return the correct type (mocked or not) for the provided fiber. This gets called on functional components and class components, but not hosts.I've added corresponding tests that show how one could use this property to emulate shallow rendering, as well as what I believe is an even more practical usage, simple mocking of components that have implementations difficult to test.
The implementation of this uses an additional optional
mockComponent
function that is added to theHostConfig
when creating the reconciler. This required that we pass the host config into a couple of additional places in the fiber codebase.There are still some outstanding questions:
createComponentMock
to returnnull
in the case of not mocking, or keep it as is where it just returnstype
?mockComponent
the right name to use?createComponentMock
gets called with aFiber
. Do we instead want to only pass ittype
andtag
, or some transformation oftag
? Right now user land code will have to know if the mock should be a functional component or a class component. Alternatively, we could have two methods, one for functional component mocking, and another for class component mocking.cc @gaearon @sebmarkbage @aweary @iamdustan