packages/nerv/__tests__/component.spec.js

Summary

Maintainability
F
4 days
Test Coverage
/** @jsx createElement */
import {
  Component,
  createElement,
  render,
  cloneElement,
  PureComponent,
  findDOMNode,
  memo
} from '../src'
// import createVText from '../src/vdom/create-vtext'
import { rerender } from '../src/render-queue'
import sinon from 'sinon'
import {
  EMPTY_CHILDREN,
  getAttributes,
  sortAttributes,
  normalizeHTML
} from './util'

function fireEvent (on, type) {
  const e = document.createEvent('Event')
  e.initEvent(type, true, true)
  on.dispatchEvent(e)
}

describe('Component', function () {
  let scratch
  beforeAll(() => {
    scratch = document.createElement('div')
    document.body.appendChild(scratch)
  })

  beforeEach(() => {
    scratch = document.createElement('div')
    document.body.appendChild(scratch)
    // const c = scratch.firstElementChild
    // if (c) {
    //   render(<Empty />, scratch)
    // }
    // scratch.innerHTML = ''
  })

  afterAll(() => {
    scratch.parentNode.removeChild(scratch)
    scratch = null
  })

  it('should render components', () => {
    class C extends Component {
      render () {
        return <div>C</div>
      }
    }
    const spy = sinon.spy(C.prototype, 'render')
    render(<C />, scratch)

    expect(spy.calledOnce).toBeTruthy()
    expect(spy.returned(sinon.match({ type: 'div' }))).toBeTruthy()

    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>C</div>'))
  })

  it('call setState() in setState\'s callback', (done) => {
    const s1 = sinon.spy()
    const s2 = sinon.spy()
    const s3 = sinon.spy()
    class App extends Component {
      state = {
        a: false,
        b: false,
        c: false
      }

      componentDidMount () {
        this.setState(
          {
            a: true
          },
          () => {
            s1()
            this.setState(
              {
                b: true
              },
              () => {
                s2()
                expect(this.state.a).toBe(true)
                expect(this.state.b).toBe(true)
                expect(s1.called).toBeTruthy()
                expect(s2.called).toBeTruthy()
                expect(s2.calledAfter(s1)).toBeTruthy()
              }
            )

            this.setState({ c: true }, () => {
              s3()
              expect(this.state.a).toBe(true)
              expect(this.state.b).toBe(true)
              expect(this.state.c).toBe(true)
              expect(s1.called).toBeTruthy()
              expect(s2.called).toBeTruthy()
              expect(s3.calledAfter(s2)).toBeTruthy()
              done()
            })
          }
        )
      }
      render () {
        return <div />
      }
    }
    render(<App />, scratch)
  })

  it('is a react component', () => {
    class C extends Component {
      render () {
        return <div>C</div>
      }
    }
    let c
    render(<C ref={ins => (c = ins)} />, scratch)

    expect(!!Component.prototype.isReactComponent).toBeTruthy()
    expect(!!c.isReactComponent).toBeTruthy()
  })

  it('should render functional components', () => {
    const props = { foo: 'bar' }
    const C = sinon.spy(options => <div {...options} />)
    render(<C {...props} />, scratch)
    expect(C.calledOnce).toBeTruthy()
    expect(C.calledWithMatch(props)).toBeTruthy()
    expect(
      C.returned(
        sinon.match({
          type: 'div'
        })
      )
    ).toBeTruthy()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div foo="bar"></div>'))
  })

  it('should callback run once', () => {
    const C = <div />
    const f = sinon.spy()
    render(C, scratch, f)
    expect(f.calledOnce).toBeTruthy()
  })

  it('should update nested functional components', () => {
    const A = <div>A</div>
    const B = <div>B</div>

    class C extends Component {
      constructor () {
        super()
        this.state = {
          show: true
        }
      }

      render () {
        return <div>{this.state.show ? <A /> : <B />}</div>
      }
    }
    let c
    render(<C ref={ins => (c = ins)} />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div><div>A</div></div>'))

    c.setState({
      show: false
    })
    c.forceUpdate()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div><div>B</div></div>'))
  })

  it('should render components with props', () => {
    const props = { foo: 'bar', onBaz: () => {} }
    let constructorProps
    class C extends Component {
      constructor () {
        super(...arguments)
        constructorProps = props
      }
      render () {
        return <div {...this.props}>C</div>
      }
    }
    const spy = sinon.spy(C.prototype, 'render')
    render(<C {...props} />, scratch)
    expect(constructorProps).toBe(props)

    expect(spy.calledOnce).toBeTruthy()
    expect(spy.calledWithMatch()).toBeTruthy()
    expect(
      spy.returned(
        sinon.match({
          type: 'div'
        })
      )
    ).toBeTruthy()
    // .to.have.been.calledOnce
    // .and.to.have.been.calledWithMatch()
    // .and.to.have.returned(sinon.match({
    //   type: 'div'
    // }))

    expect(scratch.innerHTML).toEqual(normalizeHTML('<div foo="bar">C</div>'))
  })

  it('should clone components', () => {
    function Comp () {}
    const instance = <Comp />
    const clone = cloneElement(instance)
    expect(clone.prototype).toEqual(instance.prototype)
  })

  it('should clone children as well', () => {
    let inst
    class Child extends Component {
      render () {
        return (
          <div>
            {this.props.children}
            {this.props.children}
          </div>
        )
      }
    }

    class Daddy extends Component {
      constructor () {
        super(...arguments)
        this.state = {
          xxx: 'xxx'
        }
        inst = this
      }
      render () {
        const xxx = this.state.xxx
        return <Child>{xxx ? <div>{xxx}</div> : ''}</Child>
      }
    }

    expect(() => {
      render(<Daddy />, scratch)
      inst.setState({ xxx: false })
      inst.forceUpdate()
    }).not.toThrow()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div></div>'))
  })

  it('should remove children when root changes to text node', () => {
    let comp
    class Comp extends Component {
      render () {
        return this.state.alt ? 'asdf' : <div>test</div>
      }
    }

    render(
      <Comp
        ref={c => {
          comp = c
        }}
      />,
      scratch
    )
    comp.setState({ alt: true })
    comp.forceUpdate()
    expect(scratch.innerHTML).toEqual('asdf')
    comp.setState({ alt: false })
    comp.forceUpdate()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>test</div>'))

    comp.setState({ alt: true })
    comp.forceUpdate()
    expect(scratch.innerHTML).toEqual('asdf')
  })

  it('should not recycle common class children with different keys', () => {
    let scratch = document.createElement('div')
    const container = document.createElement('div')
    container.appendChild(scratch)
    document.body.appendChild(container)
    let idx = 0
    const msgs = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
    const sideEffect = sinon.spy()

    class Comp extends Component {
      componentWillMount () {
        this.innerMsg = msgs[idx++ % 8]
        sideEffect()
      }

      render () {
        return <div>{this.innerMsg}</div>
      }
    }
    const willMount = sinon.spy(Comp.prototype, 'componentWillMount')

    class GoodContainer extends Component {
      state = {
        alt: false
      }

      render () {
        const { alt } = this.state
        return (
          <div>
            {alt ? null : <Comp key={1} alt={alt} />}
            {alt ? null : <Comp key={2} alt={alt} />}
            {alt ? <Comp key={3} alt={alt} /> : null}
          </div>
        )
      }
    }

    class BadContainer extends Component {
      state = {
        alt: false
      }

      render () {
        const { alt } = this.state
        return (
          <div>
            {alt ? null : <Comp alt={alt} />}
            {alt ? null : <Comp alt={alt} />}
            {alt ? <Comp alt={alt} /> : null}
          </div>
        )
      }
    }

    let good
    let bad
    render(<GoodContainer ref={c => (good = c)} />, scratch)
    expect(scratch.textContent).toEqual('AB')
    expect(willMount.calledTwice).toBeTruthy()
    expect(sideEffect.calledTwice).toBeTruthy()

    sideEffect.reset()
    Comp.prototype.componentWillMount.reset()
    good.setState({ alt: true })
    good.forceUpdate()
    expect(scratch.textContent).toEqual('C')
    expect(willMount.calledOnce).toBeTruthy()
    expect(sideEffect.calledOnce).toBeTruthy()

    sideEffect.reset()
    Comp.prototype.componentWillMount.reset()
    scratch.innerHTML = ''
    scratch = document.createElement('div')
    render(<BadContainer ref={c => (bad = c)} />, scratch)
    expect(scratch.textContent).toEqual('DE')
    expect(willMount.calledTwice).toBeTruthy()
    expect(sideEffect.calledTwice).toBeTruthy()
    sideEffect.reset()
    Comp.prototype.componentWillMount.reset()
    bad.setState({ alt: true })
    bad.forceUpdate()
    expect(scratch.textContent).toEqual('F')
    expect(willMount.called).toBeTruthy()
    expect(sideEffect.called).toBeTruthy()
  })

  describe('defaultProps', () => {
    it('should apply default props on initial render', () => {
      class WithDefaultProps extends Component {
        constructor (props, context) {
          super(props, context)
          expect(props).toEqual({
            children: EMPTY_CHILDREN,
            fieldA: 1,
            fieldB: 2,
            fieldC: 1,
            fieldD: 2
          })
        }
        render () {
          return <div />
        }
      }
      WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }
      render(<WithDefaultProps fieldA={1} fieldB={2} fieldD={2} />, scratch)
    })

    it('should apply default props on rerender', () => {
      let doRender
      class Outer extends Component {
        constructor () {
          super()
          this.state = { i: 1 }
        }
        componentDidMount () {
          doRender = () => this.setState({ i: 2 })
        }
        render () {
          return (
            <WithDefaultProps
              fieldA={1}
              fieldB={this.state.i}
              fieldD={this.state.i}
            />
          )
        }
      }
      class WithDefaultProps extends Component {
        constructor (props, context) {
          super(props, context)
          this.ctor(props, context)
        }
        ctor () {}
        renderCall () {}
        componentWillReceiveProps () {}
        render () {
          this.renderCall(this.props)
          return <div />
        }
      }
      WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 }

      const proto = WithDefaultProps.prototype
      const ctor = sinon.spy(proto, 'ctor')
      const receiveProps = sinon.spy(proto, 'componentWillReceiveProps')
      const renderCall = sinon.spy(proto, 'renderCall')

      render(<Outer />, scratch)
      doRender()

      const PROPS1 = {
        fieldA: 1,
        fieldB: 1,
        fieldC: 1,
        fieldD: 1
      }

      const PROPS2 = {
        fieldA: 1,
        fieldB: 2,
        fieldC: 1,
        fieldD: 2
      }

      expect(ctor.calledWithMatch(PROPS1)).toBeTruthy()
      expect(renderCall.calledWithMatch(PROPS1)).toBeTruthy()

      rerender()

      expect(receiveProps.calledWithMatch(PROPS2)).toBeTruthy()
      expect(renderCall.calledWithMatch(PROPS2)).toBeTruthy()
    })

    it('should cache default props', () => {
      class WithDefaultProps extends Component {
        constructor (props, context) {
          super(props, context)
          expect(props).toEqual({
            children: EMPTY_CHILDREN,
            fieldA: 1,
            fieldB: 2,
            fieldC: 1,
            fieldD: 2,
            fieldX: 10
          })
        }
        render () {
          return <div />
        }
      }
      WithDefaultProps.defaultProps = {
        fieldA: 1,
        fieldB: 1,
        fieldC: 1,
        fieldD: 1
      }
      render(
        <div>
          <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
          <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
          <WithDefaultProps fieldB={2} fieldD={2} fieldX={10} />
        </div>,
        scratch
      )
    })

    it('(Component) defaultProps should respect null but ignore undefined', () => {
      class Text extends Component {
        render () {
          const { text } = this.props
          return <div>{text === null ? 'null' : text}</div>
        }
      }
      Text.defaultProps = {
        text: 'aaa'
      }
      const dom = render(
        <div>
          <Text text={null} /> <Text />
        </div>,
        scratch
      )
      expect(dom.firstChild.textContent).toEqual('null')
      expect(dom.lastChild.textContent).toEqual('aaa')
    })

    it('should not update when patch the same STC', () => {
      const App = () => <div />
      const app = <App />
      render(app, scratch)
      render(app, scratch)
      expect(scratch._component).toBe(app)
    })

    it('(StatelessComponent) defaultProps should respect null but ignore undefined', () => {
      const Text = ({ text }) => <div>{text === null ? 'null' : text}</div>
      Text.defaultProps = {
        text: 'aaa'
      }
      const dom = render(
        <div>
          <Text text={null} /> <Text />
        </div>,
        scratch
      )
      expect(dom.firstChild.textContent).toEqual('null')
      expect(dom.lastChild.textContent).toEqual('aaa')
    })

    it('(StatelessComponent) should support function returning null', () => {
      const FunctionReturningNull = () => null
      expect(() => {
        render(<FunctionReturningNull />, scratch)
      }).not.toThrow()
    })
  })

  describe('setState', () => {
    it('test setState async && batched', () => {
      class A extends Component {
        constructor (props) {
          super(props)
          this.state = {
            count: 1
          }
        }

        shouldComponentUpdate () {}

        componentWillUpdate () {}

        componentDidMount () {
          this.setState({
            count: this.state.count + 1
          })
          expect(this.state.count).toEqual(1)
          this.setState({
            count: this.state.count + 1
          })
          expect(this.state.count).toEqual(1)
        }

        render () {
          return <div>{this.state.count}</div>
        }
      }

      render(<A />, scratch)
      const spy = sinon.spy(A.prototype, 'componentWillUpdate')
      expect(spy.called).toBeFalsy()
    })

    it('test setState first param to be a function and setState callback', () => {
      let a = 1
      class A extends Component {
        constructor (props) {
          super(props)
          this.state = {
            count: 1
          }
        }
        shouldComponentUpdate () {
          return false
        }
        click () {
          this.setState(
            s => {
              s.count++
            },
            () => {
              a++
            }
          )

          this.setState(
            s => {
              s.count++
            },
            () => {
              a++
            }
          )
        }
        render () {
          return <div onClick={this.click.bind(this)}>{this.state.count}</div>
        }
      }
      render(<A />, scratch)
      const firstChild = scratch.childNodes[0]
      expect(firstChild.innerHTML).toEqual('1')
      fireEvent(firstChild, 'click')
      rerender()
      expect(firstChild.innerHTML).toEqual('1')
      expect(a).toEqual(3)
    })

    it('should set state while first arg is function', async () => {
      let inst
      class A extends Component {
        constructor (props) {
          super(props)
          this.state = {
            count: 1
          }
          inst = this
        }
        shouldComponentUpdate () {
          return false
        }
        click () {
          this.setState(() => {
            return { count: 2 }
          })
        }
        render () {
          return <div onClick={this.click.bind(this)}>{this.state.count}</div>
        }
      }

      render(<A />, scratch)
      const firstChild = scratch.childNodes[0]
      expect(firstChild.innerHTML).toEqual('1')
      fireEvent(firstChild, 'click')
      rerender()
      expect(inst.state.count).toBe(2)
    })
  })

  describe('forceUpdate', () => {
    it('should force a rerender', () => {
      let forceUpdate
      class ForceUpdateComponent extends Component {
        componentWillUpdate () {}
        componentDidMount () {
          forceUpdate = () => this.forceUpdate()
        }
        render () {
          return <div />
        }
      }
      const willUpdate = sinon.spy(
        ForceUpdateComponent.prototype,
        'componentWillUpdate'
      )
      const forceUpdateSpy = sinon.spy(
        ForceUpdateComponent.prototype,
        'forceUpdate'
      )
      render(<ForceUpdateComponent />, scratch)
      expect(willUpdate.called).toBeFalsy()

      forceUpdate()

      expect(willUpdate.called).toBeTruthy()
      expect(forceUpdateSpy.called).toBeTruthy()
    })

    it('should add callback to renderCallbacks', () => {
      let forceUpdate
      const callback = sinon.spy()
      class ForceUpdateComponent extends Component {
        componentDidMount () {
          forceUpdate = () => this.forceUpdate(callback)
        }
        render () {
          return <div />
        }
      }
      const spy = sinon.spy(ForceUpdateComponent.prototype, 'forceUpdate')
      render(<ForceUpdateComponent />, scratch)

      forceUpdate()

      expect(spy.called).toBeTruthy()
      expect(spy.calledWith(callback)).toBeTruthy()
      expect(callback.call).toBeTruthy()
    })
  })

  describe('props.children', () => {
    it('should support passing children as a prop', () => {
      const Foo = props => <div {...props} />

      render(
        <Foo a='b' children={[<span class='bar'>bar</span>, '123', 456]} />,
        scratch
      )

      expect(scratch.innerHTML).toEqual(
        normalizeHTML('<div a="b"><span class="bar">bar</span>123456</div>')
      )
    })

    it('should be ignored when explicit children exist', () => {
      const Foo = props => <div {...props}>a</div>
      render(<Foo children={'b'} />, scratch)
      expect(scratch.innerHTML).toEqual(normalizeHTML('<div>a</div>'))
    })

    it('should not be ignored when pass a empty children', () => {
      const Foo = props => <div>{props.children}</div>
      const Bar = props => <Foo {...props}>{'b'}</Foo>
      render(<Bar />, scratch)
      expect(scratch.innerHTML).toEqual(normalizeHTML('<div>b</div>'))
    })
  })

  describe('memo()', () => {
    it('should work with function components', () => {
      const spy = sinon.spy()

      function Foo () {
        spy()
        return <h1>Hello World</h1>
      }

      const Memoized = memo(Foo)

      let update
      class App extends Component {
        constructor () {
          super()
          update = () => this.setState({})
        }
        render () {
          return <Memoized />
        }
      }
      render(<App />, scratch)

      expect(spy.calledOnce).toBeTruthy()

      update()
      rerender()

      expect(spy.calledOnce).toBeTruthy()
    })

    it('should support custom comparer functions', () => {
      function Foo () {
        return <h1>Hello World</h1>
      }

      const spy = sinon.spy(() => true)
      const Memoized = memo(Foo, spy)

      let update
      class App extends Component {
        constructor () {
          super()
          update = () => this.setState({})
        }
        render () {
          return <Memoized />
        }
      }
      render(<App />, scratch)

      update()
      rerender()

      expect(spy.calledOnce).toBeTruthy()
      expect(spy.calledWith({ children: [] }, { children: [] })).toBeTruthy()
    })

    it('should rerender when custom comparer returns false', () => {
      const spy = sinon.spy()
      function Foo () {
        spy()
        return <h1>Hello World</h1>
      }

      const App = memo(Foo, () => false)
      render(<App />, scratch)
      expect(spy.calledOnce).toBeTruthy()

      render(<App foo='bar' />, scratch)
      expect(spy.calledTwice).toBeTruthy()
    })

    // it('should pass props and nextProps to comparer fn', () => {
    //   const spy = sinon.spy(() => false)
    //   function Foo() {
    //     return <div>foo</div>
    //   }

    //   const props = { foo: true };
    //   const nextProps = { foo: false };
    //   const App = memo(Foo, spy);
    //   render(h(App, props), scratch);
    //   render(h(App, nextProps), scratch);

    //   expect(spy).to.be.calledWith(props, nextProps);
    // })

    it('should nest without errors', () => {
      const Foo = () => <div>foo</div>
      const App = memo(memo(Foo))

      // eslint-disable-next-line prefer-arrow-callback
      expect(function () {
        render(<App />, scratch)
      }).not.toThrow()
    })
  })

  describe('PureComponent', () => {
    it('use PureComponent', () => {
      class App extends PureComponent {
        constructor (props) {
          super(props)
          this.state = {
            a: 7
          }
        }

        click () {
          this.setState({
            a: 7
          })
        }
        componentWillUpdate () {}
        render () {
          return <div onClick={this.click.bind(this)}>{this.state.a}</div>
        }
      }
      const spy = sinon.spy(App.prototype, 'componentWillUpdate')
      const s = render(<App />, scratch)
      expect(findDOMNode(s).innerHTML).toEqual('7')
      fireEvent(scratch.childNodes[0], 'click')
      rerender()
      expect(findDOMNode(s).innerHTML).toEqual('7')
      expect(spy.called).toBeFalsy()
    })

    it('render child PureComponent', () => {
      class C extends PureComponent {
        constructor (props) {
          super(props)
          this.state = {
            a: 7
          }
        }
        componentWillUpdate () {}
        render () {
          return <div>{this.state.a}</div>
        }
      }

      class App extends Component {
        render () {
          return <C />
        }
      }
      let c
      const spy = sinon.spy(C.prototype, 'componentWillUpdate')
      const s = render(<App ref={node => (c = node)} />, scratch)
      expect(findDOMNode(s).innerHTML).toEqual('7')
      c.setState({
        xx: 1
      })
      c.forceUpdate()
      expect(findDOMNode(s).innerHTML).toEqual('7')
      expect(spy.called).toBeFalsy()
    })
  })

  describe('High-Order Components', () => {
    it('should render nested functional components', () => {
      const PROPS = { foo: 'bar', onBaz: () => {} }

      const Outer = sinon.spy(props => <Inner {...props} />)

      const Inner = sinon.spy(props => <div {...props}>inner</div>)

      render(<Outer {...PROPS} />, scratch)

      expect(Outer.calledOnce).toBeTruthy()
      expect(Outer.calledWithMatch(PROPS)).toBeTruthy()
      expect(
        Outer.returned(
          sinon.match({
            type: Inner,
            props: PROPS
          })
        )
      ).toBeTruthy()
      expect(Inner.calledOnce).toBeTruthy()
      expect(Inner.calledWithMatch(PROPS)).toBeTruthy()
      expect(
        Inner.returned(
          sinon.match({
            type: 'div',
            // children: [createVText('inner')],
            props: sinon.match.has('foo')
          })
        )
      ).toBeTruthy()

      expect(scratch.innerHTML).toEqual(
        normalizeHTML('<div foo="bar">inner</div>')
      )
    })

    it('should re-render nested functional components', () => {
      let doRender = null
      class Outer extends Component {
        componentDidMount () {
          let i = 1
          doRender = () => this.setState({ i: ++i })
        }
        componentWillUnmount () {}
        render () {
          return <Inner i={this.state.i} {...this.props} />
        }
      }
      // const renderSpy = sinon.spy(Outer.prototype, 'render')
      const willMount = sinon.spy(Outer.prototype, 'componentWillUnmount')

      let j = 0
      const Inner = sinon.spy(props => (
        <div j={++j} {...props}>
          inner
        </div>
      ))

      render(<Outer foo='bar' />, scratch)

      doRender()
      rerender()

      expect(willMount.called).toBeFalsy()
      expect(Inner.calledTwice).toBeTruthy()

      expect(Inner.calledTwice).toBeTruthy()
      expect(
        Inner.secondCall.calledWithMatch({ foo: 'bar', i: 2 })
      ).toBeTruthy()
      expect(
        Inner.secondCall.returned(
          sinon.match({
            props: {
              j: 2,
              i: 2,
              foo: 'bar'
            }
          })
        )
      ).toBeTruthy()

      expect(getAttributes(scratch.firstElementChild)).toEqual({
        j: '2',
        i: '2',
        foo: 'bar'
      })

      doRender()
      rerender()

      expect(Inner.callCount).toBe(3)

      expect(Inner.thirdCall.calledWithMatch({ foo: 'bar', i: 3 })).toBeTruthy()
      expect(
        Inner.thirdCall.returned(
          sinon.match({
            props: {
              j: 3,
              i: 3,
              foo: 'bar'
            }
          })
        )
      ).toBeTruthy()

      expect(getAttributes(scratch.firstElementChild)).toEqual({
        j: '3',
        i: '3',
        foo: 'bar'
      })
    })

    it('should re-render nested components', () => {
      let doRender = null
      let alt = false

      class Outer extends Component {
        componentDidMount () {
          let i = 1
          doRender = () => this.setState({ i: ++i })
        }
        componentWillUnmount () {}
        render () {
          if (alt) return <div is-alt />
          return <Inner i={this.state.i} {...this.props} />
        }
      }
      // const outerRender = sinon.spy(Outer.prototype, 'render')
      const outterDidMount = sinon.spy(Outer.prototype, 'componentDidMount')
      const outerWillUnmount = sinon.spy(
        Outer.prototype,
        'componentWillUnmount'
      )

      let j = 0
      class Inner extends Component {
        constructor (...args) {
          super(...args)
          this._constructor(...args)
        }
        _constructor () {}
        componentWillMount () {}
        componentDidMount () {}
        componentWillUnmount () {}
        render () {
          return (
            <div j={++j} {...this.props}>
              inner
            </div>
          )
        }
      }
      const innerCtor = sinon.spy(Inner.prototype, '_constructor')
      const innerRender = sinon.spy(Inner.prototype, 'render')
      const innerWillMount = sinon.spy(Inner.prototype, 'componentWillMount')
      const innerDidMount = sinon.spy(Inner.prototype, 'componentDidMount')
      const innerWillUnmount = sinon.spy(
        Inner.prototype,
        'componentWillUnmount'
      )

      render(<Outer foo='bar' />, scratch)

      expect(outterDidMount.calledOnce).toBeTruthy()

      doRender()
      rerender()

      expect(outerWillUnmount.called).toBeFalsy()

      expect(innerCtor.calledOnce).toBeTruthy()
      expect(innerWillUnmount.called).toBeFalsy()
      expect(innerWillMount.calledOnce).toBeTruthy()
      expect(innerDidMount.calledOnce).toBeTruthy()
      expect(innerRender.calledTwice).toBeTruthy()

      expect(
        innerRender.secondCall.returned(
          sinon.match({
            props: {
              j: 2,
              i: 2,
              foo: 'bar'
            }
          })
        )
      )
      // .and.to.have.returned(sinon.match({
      //   props: {
      //     j: 2,
      //     i: 2,
      //     foo: 'bar'
      //   }
      // }))

      expect(getAttributes(scratch.firstElementChild)).toEqual({
        j: '2',
        i: '2',
        foo: 'bar'
      })

      expect(sortAttributes(scratch.innerHTML).toLowerCase()).toEqual(
        sortAttributes('<div foo="bar" j="2" i="2">inner</div>')
      )

      doRender()
      rerender()

      expect(innerWillUnmount.called).toBeFalsy()
      expect(innerWillMount.calledOnce).toBeTruthy()
      expect(innerDidMount.calledOnce).toBeTruthy()
      expect(innerRender.calledThrice).toBeTruthy()

      expect(
        innerRender.thirdCall.returned(
          sinon.match({
            props: {
              j: 3,
              i: 3,
              foo: 'bar'
            }
          })
        )
      ).toBeTruthy()
      // .and.to.have.returned(sinon.match({
      //   props: {
      //     j: 3,
      //     i: 3,
      //     foo: 'bar'
      //   }
      // }))

      expect(getAttributes(scratch.firstElementChild)).toEqual({
        j: '3',
        i: '3',
        foo: 'bar'
      })

      alt = true
      doRender()
      rerender()

      expect(innerWillUnmount.calledOnce).toBeTruthy()

      expect(scratch.innerHTML).toEqual(
        normalizeHTML('<div is-alt="true"></div>')
      )

      alt = false
      doRender()
      rerender()

      expect(sortAttributes(scratch.innerHTML).toLowerCase()).toEqual(
        sortAttributes('<div foo="bar" j="4" i="5">inner</div>')
      )
    })

    it('should resolve intermediary functional component', () => {
      const ctx = {}
      class Root extends Component {
        getChildContext () {
          return { ctx }
        }
        render () {
          return <Func />
        }
      }
      const Func = sinon.spy(() => <Inner />)
      class Inner extends Component {
        componentWillMount () {}
        componentDidMount () {}
        componentWillUnmount () {}
        render () {
          return <div>inner</div>
        }
      }
      const willMount = sinon.spy(Inner.prototype, 'componentWillMount')
      const didMount = sinon.spy(Inner.prototype, 'componentDidMount')
      render(<Root />, scratch)
      expect(willMount.calledOnce).toBeTruthy()
      expect(didMount.calledOnce).toBeTruthy()
      expect(willMount.calledBefore(didMount)).toBeTruthy()
    })

    it('should unmount children of high-order components without unmounting parent', () => {
      let outer
      let inner2
      let counter = 0

      class Outer extends Component {
        constructor (props, context) {
          super(props, context)
          outer = this
          this.state = {
            child: this.props.child
          }
        }
        componentWillUnmount () {}
        componentWillMount () {}
        componentDidMount () {}
        render () {
          const C = this.state.child
          return <C />
        }
      }

      // const outerRender = sinon.spy(Outer.prototype, 'render')
      const outerWillMount = sinon.spy(Outer.prototype, 'componentWillMount')
      const outerDidMount = sinon.spy(Outer.prototype, 'componentDidMount')
      const outerWillUnmount = sinon.spy(
        Outer.prototype,
        'componentWillUnmount'
      )

      class Inner extends Component {
        componentWillUnmount () {}
        componentWillMount () {}
        componentDidMount () {}
        render () {
          return createElement('element' + ++counter)
        }
      }

      // const innerRender = sinon.spy(Inner.prototype, 'render')
      const innerWillMount = sinon.spy(Inner.prototype, 'componentWillMount')
      const innerDidMount = sinon.spy(Inner.prototype, 'componentDidMount')
      const innerWillUnmount = sinon.spy(
        Inner.prototype,
        'componentWillUnmount'
      )

      class Inner2 extends Component {
        constructor (props, context) {
          super(props, context)
          inner2 = this
        }
        componentWillUnmount () {}
        componentWillMount () {}
        componentDidMount () {}
        render () {
          return createElement('element' + ++counter)
        }
      }

      const inner2Render = sinon.spy(Inner2.prototype, 'render')
      const inner2WillMount = sinon.spy(Inner2.prototype, 'componentWillMount')
      const inner2DidMount = sinon.spy(Inner2.prototype, 'componentDidMount')
      const inner2WillUnmount = sinon.spy(
        Inner2.prototype,
        'componentWillUnmount'
      )

      render(<Outer child={Inner} />, scratch)

      expect(outerWillMount.calledOnce).toBeTruthy()
      expect(outerDidMount.calledOnce).toBeTruthy()
      expect(outerWillUnmount.called).toBeFalsy()

      expect(innerWillMount.calledOnce).toBeTruthy()
      expect(innerDidMount.calledOnce).toBeTruthy()
      expect(innerWillUnmount.called).toBeFalsy()

      outer.setState({ child: Inner2 })
      outer.forceUpdate()

      expect(inner2Render.calledOnce).toBeTruthy()

      expect(outerWillMount.calledOnce).toBeTruthy()
      expect(outerDidMount.calledOnce).toBeTruthy()
      expect(outerWillUnmount.called).toBeFalsy()

      expect(inner2WillMount.calledOnce).toBeTruthy()
      expect(inner2DidMount.calledOnce).toBeTruthy()
      expect(inner2WillUnmount.called).toBeFalsy()

      inner2.forceUpdate()

      expect(inner2Render.calledTwice).toBeTruthy()
      expect(inner2WillMount.calledOnce).toBeTruthy()
      expect(inner2DidMount.calledOnce).toBeTruthy()
      expect(inner2WillUnmount.called).toBeFalsy()
    })

    it('should remount when swapping between HOC child types', () => {
      let doRender = null

      class Outer extends Component {
        constructor () {
          super(...arguments)
          this.state = {
            child: this.props.child
          }
        }

        componentDidMount () {
          doRender = () =>
            this.setState({
              child: <InnerFunc />
            })
        }

        render () {
          const Child = this.state.child
          return <Child />
        }
      }

      class Inner extends Component {
        componentWillMount () {}
        componentWillUnmount () {}
        render () {
          return <div class='inner'>foo</div>
        }
      }

      sinon.spy(Inner.prototype, 'render')
      const willMount = sinon.spy(Inner.prototype, 'componentWillMount')
      const willUnmount = sinon.spy(Inner.prototype, 'componentWillUnmount')

      const InnerFunc = () => <div class='inner-func'>bar</div>

      render(<Outer child={Inner} />, scratch)

      expect(willMount.calledOnce).toBeTruthy()
      expect(willUnmount.called).toBeFalsy()

      Inner.prototype.componentWillMount.reset()
      doRender()
      rerender()
      expect(willMount.called).toBeFalsy()
      expect(willUnmount.calledOnce).toBeTruthy()
    })
  })
})