-
-
Notifications
You must be signed in to change notification settings - Fork 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
Support React.lazy and React.Suspense #1975
Conversation
@chenesan are you not able to determine the display name of the component that is being loaded by |
@rodoabad I'm not sure what the |
e656f7a
to
3577a02
Compare
@ljharb I just force pushed the branch and I think it's ok to review it now. Basically this PR will supports shallow/mount with |
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.
Thanks, this is a great start.
.babelrc
Outdated
@@ -2,6 +2,8 @@ | |||
"presets": ["airbnb"], | |||
"plugins": [ | |||
["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], | |||
"syntax-dynamic-import", | |||
"dynamic-import-node" |
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.
let's only add these in the "test" env; i don't think this is ever a safe transform except at the app level.
@@ -1,5 +1,6 @@ | |||
{ | |||
"extends": "airbnb", | |||
"parser": "babel-eslint", |
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 also not acceptable; babel-eslint enables all syntax currently, so airbnb projects only use the default parser.
"parser": "babel-eslint", |
@@ -38,6 +67,9 @@ module.exports = function detectFiberTags() { | |||
// eslint-disable-next-line no-unused-vars | |||
FwdRef = React.forwardRef((props, ref) => null); | |||
} | |||
if (supportsLazy) { | |||
LazyComponent = React.lazy(() => import('./_helpers/dynamicImportedComponent')); |
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.
There's zero point in using import()
here - it has to be transpiled, and it's never safe to transpile it below the app level, so this would have to go untranspiled, and that'd be a breaking change.
It'd be much simpler to use () => Promise.resolve().then(() => ({ default: require(…) }))
.
Separately, I'm not sure why this can't be even simpler - React.lazy(() => Promise.resolve().then(() => ({ default() { return null; } }))
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.
Ah... yeah, the Promise.resolve().then(() => ({ default() { return null; } }))
indeed works. If so I think all the added eslint plugins for import
syntax, fake module file and babel-eslint
can be removed. Thanks for this solution!
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.
For what it's worth, this way is much cleaner in the sense that it preserves a single source of truth of "how import()
should work in node" - but due to the underspecified nature of ESM, until node ships unflagged ESM, it's not safe for anything but the top-level app to define "how import()
works", unfortunately.
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 think maybe it's worth to add a utility to make it clearer like
const fakeDynamicImport = (module) => Promise.resolve().then(() => ({ default: module}))
// so we can write
const LazyComponent = React.lazy(() => fakeDynamicImport(SomeComponent))
for all the places using React.lazy
in enzyme packages. But I'm not sure where to put this utility.
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.
Sounds great! If it'd be used by only adapters, it should be in adapter-utils; if by enzyme as well, then probably in enzyme itself; since it's likely to only be used in the 16 adapter for now but the 17 adapter later, probably in adapter-utils?
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.
React.lazy
only appears in 16 adapter and tests. Looks like adapter-utils is a good place.
packages/enzyme-test-suite/.eslintrc
Outdated
@@ -1,6 +1,7 @@ | |||
{ | |||
"extends": "airbnb", | |||
"root": true, | |||
"parser": "babel-eslint", |
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.
"parser": "babel-eslint", |
"object-inspect": "^1.6.0", | ||
"object.assign": "^4.1.0", |
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.
not sure why this got re-sorted, what version of npm did you use to install it?
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 looks like I accidentally add new package with yarn. will fix this.
@@ -0,0 +1,5 @@ | |||
import React from 'react'; | |||
|
|||
const DynamicComponent = () => <div>Dynamic Component</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.
this entire file seems unnecessary if it exists in enzyme
|
||
const wrapper = shallow(<SuspenseComponent />); | ||
|
||
expect(wrapper.find('Suspense')).to.have.lengthOf(1); |
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 a shallow wrapper is "what the wrapped thing renders", i would expect this to be:
expect(wrapper.find('Suspense')).to.have.lengthOf(1); | |
expect(wrapper.is(Suspense)).to.equal(true); |
|
||
expect(wrapper.find('Suspense')).to.have.lengthOf(1); | ||
expect(wrapper.find(LazyComponent)).to.have.lengthOf(1); | ||
// won't render fallback in shallow renderer |
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 add another test that .dive()
s the Suspense
, and then becomes the fallback?
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.
Oops, I forgot to test shallow render Suspense
directly, and I found out that for now react-test-renderer/shallow
doesn't allow render Suspese
:
import ShallowRenderer from "react-test-renderer/shallow"
const renderer = new ShallowRenderer()
renderer.render(<Suspense fallback={false} />)
will throw:
Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.
at invariant (/Users/yishan/Documents/Projects/enzyme/packages/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:54:15)
at ReactShallowRenderer.render (/Users/yishan/Documents/Projects/enzyme/packages/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:382:78)
It looks like that react shallow renderer cannot recoginze the Suspense
type symbol. I should check this at first... So unless react-test-renderer supports this I think we can only support Suspense
and lazy
in mount. 😢
|
||
const wrapper = shallow(<SuspenseComponent />); | ||
|
||
expect(wrapper.find('Suspense')).to.have.lengthOf(1); |
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 a shallow wrapper is "what the wrapped thing renders", i would expect this to be:
expect(wrapper.find('Suspense')).to.have.lengthOf(1); | |
expect(wrapper.is(Suspense)).to.equal(true); |
} | ||
} | ||
const SuspenseComponent = () => ( | ||
<Suspense fallback={false}> |
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.
also here, let's pass a Fallback
Fixed problems except the |
1652d74
to
1bbe447
Compare
@ljharb I'm wondering if I should work on shallow renderer in react side to make progress for this PR. |
Hi @ljharb I tried to start a PR in facebook/react#14638 to support shallow rendering Suspense in react shallow renderer. I'm not sure if I did it right for enzyme. I guess you may be interested in looking into this ;) |
1bbe447
to
7458330
Compare
I'm not sure when @ljharb I think we can start a new round of review. If you think it's better to wait for shallow renderer support, let me know and I'll add tests back, thanks :) |
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 looks good, but I think we should re-add the shallow tests.
If the shallow renderer won't support it, then we'll have to support it for them.
ok, I'll add them back. If there's still no reply in react this week I'll start to work on support Suspense in adapter 16. And if we have to support Suspense in enzyme side, how should we handle the fallback inside Suspense? I think we should keep behavior of shallow as same as mount, and we could just turn unresolved lazy component node in children to fallback node. But if we keep |
@chenesan it kind of depends on what react's planning to do. In other words, in general, we want shallow and mount to be as close as possible. However, when you're shallow rendering something that renders a Suspense or a lazy, those things should just show up in the tree - when you dive through one of them, or when you shallow render one of them directly, then their implementation should be "invoked", whatever that ends up meaning. Similarly, it should be possible (in both shallow and mount) to force a Suspense to render the lazy component or the fallback, so both paths can be tested. |
So when we shallow render Suspense const LazyComponent = React.lazy(() => import('InternalComponent'))
shallow(<Suspense fallback={<Fallback />}>
<LazyComponent />
</Suspense>) then should it render a fallback, or the And another problem is when we
I think 1+3 sounds reasonable.
it sounds like we can pass an option when shallow rendering to determine how to render |
7458330
to
8a1dde1
Compare
Yes. Whether it should render the fallback or the LaxyComponent, I have no idea - (note that this only comes up when the Provider is the component being rendered). As for |
ok, so I'll add an option (maybe |
Sounds great. The only concern I think that's left is "what happens when you shallow render a lazy component", and it'd be great to know what React plans in the shallow renderer before we implement it here (we don't have to wait on them releasing that, we just need to know what they'll do) |
40cc703
to
0a17404
Compare
080187c
to
3139d43
Compare
@ljharb Still not figured this out :-( Sometimes the coverage recovered after I removed some lines of code, but when I tried to reproduce again it still dropped. Wondering if it's an issue in istanbul side -- Do you have any idea around this? |
Sorry, i have a question. When i can use enzyme, shallow with lazy and Suspense? Is there any updates of library in the future? Merge was 8 days ago. But i see that the version of enzyme is the same... |
This will be in the next release; there’s no timeline for it. |
- [new] add `fakeDynamicImport` (#1975) - [deps] update `airbnb-prop-types` - [dev deps] update `eslint-plugin-react`, `eslint-plugin-import`
- [new] support `suspenseFallback` option; support `Suspense`/`Lazy` (#1975) - [fix] `shallow`: properly dive through `memo` components (#2103, #2068) - [fix] avoid a Context.Provider deprecation warning - [fix] shallow renderer for `memo` does not respect `defaultProps` (#2115) - [fix] Don’t show wrapped component displayName in lazy component (#1975) - [fix] `simulateEvent`: call the adapter’s implementation, not the raw one (#2100) - [deps] update `enzyme-adapter-utils` - [dev deps] update `eslint-plugin-react`, `eslint-plugin-import`
I noticed this was released 3 days ago in v 1.13 of enzyme-adapter-react-16. Does Enzyme now fully support dynamically imported components and wrapping? We are still struggling to get this working for our local tests. Is there any documentation or working examples someone could point me to? I've been trying to test a dynamically imported component that is not wrapped in a Suspense component as we are using a Suspense higher up the component tree to wrap everything, however, I'm unsure how to tell enzyme to resolve the promise and load through the component? If I debug I can see the node as a child of my mounted wrapper but it's just listed as a react.lazy. I believe this is the correct strategy when using Suspense components, though I'm also quite new to this and may well be wrong! |
@GitHub-Mar no, this pr has two components, enzyme itself (not released) and the adapter (released). |
Ah my mistake! So this feature is not currently supported in Enzyme, then? I'm guessing your above comment still stands and there is no timeline as to when this will be released? |
Correct. |
v3.10.0 has now been released. |
Still failing. Do you have some docs for this? I mean maybe i should update my tests or something like this? |
@DonikaV all the docs are published. If you've fully updated enzyme and your adapter, please file a new issue and fill out the template. |
Can I use Enzyme |
I'm still a bit confused on what's the best way to test that a component wrapped in lazy renders inside Suspense.
If I use the method mentioned above to mock react:
The debug() returns something like:
I want to test that MyComponent actually renders MyLazyComponent.
|
Fixes #1917 .
Given a component wrapped by
React.lazy
in<Suspense />
It'll plainly render a
<Lazy />
inshallow
and render fallback component in mount.There are something I'm not sure / still working on:
What should
displayName
of the component returned byReact.lazy
be? Currently I directly named it asLazy
. Not sure if it's something we could define by ourselves.I'm trying to add an
waitUntilLazyLoaded()
onReactWrapper
, which will return a promise resolving when the dynamic import loaded and React trigger the re-render, so we can write some tests like:But I don't know how to detect if all the lazy loaded component inside
<Suspense />
has completeted loading. It looks like that we have to hack around react fiber. @ljharb Would you know any way to detect this?Also note that this PR add
babel-plugin-dynamic-import-node
andbabel-plugin-syntax-dynamic-import
for usingimport()
,babel-eslint
inenzyme-adapter-react-16
andenzyme-test-suite
for dynamic import support of eslint.