Enzyme: Support Suspense

Created on 27 Nov 2018  Â·  38Comments  Â·  Source: enzymejs/enzyme

Current behavior

I'm currently getting the following error using a Suspense component in my tests.

Enzyme Internal Error: unknown node with tag 13

Looking at the current react work tags, this is for the suspense component: https://github.com/facebook/react/blob/v16.6.0/packages/shared/ReactWorkTags.js#L44

Looking at the current implementation of detectFiberTag it doesn't look for Suspense components: https://github.com/airbnb/enzyme/blob/enzyme%403.7.0/packages/enzyme-adapter-react-16/src/detectFiberTags.js#L42

I believe the exception is coming here: https://github.com/airbnb/enzyme/blob/enzyme%403.7.0/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js#L103 after it hits the default for the switch.

Expected behavior

Can handle suspense components

API

  • [ ] shallow
  • [x] mount
  • [ ] render

Version

| library | version
| ------------------- | -------
| enzyme | 3.7.0
| react | 16.6.3
| react-dom | 16.6.3
| react-test-renderer | 16.6.3
| adapter (below) | 1.7.0

Adapter

  • [x] enzyme-adapter-react-16
  • [ ] enzyme-adapter-react-16.3
  • [ ] enzyme-adapter-react-16.2
  • [ ] enzyme-adapter-react-16.1
  • [ ] enzyme-adapter-react-15
  • [ ] enzyme-adapter-react-15.4
  • [ ] enzyme-adapter-react-14
  • [ ] enzyme-adapter-react-13
  • [ ] enzyme-adapter-react-helper
  • [ ] others ( )
help wanted package 16 minor

Most helpful comment

@anushshukla

You can create a mock: __mocks__/react.js

import React from 'react';

const react = jest.requireActual('react');

const Suspense = ({ children }) => <div>{children}</div>;
Suspense.displayName = 'Suspense';

module.exports = { ...react, Suspense };

All 38 comments

Indeed, Suspense is not yet supported.

Hey, any idea when Suspense will be supported ?

@maclockard are you also using React.lazy?

No, currently just Suspense. Will probably start using lazy as well soon though, but its lower priority to me.

Any update on this? I'm using Suspense with lazy. Just converted over to using, and getting this error. Wasn't sure it there was something being looked at. Thanks in advance.

no luck found.. on this simulate event handling....

Hello @ljharb I'd like to work on this to make React.Suspense and React.lazy work. Will look into this in the next few days. Since these featrues only works after 16.6, we also have to copy current enzyme-adapter-react-16 as enzyme-adapter-react-16.5 package right?

@chenesan for now, just implement it in the 16 adapter; we can iterate in that in the PR.

For the problem (2) in the PR #1975 comment , after some investigation I think we can just traverse the fiber tree to find all the LazyComponent fiber nodes. Every LazyComponent fiber node hold a elementType object, We can get the promise of dynamic import from elementType._result.

And in the ReactWrapper, I think we can have a new api for waiting lazy loading: updateUntilAllLazyComponentsLoaded(), the implementation may be like:

// helper function to find all lazy components under a fiber node

function findAllLazyNodes(fiberNode) {
  // will return array of LazyComponent fiber node
}

// inside mountRenderer returned by adapter

waitUntilAllLazyComponentsLoaded() {
  const lazyNodes = findAllLazyNodes(instance._reactInternalFiber)
  // the `_result` might be a promise(when pending), an error(when rejected), module(when resolved)
  // will handle all cases in implementaion, now assume there are all promises
  const promises = lazyNodes.map(node => node.elementType._result)
  await Promise.all(promises)
}

// inside ReactWrapper class
updateUntilAllLazyComponentsLoaded() {
  // check if adapter support this
  if (typeof this[RENDERER].waitUntilAllLazyComponentsLoaded() !== "function") {
    throw Error("Current adapter not support React.lazy")
  }
  // wait for lazy load, will handle rejected case in adapter
  await this[RENDERER].waitUntilAllLazyComponentsLoaded()
  // since all of lazy component loaded, force it update to rerender 
  this.update()
}

So we can test the rendering after React.lazy loaded:


const LazyComponent = lazy(() => import('/path/to/dynamic/component'));
const Fallback = () => <div />;
const SuspenseComponent = () => (
    <Suspense fallback={<Fallback />}>
      <LazyComponent />
    </Suspense>
);

const wrapper = mount(<SuspenseComponent />)
expect(wrapper.find('Fallback')).to.have.lengthOf(1)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(0)

await wrapper.waitUntilLazyLoaded()

expect(wrapper.find('Fallback')).to.have.lengthOf(0)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(1)

How do you think about the api @ljharb ? Or anyone could comment on this?

I think that since Suspense and lazy don’t actually work yet for their intended purpose (bundle splitting) that we don’t have to rush to invent a new API for it.

@ljharb do you have any info on why lazy doesn't work for code splitting? (I.e. issues etc). I know that Suspense isn't finished, but I thought lazy() worked.

Anyhow, I would love a release that just supported rendering the main path of the api so that using enzyme with react 16.6 could work - even though it didn't support testing all variants of Suspense. Maybe that should be the first target?

Indeed, that’s the first target :-)

@ljharb Using lazy() to do dynamic import should work. I think this would be useful for people want to test the rendering after dynamic module loaded. I'm sure it's doable to have an api for this, but I'm not sure how many people really need it. If it's not urgent then I can just add the Suspense and Lazy tag into enzyme to make sure the test in React^16.6 will work.

@ljharb Maybe I misunderstood the "rendering the main path of the api" in @tarjei 's comment. Do you mean that for now we should just support rendering the direct result of lazy() in initial mount (which will be the fallback passed to Suspense)?

@chenesan, @ljharb

I believe I have misunderstood what is supported in 16.6 :) What I tried to say was that it is quite painful to wait for enzyme to support lazy() and the other 16.6 functionality and I was hoping that could be prioritized before supporting all the general parts of the new Suspense component - i.e. the other usages than lazy loading. If it makes it easier I would think a first version that does not support fallback testing would also help.

That said, I'm very much a :+1: on waitUntilLazyLoaded() as that is something I have had to hack on to react-loadable.

Regards, and sorry for creating confusion.
Tarjei

@tarjei React.lazy only supports default exports for now. So if you're exporting named modules, those are not going to work unless you are also exporting them as default exports.

It also does not yet support server side rendering.

Thanks for your reply @tarjei !
I think there are two parts of work:

(1) make enzyme recognize the lazy and Suspense tag so it won't throw internal error unknown node with tag 13 and we can test the initial mount (which will render fallback for all lazy component under Suspense)

(2) support waiting lazy loading so we are able to test rendering after lazy loading done (like waitForLazyLoaded in previous comment).

I've worked on (1) in #1975 (need some polish and tests). After the discussion I might not do (2) for now since it looks not urgent. If someone needs this comment is welcome ;)

The notion of "waiting" for modules in a test environment doesn't quite make sense to me. So I added support for sync resolving of lazy() in https://github.com/facebook/react/pull/14626. In next version, if you give it a { then(cb) { cb(module) } } then it shouldn't suspend at all.

any update for a release (be it a major / minor release but stable one) date (tentative one would also do) for the fix of this issue.

@anushshukla no, see #1975.

@anushshukla

You can create a mock: __mocks__/react.js

import React from 'react';

const react = jest.requireActual('react');

const Suspense = ({ children }) => <div>{children}</div>;
Suspense.displayName = 'Suspense';

module.exports = { ...react, Suspense };

EDIT: This mock works for shallow renders, and does not work on mount.

FWIW, here is an updated version of @VincentLanglet's mock that also mocks React.lazy

import React from "react";

const react = jest.requireActual("react");

const Suspense = ({ children }) => <div>{children}</div>;
Suspense.displayName = "Suspense";

const lazy = React.lazy(() =>
  Promise.resolve().then(() => ({
    default() {
      return null;
    }
  }))
);
lazy.displayName = "lazy";

module.exports = { ...react, Suspense, lazy };

@thefivetoes When I use you lazy mock, I have no error with shallow but with mount, I get

Error: A React component suspended while rendering, but no fallback UI was specified.

Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.

Does it work for you ? This error seems to be normal since I mock the Suspense component...

