11

I have to components imported with the new React lazy API (16.6).

import React, {PureComponent, lazy} from 'react';

const Component1 = lazy(() => import('./Component1'));
const Component2 = lazy(() => import('./Component2'));

class CustomComponent extends PureComponent {
  ...
  render() {

  return (
    <div>
      <Component1 />
      <Component2 />
    </div>
  );
 }
}

In my tests, I'm doing the snapshots of this component. It's a very straightforward test:

import { create } from 'react-test-renderer';

const tree = await create(<CustomComponent />).toJSON();

expect(tree).toMatchSnapshot();

In the logs, the test is failing with this 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.

Do I have to wrap in every single test suite with <Suspense>...?

it('should show the component', async () => {
  const component = await create(
    <React.Suspense fallback={<div>loading</div>}>
     <CustomComponent /> 
    </React.Suspense> 
  ); 
  const tree = component.toJSON(); 

  expect(tree).toMatchSnapshot(); 

};

If I do that, I only see in the snapshot the fallback component.

+ Array [ + <div> + loading + </div>, + ]

So, which is the best way to do it?

Albert Olivé Corbella
  • 4,061
  • 7
  • 48
  • 66
  • Have you tried to call to `toJSON` after the `await` line. I assume that you call `toJSON` on the unresolved component and `await` gets this result instead of waiting for `CustomComponent` to be resolved. – Andreas Köberle Nov 07 '18 at 12:56
  • Hi @AndreasKöberle, I have just tried and it doesn't work. It's returning the fallback component in the snapshot: ```const component = await create( loading}> ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); ``` And the snapshot: ```+ Array [ +
    + loading +
    , + ]```
    – Albert Olivé Corbella Nov 07 '18 at 13:55
  • If I place a setTimeout it works, but this is not ideal... – Albert Olivé Corbella Nov 07 '18 at 14:05

5 Answers5

13

Do I have to wrap in every single test suite with <Suspense>?

Yes, the Suspense component is neccessary for lazily loading child components, particularly providing a fallback and for reconciliation when the lazy components are available.

Export Component1 and Component2 in CustomComponent so that they can be imported in tests.

import React, {PureComponent, lazy} from 'react';

export const Component1 = lazy(() => import('./Component1'));
export const Component2 = lazy(() => import('./Component2'));

export default class CustomComponent extends PureComponent {
  //...
}

Remember that the lazy loaded components are promise-like. Import them in the test, and wait for them to resolve before doing a check that the snapshot matches.

import { create } from 'react-test-renderer';
import React, {Suspense} from 'react';
import CustomComponent, {Component1, Component2} from './LazyComponent';

describe('CustomComponent', () => {
  it('rendered lazily', async()=> {
    const root = create(
      <Suspense fallback={<div>loading...</div>}>
        <CustomComponent/>
      </Suspense>
    );

    await Component1;
    await Component2;
    expect(root).toMatchSnapshot();
  })
})
Oluwafemi Sule
  • 36,144
  • 1
  • 56
  • 81
  • do you know any trick to handle this case for nested components? should we import all of them explicitly for being able to `await`? – skyboyer Nov 11 '18 at 14:21
  • 1
    I'm not sure of a use case for testing nested components that way; I guess importing them explicitly does it. Generally most components only care about one level down the component tree so that if you test one level up, it should be sufficient test for the component. – Oluwafemi Sule Nov 11 '18 at 14:36
  • yep, sure, `shallow` saves the day here – skyboyer Nov 11 '18 at 15:49
  • 5
    `lazy(() => import('./Component1'))` returns a react object component, so awaiting an object with `await Component1` does not work for me. It immediately resolves and does not give enough time for the lazy component to load. Suspense is still rendering the fallback property. Instead, I was able to `await import('./Component1')` and `await import('./Component2')`. I assume it's possible that the test suite could resolve importing the components before `react-test-renderer` does, but I have yet to run into that race condition. – Andrew Ferk Nov 15 '18 at 09:11
5

As per this comment in github, you can mock the lazy components with Jest to return the actual components instead, although you would need to move and export lazy statements to their own files for it to work.

// LazyComponent1.ts
import { lazy } from 'react';

export default lazy(() => import('./Component1'));
// CustomComponent.tsx
import React, { PureComponent } from 'react';
import Component1 from './LazyComponent1';
import Component2 from './LazyComponent2';

class CustomComponent extends PureComponent {
  ...
  render() {

  return (
    <div>
      <Component1 />
      <Component2 />
    </div>
  );
 }
}
// CustomComponent.spec.tsx
import React, { Suspense } from 'react';
import { create } from 'react-test-renderer';
import CustomComponent from './CustomComponent';

jest.mock('./LazyComponent1', () => require('./Component1'));
jest.mock('./LazyComponent2', () => require('./Component2'));

describe('CustomComponent', () => {
  it('should show the component', () => {
    const component = await create(
      <Suspense fallback={<div>loading</div>}>
       <CustomComponent /> 
      </Suspense> 
    ); 
    const tree = component.toJSON(); 

    expect(tree).toMatchSnapshot(); 
  });
});
RecuencoJones
  • 2,747
  • 3
  • 22
  • 21
3

With Enzyme and mount this worked for me. It does not require changing any exports.

// wait for lazy components
await import('./Component1')
await import('./Component2')

jest.runOnlyPendingTimers()
wrapper.update()

Thanks to Andrew Ferk's comment on the accepted answer.

Raine Revere
  • 30,985
  • 5
  • 40
  • 52
2

I had a similar problem where I wanted to do snapshot testing of nested components and one of them where lazy loaded. The nesting looked something like this:

SalesContainer -> SalesAreaCard -> SalesCard -> AreaMap

Where SalesContainer is the top component. The AreaMap-component is lazy loaded by SalesCard using React lazy and Suspense. The tests passed locally with AreaMap rendered in the snapshot for most developers. But the tests always failed miserably in Jenkins CI with the AreaMap never rendered. Flaky to say the least.

To make the tests pass I added the magic line await testRenderer.getInstance().loadingPromise; to the tests. This is an example of a test:

import React from 'react';
import renderer from 'react-test-renderer';
import wait from 'waait';
import SalesContainer from './index';

describe('<SalesContainer />', () => {
it('should render correctly', async () => {
    const testRenderer = renderer.create(
      <SalesContainer />
    );
    await wait(0);
    await testRenderer.getInstance().loadingPromise;
    expect(testRenderer).toMatchSnapshot();
  });
});
John P
  • 15,035
  • 4
  • 48
  • 56
0

In a way a combination of the two answers is the only thing that worked for me (using enzyme mount)

I had to use the export logic of the accepted answer and the await logic of Raine's answer

//use at own risk
await (Component1 as any)._ctor();
wrapper.update()

Raine's answer did not work on our build agent somehow, but worked on the dev environment

Gideon Mulder
  • 344
  • 3
  • 6