]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: add tests for rendererChildren (#52)
authorHaoqun Jiang <haoqunjiang@gmail.com>
Fri, 20 Sep 2019 22:17:35 +0000 (06:17 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 20 Sep 2019 22:17:35 +0000 (18:17 -0400)
* test: [wip] add tests for rendererChildren

* chore: use serializeInner for clearer output

* fix: should remove the text node if content is empty

* test: also test for appended content

* test: inserting & removing

* test: moving children

* refactor: use a helper function

* test: finish tests

* test: duplicate keys tests belong to keyed children block

* fix(runtime-test): fix insert when moving node in the same parent

* fix: fix failing test cases for rendererChildren

* test: handle rendererChildren edge case

packages/runtime-core/__tests__/rendererChildren.spec.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-test/src/nodeOps.ts

index bed025e9a35f1cfae16da7d8c90dfc6f9471c4d1..553d73b5e75d2290d5e36698213d0816dcb93591 100644 (file)
 // reference: https://github.com/vuejs/vue/blob/dev/test/unit/modules/vdom/patch/children.spec.js
+import {
+  h,
+  render,
+  nodeOps,
+  NodeTypes,
+  TestElement,
+  serialize,
+  serializeInner,
+  mockWarn
+} from '@vue/runtime-test'
 
-describe('renderer: unkeyed children', () => {
-  test.todo('append')
+mockWarn()
+
+function toSpan(content: any) {
+  if (typeof content === 'string') {
+    return h('span', content.toString())
+  } else {
+    return h('span', { key: content }, content.toString())
+  }
+}
+
+const inner = (c: TestElement) => serializeInner(c)
+
+function shuffle(array: Array<any>) {
+  let currentIndex = array.length
+  let temporaryValue
+  let randomIndex
+
+  // while there remain elements to shuffle...
+  while (currentIndex !== 0) {
+    // pick a remaining element...
+    randomIndex = Math.floor(Math.random() * currentIndex)
+    currentIndex -= 1
+    // and swap it with the current element.
+    temporaryValue = array[currentIndex]
+    array[currentIndex] = array[randomIndex]
+    array[randomIndex] = temporaryValue
+  }
+  return array
+}
+
+it('should patch previously empty children', () => {
+  const root = nodeOps.createElement('div')
+
+  render(h('div', []), root)
+  expect(inner(root)).toBe('<div></div>')
+
+  render(h('div', ['hello']), root)
+  expect(inner(root)).toBe('<div>hello</div>')
+})
+
+it('should patch previously null children', () => {
+  const root = nodeOps.createElement('div')
+
+  render(h('div'), root)
+  expect(inner(root)).toBe('<div></div>')
+
+  render(h('div', ['hello']), root)
+  expect(inner(root)).toBe('<div>hello</div>')
+})
+
+describe('renderer: keyed children', () => {
+  let root: TestElement
+  let elm: TestElement
+  const renderChildren = (arr: number[]) => {
+    render(h('div', arr.map(toSpan)), root)
+    return root.children[0] as TestElement
+  }
+
+  beforeEach(() => {
+    root = nodeOps.createElement('div')
+    render(h('div', { id: 1 }, 'hello'), root)
+  })
+
+  test('append', () => {
+    elm = renderChildren([1])
+    expect(elm.children.length).toBe(1)
+
+    elm = renderChildren([1, 2, 3])
+    expect(elm.children.length).toBe(3)
+    expect(serialize(elm.children[1])).toBe('<span>2</span>')
+    expect(serialize(elm.children[2])).toBe('<span>3</span>')
+  })
+
+  test('prepend', () => {
+    elm = renderChildren([4, 5])
+    expect(elm.children.length).toBe(2)
+
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '3',
+      '4',
+      '5'
+    ])
+  })
+
+  test('insert in middle', () => {
+    elm = renderChildren([1, 2, 4, 5])
+    expect(elm.children.length).toBe(4)
+
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '3',
+      '4',
+      '5'
+    ])
+  })
+
+  test('insert at beginning and end', () => {
+    elm = renderChildren([2, 3, 4])
+    expect(elm.children.length).toBe(3)
+
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '3',
+      '4',
+      '5'
+    ])
+  })
+
+  test('insert to empty parent', () => {
+    elm = renderChildren([])
+    expect(elm.children.length).toBe(0)
+
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '3',
+      '4',
+      '5'
+    ])
+  })
+
+  test('remove all children from parent', () => {
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '3',
+      '4',
+      '5'
+    ])
+
+    render(h('div'), root)
+    expect(elm.children.length).toBe(0)
+  })
+
+  test('remove from beginning', () => {
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+
+    elm = renderChildren([3, 4, 5])
+    expect(elm.children.length).toBe(3)
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['3', '4', '5'])
+  })
+
+  test('remove from end', () => {
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+
+    elm = renderChildren([1, 2, 3])
+    expect(elm.children.length).toBe(3)
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['1', '2', '3'])
+  })
+
+  test('remove from middle', () => {
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
+
+    elm = renderChildren([1, 2, 4, 5])
+    expect(elm.children.length).toBe(4)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '2',
+      '4',
+      '5'
+    ])
+  })
+
+  test('moving single child forward', () => {
+    elm = renderChildren([1, 2, 3, 4])
+    expect(elm.children.length).toBe(4)
+
+    elm = renderChildren([2, 3, 1, 4])
+    expect(elm.children.length).toBe(4)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '2',
+      '3',
+      '1',
+      '4'
+    ])
+  })
+
+  test('moving single child backwards', () => {
+    elm = renderChildren([1, 2, 3, 4])
+    expect(elm.children.length).toBe(4)
+
+    elm = renderChildren([1, 4, 2, 3])
+    expect(elm.children.length).toBe(4)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      '4',
+      '2',
+      '3'
+    ])
+  })
+
+  test('moving single child to end', () => {
+    elm = renderChildren([1, 2, 3])
+    expect(elm.children.length).toBe(3)
+
+    elm = renderChildren([2, 3, 1])
+    expect(elm.children.length).toBe(3)
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['2', '3', '1'])
+  })
 
