packages/nerv/__tests__/fragments.spec.js

Summary

Maintainability
F
3 wks
Test Coverage
/** @jsx createElement */
import { Component, createElement, render, Fragment } from '../src/index'
import { rerender } from '../src/render-queue'
import { span, div, ul, ol, li, section } from './util/dom'
import { normalizeHTML } from './util'

const isNode = !!(
  typeof process !== 'undefined' &&
  process.versions &&
  process.versions.node
)

if (isNode) describe.skipKarma = describe

describe.skipKarma('fragments', () => {
  let scratch

  beforeEach(() => {
    scratch = document.createElement('div')
    ops = []
  })

  let ops = []

  class Stateful extends Component {
    componentDidUpdate () {
      ops.push('Update Stateful')
    }
    render () {
      return <div>Hello</div>
    }
  }

  it('should not render empty Fragment', () => {
    render(<Fragment />, scratch)
    expect(scratch.innerHTML).toEqual('')
  })

  it('should render a single child', () => {
    render(
      <Fragment>
        <span>foo</span>
      </Fragment>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML('<span>foo</span>'))
  })

  it('should render multiple children via noop renderer', () => {
    render(
      <Fragment>
        hello <span>world</span>
      </Fragment>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML('hello <span>world</span>'))
  })

  it('should not crash with null as last child', () => {
    const fn = () => {
      render(
        <Fragment>
          <span>world</span>
          {null}
        </Fragment>
        ,
        scratch
      )
    }
    expect(fn).not.toThrow()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span>world</span>'))
    render(
      <Fragment>
        <span>world</span>
        <p>Hello</p>
      </Fragment>,
      scratch
    )
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span>world</span><p>Hello</p>'))

    expect(fn).not.toThrow()
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span>world</span>'))

    render(
      <Fragment>
        <span>world</span>
        {null}
        <span>world</span>
      </Fragment>,
      scratch
    )
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span>world</span><span>world</span>'))

    render(
      <Fragment>
        <span>world</span>
        Hello
        <span>world</span>
      </Fragment>,
      scratch
    )
    expect(scratch.innerHTML).toEqual(
      normalizeHTML('<span>world</span>Hello<span>world</span>')
    )
  })

  it('should handle reordering components that return Fragments #1325', () => {
    class X extends Component {
      render () {
        return <Fragment>{this.props.children}</Fragment>
      }
    }

    class App extends Component {
      render (props) {
        if (this.props.i === 0) {
          return (
            <div>
              <X key={1}>1</X>
              <X key={2}>2</X>
            </div>
          )
        }
        return (
          <div>
            <X key={2}>2</X>
            <X key={1}>1</X>
          </div>
        )
      }
    }

    render(<App i={0} />, scratch)
    expect(scratch.textContent).toEqual('12')
    render(<App i={1} />, scratch)
    expect(scratch.textContent).toEqual('21')
  })

  it('should patch empty fragment', () => {
    class X extends Component {
      render () {
        return <Fragment>{this.props.children}</Fragment>
      }
    }

    render(<X />, scratch)
    expect(scratch.textContent).toEqual('')
    render(<div>21</div>, scratch)
    expect(scratch.textContent).toEqual('21')
    render(<Fragment />, scratch)
    expect(scratch.textContent).toEqual('')
  })

  it('should handle changing node type within a Component that returns a Fragment #1326', () => {
    class X extends Component {
      render () {
        return this.props.children
      }
    }

    /** @type {(newState: any) => void} */
    let setState
    class App extends Component {
      constructor (props, context) {
        super(props, context)

        this.state = { i: 0 }
        setState = this.setState.bind(this)
      }

      render () {
        if (this.state.i === 0) {
          return (
            <div>
              <X>
                <span>1</span>
              </X>
              <X>
                <span>2</span>
                <span>2</span>
              </X>
            </div>
          )
        }

        return (
          <div>
            <X>
              <div>1</div>
            </X>
            <X>
              <span>2</span>
              <span>2</span>
            </X>
          </div>
        )
      }
    }

    render(<App />, scratch)
    expect(scratch.innerHTML).toEqual(
      div([span(1), span(2), span(2)].join(''))
    )

    setState({ i: 1 })

    rerender()

    expect(scratch.innerHTML).toEqual(normalizeHTML(div([div(1), span(2), span(2)].join(''))))
  })

  it('should preserve state of children with 1 level nesting', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Stateful key='a' />
      ) : (
        <Fragment>
          <Stateful key='a' />
          <div key='b'>World</div>
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div><div>World</div>'))

    render(<Foo condition />, scratch)

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

  it('should preserve state between top-level fragments', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment>
          <Stateful />
        </Fragment>
      ) : (
        <Fragment>
          <Stateful />
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual(['Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should preserve state of children nested at same level', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment>
          <Stateful key='a' />
        </Fragment>
      ) : (
        <Fragment>
          <div />
          <Stateful key='a' />
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

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

    render(<Foo condition />, scratch)

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

  it('should not preserve state in non-top-level fragment nesting', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment>
          <Fragment>
            <Stateful key='a' />
          </Fragment>
        </Fragment>
      ) : (
        <Fragment>
          <Stateful key='a' />
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should not preserve state of children if nested 2 levels without siblings', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Stateful key='a' />
      ) : (
        <Fragment>
          <Fragment>
            <Stateful key='a' />
          </Fragment>
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should just render children for fragments', () => {
    class Comp extends Component {
      render () {
        return (
          <Fragment>
            <div>Child1</div>
            <div>Child2</div>
          </Fragment>
        )
      }
    }

    render(<Comp />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Child1</div><div>Child2</div>'))
  })

  it.skip('should not preserve state of children if nested 2 levels with siblings', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Stateful key='a' />
      ) : (
        <Fragment>
          <Fragment>
            <Stateful key='a' />
          </Fragment>
          <div />
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div><div></div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should preserve state between array nested in fragment and fragment', () => {
    // In this test case, the children of the Fragment in Foo end up being the same when flatened.
    //
    // When condition == true, the children of the Fragment are a Stateful VNode.
    // When condition == false, the children of the Fragment are an Array containing a single
    // Stateful VNode.
    //
    // However, when each of these are flattened (in flattenChildren), they both become
    // an Array containing a single Stateful VNode. So when diff'ed they are compared together
    // and the state of Stateful is preserved

    function Foo ({ condition }) {
      return condition ? (
        <Fragment>
          <Stateful key='a' />
        </Fragment>
      ) : (
        <Fragment>{[<Stateful key='a' />]}</Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    // expect(ops).toEqual(['Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    // expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML(('<div>Hello</div>')))
  })

  it('should preserve state between top level fragment and array', () => {
    function Foo ({ condition }) {
      return condition ? (
        [<Stateful key='a' />]
      ) : (
        <Fragment>
          <Stateful key='a' />
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    // expect(ops).toEqual(['Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    // expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should not preserve state between array nested in fragment and double nested fragment', () => {
    // In this test case, the children of the Fragment in Foo end up being the different when flatened.
    //
    // When condition == true, the children of the Fragment are an Array of Stateful VNode.
    // When condition == false, the children of the Fragment are another Fragment whose children are
    // a single Stateful VNode.
    //
    // When each of these are flattened (in flattenChildren), the first Fragment stays the same
    // (Fragment -> [Stateful]). The second Fragment also doesn't change (flatenning doesn't erase
    // Fragments) so it remains Fragment -> Fragment -> Stateful. Therefore when diff'ed these Fragments
    // separate the two Stateful VNodes into different trees and state is not preserved between them.

    function Foo ({ condition }) {
      return condition ? (
        <Fragment>{[<Stateful key='a' />]}</Fragment>
      ) : (
        <Fragment>
          <Fragment>
            <Stateful key='a' />
          </Fragment>
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it.skip('should not preserve state between array nested in fragment and double nested array', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment>{[<Stateful key='a' />]}</Fragment>
      ) : (
        [[<Stateful key='a' />]]
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it.skip('should preserve state between double nested fragment and double nested array', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment>
          <Fragment>
            <Stateful key='a' />
          </Fragment>
        </Fragment>
      ) : (
        [[<Stateful key='a' />]]
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual(['Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should not preserve state of children when the keys are different', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment key='a'>
          <Stateful />
        </Fragment>
      ) : (
        <Fragment key='b'>
          <Stateful />
          <span>World</span>
        </Fragment>
      )
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div><span>World</span>'))
    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should not preserve state between unkeyed and keyed fragment', () => {
    function Foo ({ condition }) {
      return condition ? (
        <Fragment key='a'>
          <Stateful />
        </Fragment>
      ) : (
        <Fragment>
          <Stateful />
        </Fragment>
      )
    }

    // React & Preact: has the same behavior for components
    // https://codesandbox.io/s/57prmy5mx
    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<div>Hello</div>'))
  })

  it('should preserve state with reordering in multiple levels', () => {
    function Foo ({ condition }) {
      return condition ? (
        <div>
          <Fragment key='c'>
            <div>foo</div>
            <div key='b'>
              <Stateful key='a' />
            </div>
          </Fragment>
          <div>boop</div>
        </div>
      ) : (
        <div>
          <div>beep</div>
          <Fragment key='c'>
            <div key='b'>
              <Stateful key='a' />
            </div>
            <div>bar</div>
          </Fragment>
        </div>
      )
    }

    const htmlForTrue = div(
      [div('foo'), div(div('Hello')), div('boop')].join('')
    )

    const htmlForFalse = div(
      [div('beep'), div(div('Hello')), div('bar')].join('')
    )

    render(<Foo condition />, scratch)

    expect(scratch.innerHTML).toEqual(normalizeHTML(htmlForTrue))

    render(<Foo condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(normalizeHTML(htmlForFalse))

    render(<Foo condition />, scratch)

    // expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML(htmlForTrue))
  })

  it('should not preserve state when switching to a keyed fragment to an array', () => {
    function Foo ({ condition }) {
      return condition ? (
        <div>
          {
            <Fragment key='foo'>
              <span>1</span>
              <Stateful />
            </Fragment>
          }
          <span>2</span>
        </div>
      ) : (
        <div>
          {[<span>1</span>, <Stateful />]}
          <span>2</span>
        </div>
      )
    }

    const html = div([span('1'), div('Hello'), span('2')].join(''))

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML(html))

    render(<Foo condition />, scratch)

    expect(ops).toEqual([])
    expect(scratch.innerHTML).toEqual(normalizeHTML(html))
  })

  it('should preserve state when it does not change positions', () => {
    function Foo ({ condition }) {
      return condition
        ? [
          <span />,
          <Fragment>
            <Stateful />
          </Fragment>
        ]
        : [
          <span />,
          <Fragment>
            <Stateful />
          </Fragment>
        ]
    }

    render(<Foo condition />, scratch)
    render(<Foo condition={false} />, scratch)

    expect(ops).toEqual(['Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span></span><div>Hello</div>'))

    render(<Foo condition />, scratch)

    expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(normalizeHTML('<span></span><div>Hello</div>'))
  })

  it('should render nested Fragments', () => {
    render(
      <Fragment>
        spam
        <Fragment>foo</Fragment>
        <Fragment />
        bar
      </Fragment>,
      scratch
    )

    expect(scratch.innerHTML).toEqual('spamfoobar')

    render(
      <Fragment>
        <Fragment>foo</Fragment>
        <Fragment>bar</Fragment>
      </Fragment>,
      scratch
    )

    expect(scratch.innerHTML).toEqual('foobar')
  })

  it('should render nested Fragments with siblings', () => {
    render(
      <div>
        <div>0</div>
        <div>1</div>
        <Fragment>
          <Fragment>
            <div>2</div>
            <div>3</div>
          </Fragment>
        </Fragment>
        <div>4</div>
        <div>5</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(div([div(0), div(1), div(2), div(3), div(4), div(5)].join('')))
    )
  })

  it('should respect keyed Fragments', () => {
    /** @type {() => void} */
    let update

    class Comp extends Component {
      constructor () {
        super()
        this.state = { key: 'foo' }
        update = () => this.setState({ key: 'bar' })
      }

      render () {
        return <Fragment key={this.state.key}>foo</Fragment>
      }
    }
    render(<Comp />, scratch)
    expect(scratch.innerHTML).toEqual('foo')

    update()
    rerender()

    expect(scratch.innerHTML).toEqual('foo')
  })

  it('should support conditionally rendered children', () => {
    /** @type {() => void} */
    let update

    class Comp extends Component {
      constructor () {
        super()
        this.state = { value: true }
        update = () => this.setState({ value: !this.state.value })
      }

      render () {
        return (
          <Fragment>
            <span>0</span>
            {this.state.value && 'foo'}
            <span>1</span>
          </Fragment>
        )
      }
    }

    const html = contents => span('0') + contents + span('1')

    render(<Comp />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(html('foo')))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(normalizeHTML(html('')))

    update()
    rerender()
    expect(scratch.innerHTML).toEqual(normalizeHTML(html('foo')))
  })

  it('can modify the children of a Fragment', () => {
    /** @type {() => void} */
    let push

    class List extends Component {
      constructor () {
        super()
        this.state = { values: [0, 1, 2] }
        push = () =>
          this.setState({
            values: [...this.state.values, this.state.values.length]
          })
      }

      render () {
        return (
          <Fragment>
            {this.state.values.map(value => (
              <div>{value}</div>
            ))}
          </Fragment>
        )
      }
    }

    render(<List />, scratch)
    expect(scratch.textContent).toEqual('012')

    push()
    rerender()

    expect(scratch.textContent).toEqual('0123')

    push()
    rerender()

    expect(scratch.textContent).toEqual('01234')
  })

  it('should render sibling array children', () => {
    const Group = ({ title, values }) => (
      <Fragment>
        <li>{title}</li>
        {values.map(value => (
          <li>{value}</li>
        ))}
      </Fragment>
    )

    const Todo = () => (
      <ul>
        <Group title={'A header'} values={['a', 'b']} />
        <Group title={'A divider'} values={['c', 'd']} />
        <li>A footer</li>
      </ul>
    )

    render(<Todo />, scratch)

    expect(scratch.innerHTML).toEqual(
      ul(
        [
          li('A header'),
          li('a'),
          li('b'),
          li('A divider'),
          li('c'),
          li('d'),
          li('A footer')
        ].join('')
      )
    )
  })

  it('should reorder Fragment children', () => {
    let updateState

    class App extends Component {
      constructor () {
        super()
        this.state = { active: false }
        updateState = () => this.setState(prev => ({ active: !prev.active }))
      }

      render () {
        return (
          <div>
            <h1>Heading</h1>
            {!this.state.active ? (
              <Fragment>
                foobar
                <Fragment>
                  Hello World
                  <h2>yo</h2>
                </Fragment>
                <input type='text' />
              </Fragment>
            ) : (
              <Fragment>
                <Fragment>
                  Hello World
                  <h2>yo</h2>
                </Fragment>
                foobar
                <input type='text' />
              </Fragment>
            )}
          </div>
        )
      }
    }

    render(<App />, scratch)

    expect(scratch.innerHTML).toEqual(
      '<div><h1>Heading</h1>foobarHello World<h2>yo</h2><input type="text"></div>'
    )

    updateState()

    // See "should preserve state between top level fragment and array"
    // Perhaps rename test to "should reorder **keyed** Fragment children"

    rerender()
    expect(scratch.innerHTML).toEqual(
      normalizeHTML('<div><h1>Heading</h1>Hello World<h2>yo</h2>foobar<input type="text"></div>')
    )
  })

  it('should render sibling fragments with multiple children in the correct order', () => {
    render(
      <ol>
        <li>0</li>
        <Fragment>
          <li>1</li>
          <li>2</li>
        </Fragment>
        <li>3</li>
        <li>4</li>
        <Fragment>
          <li>5</li>
          <li>6</li>
        </Fragment>
        <li>7</li>
      </ol>,
      scratch
    )

    expect(scratch.textContent).toEqual('01234567')
  })

  it('should support HOCs that return children', () => {
    const text =
      "Don't forget to tell these special people in your life just how special they are to you."

    class BobRossProvider extends Component {
      getChildContext () {
        return { text }
      }

      render () {
        return this.props.children
      }
    }

    function BobRossConsumer (props, context) {
      return props.children(context.text)
    }

    const Say = props => <div>{props.text}</div>

    const Speak = () => (
      <Fragment>
        <span>the top</span>
        <BobRossProvider>
          <span>a span</span>
          <BobRossConsumer>
            {text => [<Say text={text} />, <Say text={text} />]}
          </BobRossConsumer>
          <span>another span</span>
        </BobRossProvider>
        <span>a final span</span>
      </Fragment>
    )

    render(<Speak />, scratch)

    expect(scratch.innerHTML).toEqual(
      normalizeHTML([
        span('the top'),
        span('a span'),
        div(text),
        div(text),
        span('another span'),
        span('a final span')
      ].join(''))
    )
  })

  it('should support conditionally rendered Fragment', () => {
    const Foo = ({ condition }) => (
      <ol>
        <li>0</li>
        {condition ? (
          <Fragment>
            <li>1</li>
            <li>2</li>
          </Fragment>
        ) : (
          [<li>1</li>, <li>2</li>]
        )}
        <li>3</li>
      </ol>
    )

    const html = ol([li('0'), li('1'), li('2'), li('3')].join(''))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(html), 'initial render of true')
    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(html), 'rendering from true to false')

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(html), 'rendering from false to true')
  })

  it('should support conditionally rendered Fragment or null', () => {
    const Foo = ({ condition }) => (
      <ol>
        <li>0</li>
        {condition ? (
          <Fragment>
            <li>1</li>
            <li>2</li>
          </Fragment>
        ) : null}
        <li>3</li>
        <li>4</li>
      </ol>
    )

    const htmlForTrue = ol(
      [li('0'), li('1'), li('2'), li('3'), li('4')].join('')
    )

    const htmlForFalse = ol([li('0'), li('3'), li('4')].join(''))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(htmlForTrue), 'initial render of true')

    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(
      normalizeHTML(htmlForFalse),
      'rendering from true to false'
    )

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(
      normalizeHTML(htmlForTrue),
      'rendering from false to true'
    )
  })

  it('should support moving Fragments between beginning and end', () => {
    const Foo = ({ condition }) => (
      <ol>
        {condition
          ? [
            <li>0</li>,
            <li>1</li>,
            <li>2</li>,
            <li>3</li>,
            <Fragment>
              <li>4</li>
              <li>5</li>
            </Fragment>
          ]
          : [
            <Fragment>
              <li>4</li>
              <li>5</li>
            </Fragment>,
            <li>0</li>,
            <li>1</li>,
            <li>2</li>,
            <li>3</li>
          ]}
      </ol>
    )

    const htmlForTrue = normalizeHTML(ol(
      [li('0'), li('1'), li('2'), li('3'), li('4'), li('5')].join('')
    ))

    const htmlForFalse = normalizeHTML(ol(
      [li('4'), li('5'), li('0'), li('1'), li('2'), li('3')].join('')
    ))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(htmlForTrue, 'initial render of true')

    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )
    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should support conditional beginning and end Fragments', () => {
    const Foo = ({ condition }) => (
      <ol>
        {condition ? (
          <Fragment>
            <li>0</li>
            <li>1</li>
          </Fragment>
        ) : null}
        <li>2</li>
        <li>2</li>
        {condition ? null : (
          <Fragment>
            <li>3</li>
            <li>4</li>
          </Fragment>
        )}
      </ol>
    )

    const htmlForTrue = normalizeHTML(ol([li(0), li(1), li(2), li(2)].join('')))

    const htmlForFalse = normalizeHTML(ol([li(2), li(2), li(3), li(4)].join('')))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(htmlForTrue, 'initial render of true')

    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )
    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should support nested conditional beginning and end Fragments', () => {
    const Foo = ({ condition }) => (
      <ol>
        {condition ? (
          <Fragment>
            <Fragment>
              <Fragment>
                <li>0</li>
                <li>1</li>
              </Fragment>
            </Fragment>
          </Fragment>
        ) : null}
        <li>2</li>
        <li>3</li>
        {condition ? null : (
          <Fragment>
            <Fragment>
              <Fragment>
                <li>4</li>
                <li>5</li>
              </Fragment>
            </Fragment>
          </Fragment>
        )}
      </ol>
    )

    const htmlForTrue = normalizeHTML(ol([li(0), li(1), li(2), li(3)].join('')))

    const htmlForFalse = normalizeHTML(ol([li(2), li(3), li(4), li(5)].join('')))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(htmlForTrue, 'initial render of true')

    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should preserve state with reordering in multiple levels with mixed # of Fragment siblings', () => {
    // Also fails if the # of divs outside the Fragment equals or exceeds
    // the # inside the Fragment for both conditions
    function Foo ({ condition }) {
      return condition ? (
        <div>
          <Fragment key='c'>
            <div>foo</div>
            <div key='b'>
              <Stateful key='a' />
            </div>
          </Fragment>
          <div>boop</div>
          <div>boop</div>
        </div>
      ) : (
        <div>
          <div>beep</div>
          <Fragment key='c'>
            <div key='b'>
              <Stateful key='a' />
            </div>
            <div>bar</div>
          </Fragment>
        </div>
      )
    }

    const htmlForTrue = normalizeHTML(div(
      [div('foo'), div(div('Hello')), div('boop'), div('boop')].join('')
    ))

    const htmlForFalse = normalizeHTML(div(
      [div('beep'), div(div('Hello')), div('bar')].join('')
    ))

    render(<Foo condition />, scratch)

    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )

    render(<Foo condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )

    render(<Foo condition />, scratch)

    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should preserve state with reordering in multiple levels with lots of Fragment siblings', () => {
    // Also fails if the # of divs outside the Fragment equals or exceeds
    // the # inside the Fragment for both conditions
    function Foo ({ condition }) {
      return condition ? (
        <div>
          <Fragment key='c'>
            <div>foo</div>
            <div key='b'>
              <Stateful key='a' />
            </div>
          </Fragment>
          <div>boop</div>
          <div>boop</div>
          <div>boop</div>
        </div>
      ) : (
        <div>
          <div>beep</div>
          <div>beep</div>
          <div>beep</div>
          <Fragment key='c'>
            <div key='b'>
              <Stateful key='a' />
            </div>
            <div>bar</div>
          </Fragment>
        </div>
      )
    }

    const htmlForTrue = normalizeHTML(div(
      [
        div('foo'),
        div(div('Hello')),
        div('boop'),
        div('boop'),
        div('boop')
      ].join('')
    ))

    const htmlForFalse = normalizeHTML(div(
      [
        div('beep'),
        div('beep'),
        div('beep'),
        div(div('Hello')),
        div('bar')
      ].join('')
    ))

    render(<Foo condition />, scratch)

    render(<Foo condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )

    render(<Foo condition />, scratch)

    // expect(ops).toEqual(['Update Stateful', 'Update Stateful'])
    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should correctly append children with siblings', () => {
    /**
     * @type {(props: { values: Array<string | number>}) => JSX.Element}
     */
    const Foo = ({ values }) => (
      <ol>
        <li>a</li>
        <Fragment>
          {values.map(value => (
            <li>{value}</li>
          ))}
        </Fragment>
        <li>b</li>
      </ol>
    )

    const getHtml = values =>
      normalizeHTML(ol([li('a'), ...values.map(value => li(value)), li('b')].join('')))

    const values = [0, 1, 2]
    render(<Foo values={values} />, scratch)
    expect(scratch.innerHTML).toEqual(
      getHtml(values),
      `original list: [${values.join(',')}]`
    )

    values.push(3)
    render(<Foo values={values} />, scratch)
    expect(scratch.innerHTML).toEqual(
      getHtml(values),
      `push 3: [${values.join(',')}]`
    )

    values.push(4)

    render(<Foo values={values} />, scratch)
    expect(scratch.innerHTML).toEqual(
      getHtml(values),
      `push 4: [${values.join(',')}]`
    )
  })

  it('should render components that conditionally return Fragments', () => {
    const Foo = ({ condition }) =>
      condition ? (
        <Fragment>
          <div>1</div>
          <div>2</div>
        </Fragment>
      ) : (
        <div>
          <div>3</div>
          <div>4</div>
        </div>
      )

    const htmlForTrue = normalizeHTML([div(1), div(2)].join(''))

    const htmlForFalse = normalizeHTML(div([div(3), div(4)].join('')))

    render(<Foo condition />, scratch)

    expect(scratch.innerHTML).toEqual(htmlForTrue)

    render(<Foo condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(htmlForFalse)

    render(<Foo condition />, scratch)

    expect(scratch.innerHTML).toEqual(htmlForTrue)
  })

  it('should clear empty Fragments', () => {
    function Foo (props) {
      if (props.condition) {
        return <Fragment>foo</Fragment>
      }
      return <Fragment />
    }

    render(<Foo condition />, scratch)
    expect(scratch.textContent).toEqual('foo')

    render(<Foo condition={false} />, scratch)
    expect(scratch.textContent).toEqual('')
  })

  it('should support conditionally rendered nested Fragments or null with siblings', () => {
    const Foo = ({ condition }) => (
      <ol>
        <li>0</li>
        <Fragment>
          <li>1</li>
          {condition ? (
            <Fragment>
              <li>2</li>
              <li>3</li>
            </Fragment>
          ) : null}
          <li>4</li>
        </Fragment>
        <li>5</li>
      </ol>
    )

    const htmlForTrue = normalizeHTML(ol(
      [li('0'), li('1'), li('2'), li('3'), li('4'), li('5')].join('')
    ))

    const htmlForFalse = normalizeHTML(ol([li('0'), li('1'), li('4'), li('5')].join('')))

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(htmlForTrue, 'initial render of true')

    render(<Foo condition={false} />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForFalse,
      'rendering from true to false'
    )

    render(<Foo condition />, scratch)
    expect(scratch.innerHTML).toEqual(
      htmlForTrue,
      'rendering from false to true'
    )
  })

  it('should render first child Fragment that wrap null components', () => {
    const Empty = () => null
    const Foo = () => (
      <ol>
        <Fragment>
          <Empty />
        </Fragment>
        <li>1</li>
      </ol>
    )

    render(<Foo />, scratch)
    expect(scratch.innerHTML).toEqual(normalizeHTML(ol([li(1)].join(''))))
  })

  it('should properly render Components that return Fragments and use shouldComponentUpdate #1415', () => {
    class SubList extends Component {
      shouldComponentUpdate (nextProps) {
        return nextProps.prop1 !== this.props.prop1
      }
      render () {
        return (
          <Fragment>
            <div>2</div>
            <div>3</div>
          </Fragment>
        )
      }
    }

    /** @type {(update: any) => void} */
    let setState
    class App extends Component {
      constructor () {
        super()
        setState = update => this.setState(update)

        this.state = { error: false }
      }

      render () {
        return (
          <div>
            {this.state.error ? (
              <div>Error!</div>
            ) : (
              <div>
                <div>1</div>
                <SubList prop1={this.state.error} />
              </div>
            )}
          </div>
        )
      }
    }

    const successHtml = normalizeHTML(div(div([div(1), div(2), div(3)].join(''))))

    const errorHtml = normalizeHTML(div(div('Error!')))

    render(<App />, scratch)
    expect(scratch.innerHTML).toEqual(successHtml)

    setState({}) // Trigger sCU
    rerender()
    expect(scratch.innerHTML).toEqual(successHtml)

    setState({ error: true })

    rerender()
    expect(scratch.innerHTML).toEqual(errorHtml)

    setState({ error: false })
    rerender()
    expect(scratch.innerHTML).toEqual(successHtml)

    setState({}) // Trigger sCU again
    rerender()
    expect(scratch.innerHTML).toEqual(successHtml)
  })

  it('should use the last dom node for _lastDomChild', () => {
    const Noop = () => null
    let update
    class App extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ items: ['A', 'B', 'C'] })
        this.state = {
          items: null
        }
      }

      render () {
        return (
          <div>
            {this.state.items && (
              <Fragment>
                {this.state.items.map(v => (
                  <div>{v}</div>
                ))}
                <Noop />
              </Fragment>
            )}
          </div>
        )
      }
    }

    render(<App />, scratch)
    expect(scratch.textContent).toEqual('')

    update()
    rerender()

    expect(scratch.textContent).toEqual('ABC')
  })

  it('should replace node in-between children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? <section>B2</section> : <div>B1</div>
      }
    }

    render(
      <div>
        <div>A</div>
        <SetState />
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B1</div><div>C</div></div>`)
    )

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><section>B2</section><div>C</div></div>`)
    )
  })

  it('should replace Fragment in-between children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? (
          <Fragment>
            <section>B3</section>
            <section>B4</section>
          </Fragment>
        ) : (
          <Fragment>
            <div>B1</div>
            <div>B2</div>
          </Fragment>
        )
      }
    }

    render(
      <div>
        <div>A</div>
        <SetState />
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(div([div('A'), div('B1'), div('B2'), div('C')].join('')))
    )

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(div([div('A'), section('B3'), section('B4'), div('C')].join('')))
    )
  })

  it('should insert in-between children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? <div>B</div> : null
      }
    }

    render(
      <div>
        <div>A</div>
        <SetState />
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B</div><div>C</div></div>`)
    )
  })

  it('should insert in-between Fragments', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? [<div>B1</div>, <div>B2</div>] : null
      }
    }

    render(
      <div>
        <div>A</div>
        <SetState />
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B1</div><div>B2</div><div>C</div></div>`)
    )
  })

  it('should insert in-between null children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? <div>B</div> : null
      }
    }

    render(
      <div>
        <div>A</div>
        {null}
        <SetState />
        {null}
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B</div><div>C</div></div>`)
    )
  })

  it('should insert Fragment in-between null children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? (
          <Fragment>
            <div>B1</div>
            <div>B2</div>
          </Fragment>
        ) : null
      }
    }

    render(
      <div>
        <div>A</div>
        {null}
        <SetState />
        {null}
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B1</div><div>B2</div><div>C</div></div>`)
    )
  })

  it('should insert in-between nested null children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? <div>B</div> : null
      }
    }

    function Outer () {
      return <SetState />
    }

    render(
      <div>
        <div>A</div>
        {null}
        <Outer />
        {null}
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B</div><div>C</div></div>`)
    )
  })

  it('should insert Fragment in-between nested null children', () => {
    let update
    class SetState extends Component {
      constructor (props) {
        super(props)
        update = () => this.setState({ active: true })
      }

      render () {
        return this.state.active ? (
          <Fragment>
            <div>B1</div>
            <div>B2</div>
          </Fragment>
        ) : null
      }
    }

    function Outer () {
      return <SetState />
    }

    render(
      <div>
        <div>A</div>
        {null}
        <Outer />
        {null}
        <div>C</div>
      </div>,
      scratch
    )

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    update()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B1</div><div>B2</div><div>C</div></div>`)
    )
  })

  it('should update at correct place', () => {
    let updateA
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { active: true }
        updateA = () => this.setState(prev => ({ active: !prev.active }))
      }

      render () {
        return this.state.active ? <div>A</div> : <span>A2</span>
      }
    }

    function B () {
      return <div>B</div>
    }

    function X (props) {
      return props.children
    }

    function App (props) {
      const b = props.condition ? <B /> : null
      return (
        <div>
          <X>
            <A />
          </X>
          <X>
            {b}
            <div>C</div>
          </X>
        </div>
      )
    }

    render(<App condition />, scratch)

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A</div><div>B</div><div>C</div></div>`)
    )

    render(<App condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><div>A</div><div>C</div></div>`))

    updateA()
    rerender()

    expect(scratch.innerHTML).toEqual(normalizeHTML(`<div><span>A2</span><div>C</div></div>`))
  })

  it('should update Fragment at correct place', () => {
    let updateA
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { active: true }
        updateA = () => this.setState(prev => ({ active: !prev.active }))
      }

      render () {
        return this.state.active
          ? [<div>A1</div>, <div>A2</div>]
          : [<span>A3</span>, <span>A4</span>]
      }
    }

    function B () {
      return <div>B</div>
    }

    function X (props) {
      return props.children
    }

    function App (props) {
      const b = props.condition ? <B /> : null
      return (
        <div>
          <X>
            <A />
          </X>
          <X>
            {b}
            <div>C</div>
          </X>
        </div>
      )
    }

    render(<App condition />, scratch)

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A1</div><div>A2</div><div>B</div><div>C</div></div>`)
    )

    render(<App condition={false} />, scratch)

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><div>A1</div><div>A2</div><div>C</div></div>`)
    )

    updateA()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(`<div><span>A3</span><span>A4</span><div>C</div></div>`)
    )
  })

  it('should insert children correctly if sibling component DOM changes', () => {
    /** @type {() => void} */
    let updateA
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { active: true }
        updateA = () => this.setState(prev => ({ active: !prev.active }))
      }

      render () {
        return this.state.active ? <div>A</div> : <span>A2</span>
      }
    }

    /** @type {() => void} */
    let updateB
    class B extends Component {
      constructor (props) {
        super(props)
        this.state = { active: false }
        updateB = () => this.setState(prev => ({ active: !prev.active }))
      }
      render () {
        return this.state.active ? <div>B</div> : null
      }
    }

    function X (props) {
      return props.children
    }

    function App () {
      return (
        <div>
          <X>
            <A />
          </X>
          <X>
            <B />
            <div>C</div>
          </X>
        </div>
      )
    }

    render(<App />, scratch)

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(div([div('A'), div('C')].join(''))),
      'initial'
    )

    updateB()
    rerender()

    expect(scratch.innerHTML).toEqual(
      normalizeHTML(div([div('A'), div('B'), div('C')].join(''))),
      'updateB'
    )
    updateA()
    rerender()

    expect(scratch.innerHTML).toEqual(
      div([span('A2'), div('B'), div('C')].join('')),
      'updateA'
    )
  })

  it('should correctly append children if last child changes DOM', () => {
    /** @type {() => void} */
    let updateA
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { active: true }
        updateA = () => this.setState(prev => ({ active: !prev.active }))
      }

      render () {
        return this.state.active
          ? [<div>A1</div>, <div>A2</div>]
          : [<span>A3</span>, <span>A4</span>]
      }
    }

    /** @type {() => void} */
    let updateB
    class B extends Component {
      constructor (props) {
        super(props)
        this.state = { active: false }
        updateB = () => this.setState(prev => ({ active: !prev.active }))
      }
      render () {
        return (
          <Fragment>
            <A />
            {this.state.active ? <div>B</div> : null}
          </Fragment>
        )
      }
    }

    render(<B />, scratch)

    expect(scratch.innerHTML).toEqual([div('A1'), div('A2')].join(''), 'initial')

    updateA()
    rerender()

    expect(scratch.innerHTML).toEqual(
      [span('A3'), span('A4')].join(''),
      'updateA'
    )

    updateB()

    rerender()

    expect(scratch.innerHTML).toEqual(
      [span('A3'), span('A4'), div('B')].join(''),
      'updateB'
    )
  })

  it('should correctly append children #0', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <main>
          <span>test</span>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
        </main>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><span>test</span><li>a</li><li>b</li><li>c</li></main>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><span>test</span></main>`)
    )
  })

  it('should correctly append children #1', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <Fragment>
          <span>test</span>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
        </Fragment>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<span>test</span><li>a</li><li>b</li><li>c</li>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<span>test</span>`)
    )
  })

  it('should correctly append children #2', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <Fragment>
          <span>test</span>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
          <span>test2</span>
        </Fragment>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<span>test</span><li>a</li><li>b</li><li>c</li><span>test2</span>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<span>test</span><span>test2</span>`)
    )
  })

  it('should correctly append children #3', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <main>
          <span>test</span>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
          <span>test2</span>
        </main>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><span>test</span><li>a</li><li>b</li><li>c</li><span>test2</span></main>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><span>test</span><span>test2</span></main>`)
    )
  })

  it('should correctly append children #4', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <Fragment>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
          <span>test2</span>
        </Fragment>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<li>a</li><li>b</li><li>c</li><span>test2</span>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<span>test2</span>`)
    )
  })

  it('should correctly append children #5', () => {
    let inst
    class A extends Component {
      constructor (props) {
        super(props)
        this.state = { arr: ['a', 'b', 'c'] }
        inst = this
      }

      render () {
        const { arr } = this.state
        return <main>
          {
            arr.length ? arr.map(a => <li key={a}>{a}</li>) : null
          }
          <span>test2</span>
        </main>
      }
    }

    render(<A />, scratch)
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><li>a</li><li>b</li><li>c</li><span>test2</span></main>`)
    )
    inst.setState({
      arr: []
    })
    inst.forceUpdate()
    expect(scratch.innerHTML).toBe(
      normalizeHTML(`<main><span>test2</span></main>`)
    )
  })
})