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');
        });
      });
    });
  });
});