-  test.todo('prepend')
+  test('swap first and last', () => {
+    elm = renderChildren([1, 2, 3, 4])
+    expect(elm.children.length).toBe(4)
 
-  test.todo('insert in middle')
+    elm = renderChildren([4, 2, 3, 1])
+    expect(elm.children.length).toBe(4)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '4',
+      '2',
+      '3',
+      '1'
+    ])
+  })
 
-  test.todo('insert at beginning and end')
+  test('move to left & replace', () => {
+    elm = renderChildren([1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(5)
 
-  test.todo('insert to empty parent')
+    elm = renderChildren([4, 1, 2, 3, 6])
+    expect(elm.children.length).toBe(5)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '4',
+      '1',
+      '2',
+      '3',
+      '6'
+    ])
+  })
 
-  test.todo('shift with offset')
+  test('move to left and leaves hold', () => {
+    elm = renderChildren([1, 4, 5])
+    expect(elm.children.length).toBe(3)
 
-  test.todo('remove from beginning')
+    elm = renderChildren([4, 6])
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['4', '6'])
+  })
 
-  test.todo('remove from end')
+  test('moved and set to undefined element ending at the end', () => {
+    elm = renderChildren([2, 4, 5])
+    expect(elm.children.length).toBe(3)
 
-  test.todo('remove from middle')
+    elm = renderChildren([4, 5, 3])
+    expect(elm.children.length).toBe(3)
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['4', '5', '3'])
+  })
 
-  test.todo('moving single child forward')
+  test('reverse element', () => {
+    elm = renderChildren([1, 2, 3, 4, 5, 6, 7, 8])
+    expect(elm.children.length).toBe(8)
 
-  test.todo('moving single child backwards')
+    elm = renderChildren([8, 7, 6, 5, 4, 3, 2, 1])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '8',
+      '7',
+      '6',
+      '5',
+      '4',
+      '3',
+      '2',
+      '1'
+    ])
+  })
 
-  test.todo('moving single child to end')
+  test('something', () => {
+    elm = renderChildren([0, 1, 2, 3, 4, 5])
+    expect(elm.children.length).toBe(6)
 
-  test.todo('swap first and last')
+    elm = renderChildren([4, 3, 2, 1, 5, 0])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '4',
+      '3',
+      '2',
+      '1',
+      '5',
+      '0'
+    ])
+  })
 
