// 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)
+ })
+})