@VincentLanglet you are correct, my bad – I am only doing shallow renders.

just wanted to thank you guys, @VincentLanglet and @thefivetoes, in writing, for the help.

Also wanted to share that in test cases of components who have nested components using Suspense + Lazy, you can do the following

  1. create __mocks__ in the same folders having such components
  2. create index.js in the __mocks__ folder in the step.1
  3. simply just mock such components in the test cases of components who use have such nested components
    (ref: https://stackoverflow.com/questions/44403165/using-jest-to-mock-a-react-component-with-props).

In my case,

AsyncComponent/index.js which uses Suspense + Lazy

AsyncComponent/__mocks__/index.js which is an alternate to Suspense + Lazy

In the failing test file, just added the below line,

jest.mock('path/to/AsyncComponent');

Also, we are utilising React test renderer other than enzyme and would suggest the same to others that not to rely on one testing utility while we all wish to have that ideal one but reality is this.

@thefivetoes did you do anything special to get that mock off the ground?

No matter what I do, I end up with the following exception:

    RangeError: Maximum call stack size exceeded
    > 1 | import React from "react";
        | ^
      2 | 
      3 | const react = jest.requireActual("react");
      4 | 

I tried with a custom webpack + babel config as well as ejecting from the latest create-react-app (plus bumping the react + enzyme versions).

EDIT:

Removing the initial import allows the mock to actually fire, but the mocked lazy implementation doesn't seem to match the actual lazy implementation.

I've got this error when tried to use new React context API

import React from 'react';
const PageContext = React.createContext('indexPage');

// later in parent component
<PageContext.Provider value={'page-name'}>
   <ChildComponent />
</PageContext.Provider>

// later in ChildComponent
<PageContext.Consumer>{pageName =>
   // do something with value of pageName var
}</PageContext.Consumer>
"enzyme-adapter-react-16": "^1.1.1"
"react": "^16.4.1",
"react-dom": "^16.4.1",
"enzyme": "^3.3.0",

@AnatoliyLitinskiy because there isn’t yet a released version of enzyme that supports it. The next one should.

This issue is about Suspense, not createContext, so please file a new issue if you still have questions.

v3.10.0 has now been released.

@thefivetoes How to make this work on mount? Can you please help?

Anyone got React.lazy to work without affecting coverage?

Screen Shot 2019-08-14 at 11 49 32 AM

While the test is able to assert that the component is being loaded, jest is reporting that the imported component is not being covered.

Not sure if @ljharb (Jordan) is still getting replies on this thread so I'll tag him directly (sorry, Jordan if you already are!) ❤️

@rodoabad i am, but haven't had time to get to all of my hundreds of notifications yet. please file a new issue, since code coverage of React.lazy isn't the same as overall Suspense support.

@shridharkalagi similarly, if you're still having an issue, please file a new issue.

Quoting https://github.com/enzymejs/enzyme/issues/1917#issuecomment-455690123

The notion of "waiting" for modules in a test environment doesn't quite make sense to me. So I added support for sync resolving of lazy() in facebook/react#14626. In next version, if you give it a { then(cb) { cb(module) } } then it shouldn't suspend at all.

@ljharb given that, do you think that https://github.com/airbnb/babel-plugin-dynamic-import-node could support sync thenable mode? That is, transforming import('foo') to { then(cb) { cb(require('foo')) } }

No, I don't - that would violate the spec (altho an older version of that transform did operate that way; you could always peg to that version).

Either way, forcing a babel transform so that enzyme works properly both won't work in browsers and doesn't seem ideal.

OK I see, I just wonder how can I actually benefit from https://github.com/facebook/react/pull/14626

I tried with 3.11.0 but getting an error when using mount with a lazy-loaded component in the tree

For coverage propose, I've done the following code

jest.mock('react', () => { const React = jest.requireActual('react'); return { ...React, Suspense: ({ children }) => <div>{children}</div>, lazy: (factory) => factory() } });

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andrewhl picture andrewhl  Â·  3Comments

blainekasten picture blainekasten  Â·  3Comments

abe903 picture abe903  Â·  3Comments

mattkauffman23 picture mattkauffman23  Â·  3Comments

thurt picture thurt  Â·  3Comments