-  test.todo('move to left & replace')
+  test('random shuffle', () => {
+    const elms = 14
+    const samples = 5
+    const arr = [...Array(elms).keys()]
+    const opacities: string[] = []
 
-  test.todo('generic reorder')
+    function spanNumWithOpacity(n: number, o: string) {
+      return h('span', { key: n, style: { opacity: o } }, n.toString())
+    }
 
-  test.todo('should not de-opt when both head and tail change')
+    for (let n = 0; n < samples; ++n) {
+      render(h('span', arr.map(n => spanNumWithOpacity(n, '1'))), root)
+      elm = root.children[0] as TestElement
+
+      for (let i = 0; i < elms; ++i) {
+        expect(serializeInner(elm.children[i] as TestElement)).toBe(
+          i.toString()
+        )
+        opacities[i] = Math.random()
+          .toFixed(5)
+          .toString()
+      }
+
+      const shufArr = shuffle(arr.slice(0))
+      render(
+        h('span', arr.map(n => spanNumWithOpacity(shufArr[n], opacities[n]))),
+        root
+      )
+      elm = root.children[0] as TestElement
+      for (let i = 0; i < elms; ++i) {
+        expect(serializeInner(elm.children[i] as TestElement)).toBe(
+          shufArr[i].toString()
+        )
+        expect(elm.children[i]).toMatchObject({
+          props: {
+            style: {
+              opacity: opacities[i]
+            }
+          }
+        })
+      }
+    }
+  })
+
+  test('children with the same key but with different tag', () => {
+    render(
+      h('div', [
+        h('div', { key: 1 }, 'one'),
+        h('div', { key: 2 }, 'two'),
+        h('div', { key: 3 }, 'three'),
+        h('div', { key: 4 }, 'four')
+      ]),
+      root
+    )
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
+      'div',
+      'div',
+      'div',
+      'div'
+    ])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'one',
+      'two',
+      'three',
+      'four'
+    ])
+
+    render(
+      h('div', [
+        h('div', { key: 4 }, 'four'),
+        h('span', { key: 3 }, 'three'),
+        h('span', { key: 2 }, 'two'),
+        h('div', { key: 1 }, 'one')
+      ]),
+      root
+    )
+    expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
+      'div',
+      'span',
+      'span',
+      'div'
+    ])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'four',
+      'three',
+      'two',
+      'one'
+    ])
+  })
+
+  test('children with the same tag, same key, but one with data and one without data', () => {
+    render(h('div', [h('div', { class: 'hi' }, 'one')]), root)
+    elm = root.children[0] as TestElement
+    expect(elm.children[0]).toMatchObject({
+      props: {
+        class: 'hi'
+      }
+    })
+
+    render(h('div', [h('div', 'four')]), root)
+    elm = root.children[0] as TestElement
+    expect(elm.children[0] as TestElement).toMatchObject({
+      props: {
+        // in the DOM renderer this will be ''
+        // but the test renderer simply sets whatever value it receives.
+        class: null
+      }
+    })
+    expect(serialize(elm.children[0])).toBe(`<div>four</div>`)
+  })
+
+  test('should warn with duplicate keys', () => {
+    renderChildren([1, 2, 3, 4, 5])
+    renderChildren([1, 6, 6, 3, 5])
+    expect(`Duplicate keys`).toHaveBeenWarned()
+  })
 })
 
