test.js (568 lines of code) (raw):

'use strict'; const {readdirSync} = require('fs'); const {join} = require('path'); const POLYFILLS = { cjs: require('./react-lifecycles-compat.cjs'), 'umd: dev': require('./react-lifecycles-compat'), 'umd: prod': require('./react-lifecycles-compat.min'), }; Object.entries(POLYFILLS).forEach(([name, module]) => { describe(`react-lifecycles-compat (${name})`, () => { readdirSync(join(__dirname, 'react')).forEach(version => { const basePath = `./react/${version}/node_modules/`; let createReactClass; let polyfill; let React; let ReactDOM; let ReactTestRenderer; beforeAll(() => { createReactClass = require(basePath + 'create-react-class'); polyfill = module.polyfill; React = require(basePath + 'react'); ReactDOM = require(basePath + 'react-dom'); try { ReactTestRenderer = require(basePath + 'react-test-renderer/shallow'); } catch (e) { ReactTestRenderer = require(basePath + 'react-addons-test-utils'); } }); describe(`react@${version}`, () => { beforeEach(() => { jest.spyOn(console, 'error'); global.console.error.mockImplementation(() => {}); }); afterEach(() => { global.console.error.mockRestore(); }); it('should initialize and update state correctly', () => { class ClassComponent extends React.Component { constructor(props) { super(props); this.state = {count: 1}; } static getDerivedStateFromProps(nextProps, prevState) { return { count: prevState.count + nextProps.incrementBy, }; } render() { return React.createElement('div', null, this.state.count); } } polyfill(ClassComponent); const container = document.createElement('div'); ReactDOM.render( React.createElement(ClassComponent, {incrementBy: 2}), container ); expect(container.textContent).toBe('3'); ReactDOM.render( React.createElement(ClassComponent, {incrementBy: 3}), container ); expect(container.textContent).toBe('6'); }); it('should support shallow rendering', () => { class ClassComponent extends React.Component { constructor(props) { super(props); this.state = {count: 1}; } static getDerivedStateFromProps(nextProps, prevState) { return { count: prevState.count + nextProps.incrementBy, }; } render() { return React.createElement('div', null, this.state.count); } } polyfill(ClassComponent); const testRenderer = ReactTestRenderer.createRenderer(); testRenderer.render( React.createElement(ClassComponent, {incrementBy: 2}) ); let result = testRenderer.getRenderOutput(); expect(result.props.children).toBe(3); testRenderer.render( React.createElement(ClassComponent, {incrementBy: 3}) ); result = testRenderer.getRenderOutput(); expect(result.props.children).toBe(6); }); it('should support create-react-class components', () => { let componentDidUpdateCalled = false; const CRCComponent = createReactClass({ statics: { getDerivedStateFromProps(nextProps, prevState) { return { count: prevState.count + nextProps.incrementBy, }; }, }, getInitialState() { return {count: 1}; }, getSnapshotBeforeUpdate(prevProps, prevState) { return prevState.count * 2 + this.state.count * 3; }, componentDidUpdate(prevProps, prevState, snapshot) { expect(prevProps).toEqual({incrementBy: 2}); expect(prevState).toEqual({count: 3}); expect(this.props).toEqual({incrementBy: 3}); expect(this.state).toEqual({count: 6}); expect(snapshot).toBe(24); componentDidUpdateCalled = true; }, render() { return React.createElement('div', null, this.state.count); }, }); polyfill(CRCComponent); const container = document.createElement('div'); ReactDOM.render( React.createElement(CRCComponent, {incrementBy: 2}), container ); expect(container.textContent).toBe('3'); expect(componentDidUpdateCalled).toBe(false); ReactDOM.render( React.createElement(CRCComponent, {incrementBy: 3}), container ); expect(container.textContent).toBe('6'); expect(componentDidUpdateCalled).toBe(true); }); it('should support class components', () => { let componentDidUpdateCalled = false; class Component extends React.Component { constructor(props) { super(props); this.state = {count: 1}; this.setRef = ref => { this.divRef = ref; }; } static getDerivedStateFromProps(nextProps, prevState) { return { count: prevState.count + nextProps.incrementBy, }; } getSnapshotBeforeUpdate(prevProps, prevState) { expect(prevProps).toEqual({incrementBy: 2}); expect(prevState).toEqual({count: 3}); return this.divRef.textContent; } componentDidUpdate(prevProps, prevState, snapshot) { expect(prevProps).toEqual({incrementBy: 2}); expect(prevState).toEqual({count: 3}); expect(this.props).toEqual({incrementBy: 3}); expect(this.state).toEqual({count: 6}); expect(snapshot).toBe('3'); componentDidUpdateCalled = true; } render() { return React.createElement( 'div', { ref: this.setRef, }, this.state.count ); } } polyfill(Component); const container = document.createElement('div'); ReactDOM.render( React.createElement(Component, {incrementBy: 2}), container ); expect(container.textContent).toBe('3'); expect(componentDidUpdateCalled).toBe(false); ReactDOM.render( React.createElement(Component, {incrementBy: 3}), container ); expect(container.textContent).toBe('6'); expect(componentDidUpdateCalled).toBe(true); }); it('should support getDerivedStateFromProps in subclass', () => { class BaseClass extends React.Component { constructor(props) { super(props); this.state = {}; } static getDerivedStateFromProps(nextProps, prevState) { return { foo: 'foo', }; } render() { return null; } } polyfill(BaseClass); class SubClass extends BaseClass { static getDerivedStateFromProps(nextProps, prevState) { return { ...BaseClass.getDerivedStateFromProps(nextProps, prevState), bar: 'bar', }; } render() { return React.createElement( 'div', null, this.state.foo + ',' + this.state.bar ); } } const container = document.createElement('div'); ReactDOM.render(React.createElement(SubClass), container); expect(container.textContent).toBe('foo,bar'); }); it('should properly recover from errors thrown by getSnapshotBeforeUpdate()', () => { let instance; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = {error: null}; } componentDidCatch(error) { this.setState({error}); } render() { return this.state.error !== null ? null : this.props.children; } } class Component extends React.Component { constructor(props) { super(props); this.state = {count: 1}; } static getDerivedStateFromProps(nextProps, prevState) { return { count: prevState.count + nextProps.incrementBy, }; } getSnapshotBeforeUpdate(prevProps) { throw Error('whoops'); } componentDidUpdate(prevProps, prevState, snapshot) {} render() { instance = this; return null; } } polyfill(Component); const container = document.createElement('div'); ReactDOM.render( React.createElement( ErrorBoundary, null, React.createElement(Component, {incrementBy: 2}) ), container ); try { ReactDOM.render( React.createElement( ErrorBoundary, null, React.createElement(Component, {incrementBy: 3}) ), container ); } catch (error) {} // Verify that props and state get reset after the error // Note that the polyfilled and real versions necessarily differ, // Because one is run during the "render" phase and the other during "commit". if (parseFloat(version) < '16.3') { expect(instance.props.incrementBy).toBe(2); expect(instance.state.count).toBe(3); } else { expect(instance.props.incrementBy).toBe(3); expect(instance.state.count).toBe(6); } }); it('should properly handle falsy return values from getSnapshotBeforeUpdate()', () => { let componentDidUpdateCalls = 0; class Component extends React.Component { getSnapshotBeforeUpdate(prevProps) { return prevProps.value; } componentDidUpdate(prevProps, prevState, snapshot) { expect(snapshot).toBe(prevProps.value); componentDidUpdateCalls++; } render() { return null; } } polyfill(Component); const container = document.createElement('div'); ReactDOM.render( React.createElement(Component, {value: 'initial'}), container ); expect(componentDidUpdateCalls).toBe(0); ReactDOM.render( React.createElement(Component, {value: null}), container ); expect(componentDidUpdateCalls).toBe(1); ReactDOM.render( React.createElement(Component, {value: false}), container ); expect(componentDidUpdateCalls).toBe(2); ReactDOM.render( React.createElement(Component, {value: undefined}), container ); expect(componentDidUpdateCalls).toBe(3); ReactDOM.render( React.createElement(Component, {value: 123}), container ); expect(componentDidUpdateCalls).toBe(4); }); it('should error for non-class components', () => { function FunctionalComponent() { return null; } expect(() => polyfill(FunctionalComponent)).toThrow( 'Can only polyfill class components' ); }); it('should ignore components with old lifecycles if they do not define new ones', () => { class ComponentWithLifecycles extends React.Component { componentWillMount() {} componentWillReceiveProps() {} componentWillUpdate() {} render() { return null; } } polyfill(ComponentWithLifecycles); class ComponentWithUnsafeLifecycles extends React.Component { UNSAFE_componentWillMount() {} UNSAFE_componentWillReceiveProps() {} UNSAFE_componentWillUpdate() {} render() { return null; } } polyfill(ComponentWithUnsafeLifecycles); }); it('should error if component tries to combine gDSFP with any of the old API lifecycles', () => { class ComponentWithWillMount extends React.Component { componentWillMount() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithWillMount)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' componentWillMount' ); class ComponentWithWillReceiveProps extends React.Component { componentWillReceiveProps() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithWillReceiveProps)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' componentWillReceiveProps' ); class ComponentWithWillUpdate extends React.Component { componentWillUpdate() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithWillUpdate)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' componentWillUpdate' ); class ComponentWithUnsafeWillMount extends React.Component { UNSAFE_componentWillMount() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillMount)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillMount' ); class ComponentWithUnsafeWillReceiveProps extends React.Component { UNSAFE_componentWillReceiveProps() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillReceiveProps)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillReceiveProps' ); class ComponentWithUnsafeWillUpdate extends React.Component { UNSAFE_componentWillUpdate() {} static getDerivedStateFromProps() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillUpdate)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillUpdate' ); }); it('should error if component tries to combine gSBU with any of the old API lifecycles', () => { class ComponentWithWillMount extends React.Component { componentWillMount() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithWillMount)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillMount uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' componentWillMount' ); class ComponentWithWillReceiveProps extends React.Component { componentWillReceiveProps() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithWillReceiveProps)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' componentWillReceiveProps' ); class ComponentWithWillUpdate extends React.Component { componentWillUpdate() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithWillUpdate)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithWillUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' componentWillUpdate' ); class ComponentWithUnsafeWillMount extends React.Component { UNSAFE_componentWillMount() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillMount)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillMount uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillMount' ); class ComponentWithUnsafeWillReceiveProps extends React.Component { UNSAFE_componentWillReceiveProps() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillReceiveProps)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillReceiveProps' ); class ComponentWithUnsafeWillUpdate extends React.Component { UNSAFE_componentWillUpdate() {} getSnapshotBeforeUpdate() {} render() { return null; } } expect(() => polyfill(ComponentWithUnsafeWillUpdate)).toThrow( 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'ComponentWithUnsafeWillUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillUpdate' ); }); it('should error if component defines gSBU but does not define cDU', () => { class Component extends React.Component { getSnapshotBeforeUpdate(prevProps, prevState) {} render() { return null; } } expect(() => polyfill(Component)).toThrow( 'Cannot polyfill getSnapshotBeforeUpdate() for components that do not define componentDidUpdate() on the prototype' ); }); it('should not pass stale state to a setState updater function when parent component also re-renders as part of a batched update', () => { class ParentComponent extends React.Component { constructor(props) { super(props); this.state = {}; this.updateState = this.updateState.bind(this); } updateState() { this.setState({}); } render() { return React.createElement(PolyfilledComponent, { parentCallback: this.updateState, }); } } let instance; class PolyfilledComponent extends React.Component { constructor(props) { super(props); this.state = {flag: true}; this.handleClick = this.handleClick.bind(this); } static getDerivedStateFromProps(nextProps, prevState) { return prevState; } handleClick() { this.setState(function(prevState) { return {flag: !prevState.flag}; }); this.props.parentCallback(); } render() { instance = this; return React.createElement( 'button', {id: 'button', onClick: this.handleClick}, String(this.state.flag) ); } } polyfill(PolyfilledComponent); const container = document.createElement('div'); ReactDOM.render(React.createElement(ParentComponent), container); const button = container.firstChild; expect(container.textContent).toBe('true'); ReactDOM.unstable_batchedUpdates(instance.handleClick); // Simulate click expect(container.textContent).toBe('false'); ReactDOM.unstable_batchedUpdates(instance.handleClick); // Simulate click expect(container.textContent).toBe('true'); }); }); }); }); });