__tests__/index.js
// @noflow
import path from 'path'
import React from 'react'
import renderer from 'react-test-renderer'
import universal from '../src'
import { flushModuleIds, flushChunkNames } from '../src/requireUniversalModule'
import {
createApp,
createDynamicApp,
createPath,
createBablePluginApp,
createDynamicBablePluginApp
} from '../__test-helpers__/createApp'
import {
normalizePath,
waitFor,
Loading,
Err,
MyComponent,
MyComponent2,
createComponent,
createDynamicComponent,
createBablePluginComponent,
createDynamicBablePluginComponent,
dynamicBabelNodeComponent,
createDynamicComponentAndOptions
} from '../__test-helpers__'
describe('async lifecycle', () => {
it('loading', async () => {
const asyncComponent = createComponent(400, MyComponent)
const Component = universal(asyncComponent)
const component1 = renderer.create(<Component />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(200)
expect(component1.toJSON()).toMatchSnapshot() // loading
await waitFor(200)
expect(component1.toJSON()).toMatchSnapshot() // loaded
const component2 = renderer.create(<Component />)
expect(component2.toJSON()).toMatchSnapshot() // re-loaded
})
it('error', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent)
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component.toJSON()).toMatchSnapshot() // errored
})
it('timeout error', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent, {
timeout: 10
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component.toJSON()).toMatchSnapshot() // error
})
it('component unmounted: setState not called', async () => {
const asyncComponent = createComponent(10, MyComponent)
const Component = universal(asyncComponent)
let instance
const component = renderer.create(<Component ref={i => (instance = i)} />)
instance.componentWillUnmount()
await waitFor(20)
// component will still be in loading state because setState is NOT called
// since its unmounted. In reality, it won't be rendered anymore.
expect(component.toJSON()).toMatchSnapshot() /*? component.toJSON() */
})
})
describe('props: all components receive props', () => {
it('custom loading component', async () => {
const asyncComponent = createComponent(40, MyComponent)
const Component = universal(asyncComponent, {
loading: Loading
})
const component1 = renderer.create(<Component prop='foo' />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loaded
const component2 = renderer.create(<Component prop='bar' />)
expect(component2.toJSON()).toMatchSnapshot() // re-loaded
})
it('custom error component', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent, {
error: Err
})
const component1 = renderer.create(<Component prop='foo' />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // Error!
const component2 = renderer.create(<Component prop='bar' />)
expect(component2.toJSON()).toMatchSnapshot() // loading again..
})
it('<MyUniversalComponent isLoading /> - also displays loading component', async () => {
const asyncComponent = createComponent(40, MyComponent)
const Component = universal(asyncComponent)
const component1 = renderer.create(<Component isLoading />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(50)
expect(component1.toJSON()).toMatchSnapshot() // loading even though async component is available
})
it('<MyUniversalComponent error={new Error} /> - also displays error component', async () => {
const asyncComponent = createComponent(40, MyComponent)
const Component = universal(asyncComponent, { error: Err })
const component1 = renderer.create(<Component error={new Error('ah')} />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(50)
expect(component1.toJSON()).toMatchSnapshot() // error even though async component is available
})
it('components passed as elements: loading', async () => {
const asyncComponent = createComponent(40, <MyComponent />)
const Component = universal(asyncComponent, {
loading: <Loading />
})
const component1 = renderer.create(<Component prop='foo' />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loaded
const component2 = renderer.create(<Component prop='bar' />)
expect(component2.toJSON()).toMatchSnapshot() // reload
})
it('components passed as elements: error', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent, {
error: <Err />
})
const component1 = renderer.create(<Component prop='foo' />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component1.toJSON()).toMatchSnapshot() // Error!
const component2 = renderer.create(<Component prop='bar' />)
expect(component2.toJSON()).toMatchSnapshot() // loading again...
})
it('arguments/props passed to asyncComponent function for data-fetching', async () => {
const asyncComponent = async (props, cb) => {
// this is what you would actually be doing here:
// const data = await fetch(`/path?key=${props.prop}`)
// const value = await data.json()
const value = props.prop
const component = await Promise.resolve(<div>{value}</div>)
return component
}
const Component = universal(asyncComponent, {
key: null
})
const component1 = renderer.create(<Component prop='foo' />)
expect(component1.toJSON()).toMatchSnapshot() // initial
await waitFor(10)
expect(component1.toJSON()).toMatchSnapshot() // loaded
})
})
describe('server-side rendering', () => {
it('es6: default export automatically resolved', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent, {
path: path.join(__dirname, '../__fixtures__/component')
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // serverside
})
it('es5: module.exports resolved', async () => {
const asyncComponent = createComponent(40, null)
const Component = universal(asyncComponent, {
path: path.join(__dirname, '../__fixtures__/component.es5')
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // serverside
})
})
describe('other options', () => {
it('key (string): resolves export to value of key', async () => {
const asyncComponent = createComponent(20, { fooKey: MyComponent })
const Component = universal(asyncComponent, {
key: 'fooKey'
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // initial
await waitFor(5)
expect(component.toJSON()).toMatchSnapshot() // loading
await waitFor(30)
expect(component.toJSON()).toMatchSnapshot() // success
})
it('key (function): resolves export to function return', async () => {
const asyncComponent = createComponent(20, { fooKey: MyComponent })
const Component = universal(asyncComponent, {
key: module => module.fooKey
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // initial
await waitFor(5)
expect(component.toJSON()).toMatchSnapshot() // loading
await waitFor(20)
expect(component.toJSON()).toMatchSnapshot() // success
})
it('onLoad (async): is called and passed an es6 module', async () => {
const onLoad = jest.fn()
const mod = { __esModule: true, default: MyComponent }
const asyncComponent = createComponent(40, mod)
const Component = universal(asyncComponent, { onLoad })
const component = renderer.create(<Component foo='bar' />)
await waitFor(50)
const info = { isServer: false, isSync: false }
const props = { foo: 'bar' }
expect(onLoad).toBeCalledWith(mod, info, props)
expect(component.toJSON()).toMatchSnapshot() // success
})
it('onLoad (async): is called and passed entire module', async () => {
const onLoad = jest.fn()
const mod = { __esModule: true, foo: MyComponent }
const asyncComponent = createComponent(40, mod)
const Component = universal(asyncComponent, {
onLoad,
key: 'foo'
})
const component = renderer.create(<Component />)
await waitFor(50)
const info = { isServer: false, isSync: false }
expect(onLoad).toBeCalledWith(mod, info, {})
expect(component.toJSON()).toMatchSnapshot() // success
})
it('onLoad (sync): is called and passed entire module', async () => {
const onLoad = jest.fn()
const asyncComponent = createComponent(40)
const Component = universal(asyncComponent, {
onLoad,
key: 'default',
path: path.join(__dirname, '..', '__fixtures__', 'component')
})
renderer.create(<Component />)
expect(onLoad).toBeCalledWith(
require('../__fixtures__/component'),
{
isServer: false,
isSync: true
},
{}
)
})
it('minDelay: loads for duration of minDelay even if component ready', async () => {
const asyncComponent = createComponent(40, MyComponent)
const Component = universal(asyncComponent, {
minDelay: 60
})
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // initial
await waitFor(45)
expect(component.toJSON()).toMatchSnapshot() // still loading
await waitFor(30)
expect(component.toJSON()).toMatchSnapshot() // loaded
})
})
describe('SSR flushing: flushModuleIds() + flushChunkNames()', () => {
it('babel', async () => {
const App = createApp()
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
})
it('babel (babel-plugin)', async () => {
const App = createBablePluginApp()
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames().map(normalizePath)
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['/component', '/component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames().map(normalizePath)
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['/component', '/component3'])
})
it('webpack', async () => {
global.__webpack_require__ = path => __webpack_modules__[path]
// modules stored by paths instead of IDs (replicates babel implementation)
global.__webpack_modules__ = {
[createPath('component')]: require(createPath('component')),
[createPath('component2')]: require(createPath('component2')),
[createPath('component3')]: require(createPath('component3'))
}
const App = createApp(true)
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
delete global.__webpack_require__
delete global.__webpack_modules__
})
it('webpack (babel-plugin)', async () => {
global.__webpack_require__ = path => __webpack_modules__[path]
// modules stored by paths instead of IDs (replicates babel implementation)
global.__webpack_modules__ = {
[createPath('component')]: require(createPath('component')),
[createPath('component2')]: require(createPath('component2')),
[createPath('component3')]: require(createPath('component3'))
}
const App = createBablePluginApp(true)
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames().map(normalizePath)
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['/component', '/component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames().map(normalizePath)
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['/component', '/component3'])
delete global.__webpack_require__
delete global.__webpack_modules__
})
it('babel: dynamic require', async () => {
const App = createDynamicApp()
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
})
it('webpack: dynamic require', async () => {
global.__webpack_require__ = path => __webpack_modules__[path]
// modules stored by paths instead of IDs (replicates babel implementation)
global.__webpack_modules__ = {
[createPath('component')]: require(createPath('component')),
[createPath('component2')]: require(createPath('component2')),
[createPath('component3')]: require(createPath('component3'))
}
const App = createDynamicApp(true)
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
delete global.__webpack_require__
delete global.__webpack_modules__
})
it('babel: dynamic require (babel-plugin)', async () => {
const App = createDynamicBablePluginApp()
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
})
it('webpack: dynamic require (babel-plugin)', async () => {
global.__webpack_require__ = path => __webpack_modules__[path]
// modules stored by paths instead of IDs (replicates babel implementation)
global.__webpack_modules__ = {
[createPath('component')]: require(createPath('component')),
[createPath('component2')]: require(createPath('component2')),
[createPath('component3')]: require(createPath('component3'))
}
const App = createDynamicBablePluginApp(true)
flushModuleIds() // insure sets are empty:
flushChunkNames()
renderer.create(<App one two three={false} />)
let paths = flushModuleIds().map(normalizePath)
let chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component2'])
expect(chunkNames).toEqual(['component', 'component2'])
renderer.create(<App one two={false} three />)
paths = flushModuleIds().map(normalizePath)
chunkNames = flushChunkNames()
expect(paths).toEqual(['/component', '/component3'])
expect(chunkNames).toEqual(['component', 'component3'])
delete global.__webpack_require__
delete global.__webpack_modules__
})
})
describe('advanced', () => {
it('dynamic requires (async)', async () => {
const components = { MyComponent }
const asyncComponent = createDynamicComponent(0, components)
const Component = universal(asyncComponent)
const component = renderer.create(<Component page='MyComponent' />)
await waitFor(5)
expect(component.toJSON()).toMatchSnapshot() // success
})
it('Component.preload: static preload method pre-fetches chunk', async () => {
const components = { MyComponent }
const asyncComponent = createDynamicComponent(40, components)
const Component = universal(asyncComponent)
Component.preload({ page: 'MyComponent' })
await waitFor(20)
const component1 = renderer.create(<Component />)
expect(component1.toJSON()).toMatchSnapshot() // still loading...
// without the preload, it still would be loading
await waitFor(22)
expect(component1.toJSON()).toMatchSnapshot() // success
const component2 = renderer.create(<Component />)
expect(component2.toJSON()).toMatchSnapshot() // success
})
it('Component.preload: static preload method hoists non-react statics', async () => {
// define a simple component with static properties
const FooComponent = props => (
<div>FooComponent {JSON.stringify(props)}</div>
)
FooComponent.propTypes = {}
FooComponent.nonReactStatic = { foo: 'bar' }
// prepare that component to be universally loaded
const components = { FooComponent }
const asyncComponent = createDynamicComponent(40, components)
const Component = universal(asyncComponent)
// wait for preload to finish
await Component.preload({ page: 'FooComponent' })
// assert desired static is available
expect(Component).not.toHaveProperty('propTypes')
expect(Component).toHaveProperty('nonReactStatic')
expect(Component.nonReactStatic).toBe(FooComponent.nonReactStatic)
})
it('Component.preload: static preload method on node', async () => {
const onLoad = jest.fn()
const onErr = jest.fn()
const opts = { testBabelPlugin: true }
const Component = universal(dynamicBabelNodeComponent, opts)
await Component.preload({ page: 'component' }).then(onLoad, onErr)
expect(onErr).not.toHaveBeenCalled()
const targetComponent = require(createPath('component')).default
expect(onLoad).toBeCalledWith(targetComponent)
})
it('promise passed directly', async () => {
const asyncComponent = createComponent(0, MyComponent, new Error('ah'))
const options = {
chunkName: ({ page }) => page,
error: ({ message }) => <div>{message}</div>
}
const Component = universal(asyncComponent(), options)
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
})
it('babel-plugin', async () => {
const asyncComponent = createBablePluginComponent(
0,
MyComponent,
new Error('ah'),
'MyComponent'
)
const options = {
testBabelPlugin: true,
chunkName: ({ page }) => page
}
const Component = universal(asyncComponent, options)
const component = renderer.create(<Component />)
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
})
it('componentWillReceiveProps: changes component (dynamic require)', async () => {
const components = { MyComponent, MyComponent2 }
const asyncComponent = createDynamicBablePluginComponent(0, components)
const options = {
testBabelPlugin: true,
chunkName: ({ page }) => page
}
const Component = universal(asyncComponent, options)
class Container extends React.Component {
render() {
const page = (this.state && this.state.page) || 'MyComponent'
return <Component page={page} />
}
}
let instance
const component = renderer.create(<Container ref={i => (instance = i)} />)
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
instance.setState({ page: 'MyComponent2' })
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
})
it('componentWillReceiveProps: changes component (dynamic require) (no babel plugin)', async () => {
const components = { MyComponent, MyComponent2 }
const { load, options } = createDynamicComponentAndOptions(0, components)
const Component = universal(load, options)
class Container extends React.Component {
render() {
const page = (this.state && this.state.page) || 'MyComponent'
return <Component page={page} />
}
}
let instance
const component = renderer.create(<Container ref={i => (instance = i)} />)
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
instance.setState({ page: 'MyComponent2' })
expect(component.toJSON()).toMatchSnapshot() // loading...
await waitFor(2)
expect(component.toJSON()).toMatchSnapshot() // loaded
})
})