/* eslint-disable import/no-extraneous-dependencies, react/no-multi-comp */ import 'regenerator-runtime/runtime' import '@testing-library/jest-dom/extend-expect' import React from 'react' import { render, cleanup, wait } from '@testing-library/react' import loadable, { lazy } from './index' afterEach(cleanup) function createLoadFunction() { const ref = {} const fn = jest.fn(() => ref.promise) ref.promise = new Promise((resolve, reject) => { fn.resolve = resolve fn.reject = reject }) return fn } class Catch extends React.Component { state = { error: false } static getDerivedStateFromError() { return { error: true } } render() { return this.state.error ? 'error' : this.props.children } } describe('#loadable', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { // eslint-disable-next-line no-console console.error.mockRestore() }) it('renders nothing without a fallback', () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() expect(container).toBeEmpty() }) it('uses option fallback if specified', () => { const load = createLoadFunction() const Component = loadable(load, { fallback: 'progress' }) const { container } = render() expect(container).toHaveTextContent('progress') }) it('uses props fallback if specified', () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() expect(container).toHaveTextContent('progress') }) it('should use props fallback instead of option fallback if specified', () => { const load = createLoadFunction() const Component = loadable(load, { fallback: 'opt fallback' }) const { container } = render() expect(container).toHaveTextContent('prop fallback') }) it('mounts component when loaded', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() expect(container).toBeEmpty() load.resolve({ default: () => 'loaded' }) await wait(() => expect(container).toHaveTextContent('loaded')) }) it('supports preload', async () => { const load = createLoadFunction() const Component = loadable(load) expect(load).not.toHaveBeenCalled() Component.preload({ foo: 'bar' }) expect(load).toHaveBeenCalledWith({ foo: 'bar' }) expect(load).toHaveBeenCalledTimes(1) const { container } = render() expect(container).toBeEmpty() load.resolve({ default: () => 'loaded' }) await wait(() => expect(container).toHaveTextContent('loaded')) expect(load).toHaveBeenCalledTimes(2) }) it('supports non-default export', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() load.resolve(() => 'loaded') await wait(() => expect(container).toHaveTextContent('loaded')) }) it('forwards props', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() load.resolve({ default: ({ name }) => name }) await wait(() => expect(container).toHaveTextContent('James Bond')) }) it('should update component if props change', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render() load.resolve({ default: ({ value }) => value }) await wait(() => expect(container).toHaveTextContent('first')) render(, { container }) await wait(() => expect(container).toHaveTextContent('second')) expect(load).toHaveBeenCalledTimes(1) }) it('calls load func if cacheKey change', async () => { const load = createLoadFunction() const Component = loadable(load, { cacheKey: ({ value }) => value }) const { container } = render() load.resolve({ default: ({ value }) => value }) await wait(() => expect(container).toHaveTextContent('first')) expect(load).toHaveBeenCalledTimes(1) render(, { container }) await wait(() => expect(container).toHaveTextContent('second')) expect(load).toHaveBeenCalledTimes(2) }) it('calls load func if resolve change', async () => { const load = createLoadFunction() const Component = loadable({ requireAsync: load, resolve: ({ value }) => value, }) const { container } = render() load.resolve({ default: ({ value }) => value }) await wait(() => expect(container).toHaveTextContent('first')) expect(load).toHaveBeenCalledTimes(1) render(, { container }) await wait(() => expect(container).toHaveTextContent('second')) expect(load).toHaveBeenCalledTimes(2) }) it('does not call previous component if cacheKey change', async () => { const A = jest.fn(({ id }) => `A-${id}`) const B = jest.fn(({ id }) => `B-${id}`) const components = { A, B } const load = jest.fn(async ({ name }) => { const Component = components[name] return { default: Component } }) const Component = loadable(load, { cacheKey: ({ name }) => name }) const { container } = render() await wait(() => expect(container).toHaveTextContent('A-0')) expect(load).toHaveBeenCalledTimes(1) expect(load).toHaveBeenCalledWith({ name: 'A', id: 0 }) expect(A).toHaveBeenCalledTimes(1) expect(A).toHaveBeenCalledWith({ name: 'A', id: 0 }, {}) render(, { container }) await wait(() => expect(container).toHaveTextContent('A-1')) expect(A).toHaveBeenCalledTimes(2) expect(A).toHaveBeenCalledWith({ name: 'A', id: 1 }, {}) render(, { container }) await wait(() => expect(container).toHaveTextContent('B-2')) expect(load).toHaveBeenCalledTimes(2) expect(load).toHaveBeenCalledWith({ name: 'B', id: 2 }) expect(B).toHaveBeenCalledTimes(1) expect(B).toHaveBeenCalledWith({ name: 'B', id: 2 }, {}) // A should not have been rendered with "id: 2" expect(A).toHaveBeenCalledTimes(2) }) it('forwards ref', async () => { const load = createLoadFunction() const Component = loadable(load) const ref = React.createRef() render() load.resolve({ default: React.forwardRef((props, fref) =>
), }) await wait(() => expect(ref.current.tagName).toBe('DIV')) }) it('throws when an error occurs', async () => { const load = createLoadFunction() const Component = loadable(load) const { container } = render( , ) expect(container).toBeEmpty() load.reject(new Error('boom')) await wait(() => expect(container).toHaveTextContent('error')) }) }) describe('#lazy', () => { it('supports Suspense', async () => { const load = createLoadFunction() const Component = lazy(load) const { container } = render( , ) expect(container).toHaveTextContent('progress') load.resolve({ default: () => 'loaded' }) await wait(() => expect(container).toHaveTextContent('loaded')) }) }) describe('#loadable.lib', () => { it('loads library as render prop', async () => { const load = createLoadFunction() const Lib = loadable.lib(load) const renderFn = jest.fn(() => 'loaded') const { container } = render({renderFn}) expect(container).toBeEmpty() const library = { it: 'is', a: 'lib' } load.resolve(library) await wait(() => expect(container).toHaveTextContent('loaded')) expect(renderFn).toHaveBeenCalledWith(library) }) }) describe('#lazy.lib', () => { it('supports Suspense', async () => { const load = createLoadFunction() const Lib = lazy.lib(load) const renderFn = jest.fn(() => 'loaded') const { container } = render( {renderFn} , ) expect(container).toHaveTextContent('progress') const library = { it: 'is', a: 'lib' } load.resolve(library) await wait(() => expect(container).toHaveTextContent('loaded')) expect(container).toHaveTextContent('loaded') }) })