-describe('renderer: keyed children', () => {})
+describe('renderer: unkeyed children', () => {
+  let root: TestElement
+  let elm: TestElement
+  const renderChildren = (arr: Array<number | string>) => {
+    render(h('div', arr.map(toSpan)), root)
+    return root.children[0] as TestElement
+  }
+
+  beforeEach(() => {
+    root = nodeOps.createElement('div')
+    render(h('div', { id: 1 }, 'hello'), root)
+  })
+
+  test('move a key in non-keyed nodes with a size up', () => {
+    elm = renderChildren([1, 'a', 'b', 'c'])
+    expect(elm.children.length).toBe(4)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      '1',
+      'a',
+      'b',
+      'c'
+    ])
+
+    elm = renderChildren(['d', 'a', 'b', 'c', 1, 'e'])
+    expect(elm.children.length).toBe(6)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'd',
+      'a',
+      'b',
+      'c',
+      '1',
+      'e'
+    ])
+  })
+
+  test('append elements with updating children without keys', () => {
+    elm = renderChildren(['hello'])
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['hello'])
+
+    elm = renderChildren(['hello', 'world'])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'hello',
+      'world'
+    ])
+  })
+
+  test('unmoved text nodes with updating children without keys', () => {
+    render(h('div', ['text', h('span', ['hello'])]), root)
+
+    elm = root.children[0] as TestElement
+    expect(elm.children[0]).toMatchObject({
+      type: NodeTypes.TEXT,
+      text: 'text'
+    })
+
+    render(h('div', ['text', h('span', ['hello'])]), root)
+
+    elm = root.children[0] as TestElement
+    expect(elm.children[0]).toMatchObject({
+      type: NodeTypes.TEXT,
+      text: 'text'
+    })
+  })
+
+  test('changing text children with updating children without keys', () => {
+    render(h('div', ['text', h('span', ['hello'])]), root)
+
+    elm = root.children[0] as TestElement
+    expect(elm.children[0]).toMatchObject({
+      type: NodeTypes.TEXT,
+      text: 'text'
+    })
+
+    render(h('div', ['text2', h('span', ['hello'])]), root)
+
+    elm = root.children[0] as TestElement
+    expect(elm.children[0]).toMatchObject({
+      type: NodeTypes.TEXT,
+      text: 'text2'
+    })
+  })
+
+  test('prepend element with updating children without keys', () => {
+    render(h('div', [h('span', ['world'])]), root)
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
+
+    render(h('div', [h('span', ['hello']), h('span', ['world'])]), root)
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'hello',
+      'world'
+    ])
+  })
+
+  test('prepend element of different tag type with updating children without keys', () => {
+    render(h('div', [h('span', ['world'])]), root)
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['world'])
+
+    render(h('div', [h('div', ['hello']), h('span', ['world'])]), root)
+    expect((elm.children as TestElement[]).map(c => c.tag)).toEqual([
+      'div',
+      'span'
+    ])
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'hello',
+      'world'
+    ])
+  })
+
+  test('remove elements with updating children without keys', () => {
+    render(
+      h('div', [h('span', ['one']), h('span', ['two']), h('span', ['three'])]),
+      root
+    )
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'one',
+      'two',
+      'three'
+    ])
+
+    render(h('div', [h('span', ['one']), h('span', ['three'])]), root)
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['one', 'three'])
+  })
+
+  test('remove a single text node with updating children without keys', () => {
+    render(h('div', ['one']), root)
+    elm = root.children[0] as TestElement
+    expect(serializeInner(elm)).toBe('one')
+
+    render(h('div'), root)
+    expect(serializeInner(elm)).toBe('')
+  })
+
+  test('remove a single text node when children are updated', () => {
+    render(h('div', ['one']), root)
+    elm = root.children[0] as TestElement
+    expect(serializeInner(elm)).toBe('one')
+
+    render(h('div', [h('div', ['two']), h('span', ['three'])]), root)
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual(['two', 'three'])
+  })
+
+  test('remove a text node among other elements', () => {
+    render(h('div', ['one', h('span', ['two'])]), root)
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(c => serialize(c))).toEqual([
+      'one',
+      '<span>two</span>'
+    ])
+
+    render(h('div', [h('div', ['three'])]), root)
+    elm = root.children[0] as TestElement
+    expect(elm.children.length).toBe(1)
+    expect(serialize(elm.children[0])).toBe('<div>three</div>')
+  })
+
+  test('reorder elements', () => {
+    render(
+      h('div', [h('span', ['one']), h('div', ['two']), h('b', ['three'])]),
+      root
+    )
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'one',
+      'two',
+      'three'
+    ])
+
+    render(
+      h('div', [h('b', ['three']), h('div', ['two']), h('span', ['one'])]),
+      root
+    )
+    elm = root.children[0] as TestElement
+    expect((elm.children as TestElement[]).map(inner)).toEqual([
+      'three',
+      'two',
+      'one'
+    ])
+  })
+
+  // #6502
+  test('should not de-opt when both head and tail change', () => {
+    render(h('div', [null, h('div'), null]), root)
+    elm = root.children[0] as TestElement
+    const original = elm.children[1]
+
+    render(h('div', [h('p'), h('div'), h('p')]), root)
+    elm = root.children[0] as TestElement
+    const postPatch = elm.children[1]
+
+    expect(postPatch).toBe(original)
+  })
+})
index 22cf5ef90d66cd9fb84c77fec7a886840c759523..f294f941a982e41306868f509a678716db94b45d 100644 (file)
@@ -1512,7 +1512,10 @@ export function createRenderer<
         } else {
           // key-less node, try to locate a key-less node of the same type
           for (j = s2; j <= e2; j++) {
-            if (isSameType(prevChild, c2[j] as HostVNode)) {
+            if (
+              newIndexToOldIndexMap[j - s2] === 0 &&
+              isSameType(prevChild, c2[j] as HostVNode)
+            ) {
               newIndex = j
               break
             }
index 41e2c3967f73d3d379666ca3af90e4fe26556983..6d0c7c61748a74995c6f3feb58b53a6c99052a87 100644 (file)
@@ -155,7 +155,9 @@ function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) {
   })
   // remove the node first, but don't log it as a REMOVE op
   remove(child, false)
-  if (refIndex === undefined) {
+  // re-calculate the ref index because the child's removal may have affected it
+  refIndex = ref ? parent.children.indexOf(ref) : -1
+  if (refIndex === -1) {
     parent.children.push(child)
     child.parentNode = parent
   } else {
@@ -195,14 +197,18 @@ function setElementText(el: TestElement, text: string) {
   el.children.forEach(c => {
     c.parentNode = null
   })
-  el.children = [
-    {
-      id: nodeId++,
-      type: NodeTypes.TEXT,
-      text,
-      parentNode: el
-    }
-  ]
+  if (!text) {
+    el.children = []
+  } else {
+    el.children = [
+      {
+        id: nodeId++,
+        type: NodeTypes.TEXT,
+        text,
+        parentNode: el
+      }
+    ]
+  }
 }
 
 function parentNode(node: TestNode): TestElement | null {