Parcourir la source

add patch tests

ref: #2872
kazuya kawaguchi il y a 10 ans
Parent
commit
02cdbd2dbb

+ 2 - 8
src/entries/web-runtime.js

@@ -3,10 +3,7 @@
 import Vue from 'core/index'
 import config from 'core/config'
 import { noop } from 'shared/util'
-import * as nodeOps from 'web/runtime/node-ops'
-import { createPatchFunction } from 'core/vdom/patch'
-import baseModules from 'core/vdom/modules/index'
-import platformModules from 'web/runtime/modules/index'
+import { basePatch } from 'web/runtime/patch'
 import platformDirectives from 'web/runtime/directives/index'
 import { query, isUnknownElement, isReservedTag, mustUseProp } from 'web/util/index'
 
@@ -19,10 +16,7 @@ Vue.config.mustUseProp = mustUseProp
 Vue.options.directives = platformDirectives
 
 // install platform patch function
-const modules = baseModules.concat(platformModules)
-Vue.prototype.__patch__ = config._isServer
-  ? noop
-  : createPatchFunction({ nodeOps, modules })
+Vue.prototype.__patch__ = config._isServer ? noop : basePatch
 
 // wrap mount
 Vue.prototype.$mount = function (el?: string | Element): Vue {

+ 8 - 0
src/platforms/web/runtime/patch.js

@@ -0,0 +1,8 @@
+import * as nodeOps from 'web/runtime/node-ops'
+import { createPatchFunction } from 'core/vdom/patch'
+import baseModules from 'core/vdom/modules/index'
+import platformModules from 'web/runtime/modules/index'
+
+const modules = baseModules.concat(platformModules)
+
+export const basePatch: Function = createPatchFunction({ nodeOps, modules })

+ 1 - 0
test/unit/index.js

@@ -1,6 +1,7 @@
 import Vue from 'vue'
 import '../helpers/to-have-been-warned.js'
 import '../helpers/wait-for-update.js'
+import '../helpers/vdom.js'
 
 Vue.config.preserveWhitespace = false
 

+ 75 - 0
test/unit/modules/vdom/modules/attrs.spec.js

@@ -0,0 +1,75 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+import { xlinkNS } from 'web/util/index'
+
+describe('attrs module', () => {
+  it('should create an element with staticAttrs', () => {
+    const vnode = new VNode('p', { staticAttrs: { id: 1, class: 'class1' }})
+    const elm = patch(null, vnode)
+    expect(elm.id).toBe('1')
+    expect(elm.classList.contains('class1')).toBe(true)
+  })
+
+  it('should create an element with attrs', () => {
+    const vnode = new VNode('p', { attrs: { id: 1, class: 'class1' }})
+    const elm = patch(null, vnode)
+    expect(elm.id).toBe('1')
+    expect(elm.classList.contains('class1')).toBe(true)
+  })
+
+  it('should change the elements attrs', () => {
+    const vnode1 = new VNode('i', { staticAttrs: { id: '1', class: 'i am vdom' }})
+    const vnode2 = new VNode('i', { attrs: { id: '2', class: 'i am' }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.id).toBe('2')
+    expect(elm.classList.contains('i')).toBe(true)
+    expect(elm.classList.contains('am')).toBe(true)
+    expect(elm.classList.contains('vdom')).toBe(false)
+  })
+
+  it('should remove the elements attrs', () => {
+    const vnode1 = new VNode('i', { attrs: { id: '1', class: 'i am vdom' }})
+    const vnode2 = new VNode('i', { attrs: { id: '1' }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.id).toBe('1')
+    expect(elm.classList.length).toBe(0)
+  })
+
+  it('should remove the falsy value from boolean attr', () => {
+    const vnode = new VNode('option', { staticAttrs: { disabled: null }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttribute('disabled')).toBe(null)
+  })
+
+  it('should set the attr name to boolean attr', () => {
+    const vnode = new VNode('option', { staticAttrs: { disabled: true }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttribute('disabled')).toBe('disabled')
+  })
+
+  it('should set the falsy value to enumerated attr', () => {
+    const vnode = new VNode('div', { staticAttrs: { contenteditable: null }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttribute('contenteditable')).toBe('false')
+  })
+
+  it('should set the boolean string value to enumerated attr', () => {
+    const vnode = new VNode('div', { staticAttrs: { contenteditable: 'true' }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttribute('contenteditable')).toBe('true')
+  })
+
+  it('should set the xlink value to attr', () => {
+    const vnode = new VNode('a', { staticAttrs: { 'xlink:href': '#id1' }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttributeNS(xlinkNS, 'href')).toBe('#id1')
+  })
+
+  it('should set the xlink boolean string value to attr', () => {
+    const vnode = new VNode('option', { staticAttrs: { 'xlink:disabled': true }})
+    const elm = patch(null, vnode)
+    expect(elm.getAttributeNS(xlinkNS, 'disabled')).toBe('true')
+  })
+})

+ 95 - 0
test/unit/modules/vdom/modules/class.spec.js

@@ -0,0 +1,95 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('class module', () => {
+  it('shuold create an element with staticClass', () => {
+    const vnode = new VNode('p', { staticClass: 'class1' })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(true)
+  })
+
+  it('should create an element with class', () => {
+    const vnode = new VNode('p', { class: 'class1' })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(true)
+  })
+
+  it('should create an element with array class', () => {
+    const vnode = new VNode('p', { class: ['class1', 'class2'] })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(true)
+    expect(elm.classList.contains('class2')).toBe(true)
+  })
+
+  it('should create an element with object class', () => {
+    const vnode = new VNode('p', {
+      class: { class1: true, class2: false, class3: true }
+    })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(true)
+    expect(elm.classList.contains('class2')).toBe(false)
+    expect(elm.classList.contains('class3')).toBe(true)
+  })
+
+  it('should create an element with mixed class', () => {
+    const vnode = new VNode('p', {
+      class: [{ class1: false, class2: true, class3: false }, 'class4', ['class5', 'class6']]
+    })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(false)
+    expect(elm.classList.contains('class2')).toBe(true)
+    expect(elm.classList.contains('class3')).toBe(false)
+    expect(elm.classList.contains('class4')).toBe(true)
+    expect(elm.classList.contains('class5')).toBe(true)
+    expect(elm.classList.contains('class6')).toBe(true)
+  })
+
+  it('should create an element with staticClass and class', () => {
+    const vnode = new VNode('p', { staticClass: 'class1', class: 'class2' })
+    const elm = patch(null, vnode)
+    expect(elm.classList.contains('class1')).toBe(true)
+    expect(elm.classList.contains('class2')).toBe(true)
+  })
+
+  it('should handle transition class', () => {
+    const vnode1 = new VNode('p', {
+      class: { class1: true, class2: false, class3: true }
+    })
+    let elm = patch(null, vnode1)
+    elm._transitionClasses = ['class4']
+    const vnode2 = new VNode('p', {
+      class: { class1: true, class2: true, class3: true }
+    })
+    elm = patch(vnode1, vnode2)
+    expect(elm.classList.contains('class1')).toBe(true)
+    expect(elm.classList.contains('class2')).toBe(true)
+    expect(elm.classList.contains('class3')).toBe(true)
+    expect(elm.classList.contains('class4')).toBe(true)
+  })
+
+  it('should change the elements class', () => {
+    const vnode1 = new VNode('p', {
+      class: { class1: true, class2: false, class3: true }
+    })
+    const vnode2 = new VNode('p', { staticClass: 'foo bar' })
+    let elm = patch(null, vnode1)
+    elm = patch(vnode1, vnode2)
+    expect(elm.classList.contains('class1')).toBe(false)
+    expect(elm.classList.contains('class2')).toBe(false)
+    expect(elm.classList.contains('class3')).toBe(false)
+    expect(elm.classList.contains('foo')).toBe(true)
+    expect(elm.classList.contains('bar')).toBe(true)
+  })
+
+  it('should remove the elements class', () => {
+    const vnode1 = new VNode('p', {
+      class: { class1: true, class2: false, class3: true }
+    })
+    const vnode2 = new VNode('p', { class: {}})
+    let elm = patch(null, vnode1)
+    elm = patch(vnode1, vnode2)
+    expect(elm.classList.contains('class1')).toBe(false)
+    expect(elm.classList.contains('class2')).toBe(false)
+    expect(elm.classList.contains('class3')).toBe(false)
+  })
+})

+ 62 - 0
test/unit/modules/vdom/modules/directive.spec.js

@@ -0,0 +1,62 @@
+import Vue from 'vue'
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('directive module', () => {
+  it('should work directive', () => {
+    const directive1 = {
+      bind: jasmine.createSpy('bind'),
+      update: jasmine.createSpy('update'),
+      unbind: jasmine.createSpy('unbind')
+    }
+    const vm = new Vue({ directives: { directive1 }})
+    // create
+    const vnode1 = new VNode('div', {}, [
+      new VNode('p', {
+        directives: [{
+          name: 'directive1', value: 'hello', arg: 'arg1', modifiers: { modifire1: true }
+        }]
+      }, undefined, 'hello world', undefined, undefined, vm)
+    ])
+    patch(null, vnode1)
+    expect(directive1.bind).toHaveBeenCalled()
+    // update
+    const vnode2 = new VNode('div', {}, [
+      new VNode('p', {
+        directives: [{
+          name: 'directive1', value: 'world', arg: 'arg1', modifiers: { modifire1: true }
+        }]
+      }, undefined, 'hello world', undefined, undefined, vm)
+    ])
+    patch(vnode1, vnode2)
+    expect(directive1.update).toHaveBeenCalled()
+    // destroy
+    const vnode3 = new VNode('div')
+    patch(vnode2, vnode3)
+    expect(directive1.unbind).toHaveBeenCalled()
+  })
+
+  it('should not update when same binding value', () => {
+    const directive1 = {
+      update: jasmine.createSpy('update')
+    }
+    const vm = new Vue({ directives: { directive1 }})
+    const vnode1 = new VNode('div', {}, [
+      new VNode('p', {
+        directives: [{
+          name: 'directive1', value: 'hello', arg: 'arg1', modifiers: { modifire1: true }
+        }]
+      }, undefined, 'hello world', undefined, undefined, vm)
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('p', {
+        directives: [{
+          name: 'directive1', value: 'hello', arg: 'arg1', modifiers: { modifire1: true }
+        }]
+      }, undefined, 'hello world', undefined, undefined, vm)
+    ])
+    patch(null, vnode1)
+    patch(vnode1, vnode2)
+    expect(directive1.update).not.toHaveBeenCalled()
+  })
+})

+ 41 - 0
test/unit/modules/vdom/modules/events.spec.js

@@ -0,0 +1,41 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('events module', () => {
+  it('should attach event handler to element', () => {
+    const click = jasmine.createSpy()
+    const vnode = new VNode('a', { on: { click }})
+    const elm = patch(null, vnode)
+    elm.click()
+    expect(click.calls.count()).toBe(1)
+  })
+
+  it('should not attach new listener', () => {
+    const click = jasmine.createSpy()
+    const vnode1 = new VNode('a', { on: { click }})
+    const vnode2 = new VNode('a', { on: { click }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    elm.click()
+    expect(click.calls.count()).toBe(1)
+  })
+
+  it('should attach event handlers', () => {
+    const click = jasmine.createSpy()
+    const vnode = new VNode('a', { on: { click: [click, click] }})
+    const elm = patch(null, vnode)
+    elm.click()
+    expect(click.calls.count()).toBe(2)
+  })
+
+  it('should change attach event handlers', () => {
+    const click = jasmine.createSpy()
+    const focus = jasmine.createSpy()
+    const vnode1 = new VNode('a', { on: { click: [click, focus] }})
+    const vnode2 = new VNode('a', { on: { click: [click] }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    elm.click()
+    expect(click.calls.count()).toBe(1)
+  })
+})

+ 26 - 0
test/unit/modules/vdom/modules/props.js

@@ -0,0 +1,26 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('props module', () => {
+  it('should create an element with props', () => {
+    const vnode = new VNode('a', { props: { src: 'http://localhost/' }})
+    const elm = patch(null, vnode)
+    expect(elm.src).toBe('http://localhost/')
+  })
+
+  it('should change the elements props', () => {
+    const vnode1 = new VNode('a', { props: { src: 'http://localhost/' }})
+    const vnode2 = new VNode('a', { props: { src: 'http://vuejs.org/' }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.src).toBe('http://vuejs.org/')
+  })
+
+  it('should remove the elements props', () => {
+    const vnode1 = new VNode('a', { props: { src: 'http://localhost/' }})
+    const vnode2 = new VNode('a', { props: {}})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.src).toBeUndefined()
+  })
+})

+ 35 - 0
test/unit/modules/vdom/modules/style.spec.js

@@ -0,0 +1,35 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('style module', () => {
+  it('should create an element with style', () => {
+    const vnode = new VNode('p', { style: { fontSize: '12px' }})
+    const elm = patch(null, vnode)
+    expect(elm.style.fontSize).toBe('12px')
+  })
+
+  it('should create an element with array style', () => {
+    const vnode = new VNode('p', { style: [{ fontSize: '12px' }, { color: 'red' }] })
+    const elm = patch(null, vnode)
+    expect(elm.style.fontSize).toBe('12px')
+    expect(elm.style.color).toBe('red')
+  })
+
+  it('should change elements style', () => {
+    const vnode1 = new VNode('p', { style: { fontSize: '12px' }})
+    const vnode2 = new VNode('p', { style: { fontSize: '10px', display: 'block' }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.style.fontSize).toBe('10px')
+    expect(elm.style.display).toBe('block')
+  })
+
+  it('should remove elements attrs', () => {
+    const vnode1 = new VNode('p', { style: { fontSize: '12px' }})
+    const vnode2 = new VNode('p', { style: { display: 'block' }})
+    patch(null, vnode1)
+    const elm = patch(vnode1, vnode2)
+    expect(elm.style.fontSize).toBe('')
+    expect(elm.style.display).toBe('block')
+  })
+})

+ 23 - 0
test/unit/modules/vdom/modules/transition.spec.js

@@ -0,0 +1,23 @@
+import Vue from 'vue'
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+import { inBrowser } from 'core/util/index'
+
+if (inBrowser) {
+  describe('trasition module', () => {
+    it('should transit with basic transition', () => {
+      const vm = new Vue()
+      // create
+      const vnode1 = new VNode('div', {}, [
+        new VNode('p', { transition: { definition: 'expand', appear: true }},
+          undefined, undefined, undefined, undefined, vm
+        )
+      ])
+      let elm = patch(null, vnode1)
+      expect(elm.childNodes[0].classList.contains('expand-enter')).toBe(true)
+      // remove
+      const vnode2 = new VNode('div', {})
+      elm = patch(vnode1, vnode2)
+    })
+  })
+}

+ 0 - 0
test/unit/modules/vdom/patch.spec.js


+ 431 - 0
test/unit/modules/vdom/patch/children.spec.js

@@ -0,0 +1,431 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+function prop (name) {
+  return obj => { return obj[name] }
+}
+
+function map (fn, list) {
+  const ret = []
+  for (let i = 0; i < list.length; i++) {
+    ret[i] = fn(list[i])
+  }
+  return ret
+}
+
+function spanNum (n) {
+  if (typeof n === 'string') {
+    return new VNode('span', {}, undefined, n)
+  } else {
+    return new VNode('span', { key: n }, undefined, n.toString())
+  }
+}
+
+function shuffle (array) {
+  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
+}
+
+const inner = prop('innerHTML')
+
+describe('children', () => {
+  let vnode0
+  beforeEach(() => {
+    vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
+    patch(null, vnode0)
+  })
+
+  it('should appends elements', () => {
+    const vnode1 = new VNode('p', {}, [1].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 3].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(1)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(3)
+    expect(elm.children[1].innerHTML).toBe('2')
+    expect(elm.children[2].innerHTML).toBe('3')
+  })
+
+  it('should prepends elements', () => {
+    const vnode1 = new VNode('p', {}, [4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(2)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['1', '2', '3', '4', '5'])
+  })
+
+  it('should add elements in the middle', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(4)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['1', '2', '3', '4', '5'])
+  })
+
+  it('should add elements at begin and end', () => {
+    const vnode1 = new VNode('p', {}, [2, 3, 4].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(3)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['1', '2', '3', '4', '5'])
+  })
+
+  it('should add children to parent with no children', () => {
+    const vnode1 = new VNode('p', { key: 'p' })
+    const vnode2 = new VNode('p', { key: 'p' }, [1, 2, 3].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(0)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['1', '2', '3'])
+  })
+
+  it('should remove all children from parent', () => {
+    const vnode1 = new VNode('p', { key: 'p' }, [1, 2, 3].map(spanNum))
+    const vnode2 = new VNode('p', { key: 'p' })
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['1', '2', '3'])
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(0)
+  })
+
+  it('should remove elements from the beginning', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [3, 4, 5].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(5)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['3', '4', '5'])
+  })
+
+  it('should removes elements from end', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 3].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(5)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(3)
+    expect(elm.children[0].innerHTML).toBe('1')
+    expect(elm.children[1].innerHTML).toBe('2')
+    expect(elm.children[2].innerHTML).toBe('3')
+  })
+
+  it('should remove elements from the middle', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 2, 4, 5].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(5)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(4)
+    expect(elm.children[0].innerHTML).toBe('1')
+    expect(elm.children[1].innerHTML).toBe('2')
+    expect(elm.children[2].innerHTML).toBe('4')
+    expect(elm.children[3].innerHTML).toBe('5')
+  })
+
+  it('should moves element forward', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4].map(spanNum))
+    const vnode2 = new VNode('p', {}, [2, 3, 1, 4].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(4)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(4)
+    expect(elm.children[0].innerHTML).toBe('2')
+    expect(elm.children[1].innerHTML).toBe('3')
+    expect(elm.children[2].innerHTML).toBe('1')
+    expect(elm.children[3].innerHTML).toBe('4')
+  })
+
+  it('should move elements to end', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3].map(spanNum))
+    const vnode2 = new VNode('p', {}, [2, 3, 1].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(3)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(3)
+    expect(elm.children[0].innerHTML).toBe('2')
+    expect(elm.children[1].innerHTML).toBe('3')
+    expect(elm.children[2].innerHTML).toBe('1')
+  })
+
+  it('should move element backwards', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4].map(spanNum))
+    const vnode2 = new VNode('p', {}, [1, 4, 2, 3].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(4)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(4)
+    expect(elm.children[0].innerHTML).toBe('1')
+    expect(elm.children[1].innerHTML).toBe('4')
+    expect(elm.children[2].innerHTML).toBe('2')
+    expect(elm.children[3].innerHTML).toBe('3')
+  })
+
+  it('should swap first and last', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4].map(spanNum))
+    const vnode2 = new VNode('p', {}, [4, 2, 3, 1].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(4)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(4)
+    expect(elm.children[0].innerHTML).toBe('4')
+    expect(elm.children[1].innerHTML).toBe('2')
+    expect(elm.children[2].innerHTML).toBe('3')
+    expect(elm.children[3].innerHTML).toBe('1')
+  })
+
+  it('should move to left and replace', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [4, 1, 2, 3, 6].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(5)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(5)
+    expect(elm.children[0].innerHTML).toBe('4')
+    expect(elm.children[1].innerHTML).toBe('1')
+    expect(elm.children[2].innerHTML).toBe('2')
+    expect(elm.children[3].innerHTML).toBe('3')
+    expect(elm.children[4].innerHTML).toBe('6')
+  })
+
+  it('should move to left and leaves hold', () => {
+    const vnode1 = new VNode('p', {}, [1, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [4, 6].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(3)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['4', '6'])
+  })
+
+  it('should handle moved and set to undefined element ending at the end', () => {
+    const vnode1 = new VNode('p', {}, [2, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [4, 5, 3].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(3)
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(3)
+    expect(elm.children[0].innerHTML).toBe('4')
+    expect(elm.children[1].innerHTML).toBe('5')
+    expect(elm.children[2].innerHTML).toBe('3')
+  })
+
+  it('should move a key in non-keyed nodes with a size up', () => {
+    const vnode1 = new VNode('p', {}, [1, 'a', 'b', 'c'].map(spanNum))
+    const vnode2 = new VNode('p', {}, ['d', 'a', 'b', 'c', 1, 'e'].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(4)
+    expect(elm.textContent, '1abc')
+    elm = patch(vnode1, vnode2)
+    expect(elm.children.length).toBe(6)
+    expect(elm.textContent, 'dabc1e')
+  })
+
+  it('should reverse element', () => {
+    const vnode1 = new VNode('p', {}, [1, 2, 3, 4, 5, 6, 7, 8].map(spanNum))
+    const vnode2 = new VNode('p', {}, [8, 7, 6, 5, 4, 3, 2, 1].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(8)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['8', '7', '6', '5', '4', '3', '2', '1'])
+  })
+
+  it('something', () => {
+    const vnode1 = new VNode('p', {}, [0, 1, 2, 3, 4, 5].map(spanNum))
+    const vnode2 = new VNode('p', {}, [4, 3, 2, 1, 5, 0].map(spanNum))
+    let elm = patch(vnode0, vnode1)
+    expect(elm.children.length).toBe(6)
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['4', '3', '2', '1', '5', '0'])
+  })
+
+  it('should handle random shuffle', () => {
+    let n
+    let i
+    const arr = []
+    const opacities = []
+    const elms = 14
+    const samples = 5
+    function spanNumWithOpacity (n, o) {
+      return new VNode('span', { key: n, style: { opacity: o }}, undefined, n.toString())
+    }
+
+    for (n = 0; n < elms; ++n) { arr[n] = n }
+    for (n = 0; n < samples; ++n) {
+      const vnode1 = new VNode('span', {}, arr.map(n => {
+        return spanNumWithOpacity(n, '1')
+      }))
+      const shufArr = shuffle(arr.slice(0))
+      let elm = patch(vnode0, vnode1)
+      for (i = 0; i < elms; ++i) {
+        expect(elm.children[i].innerHTML).toBe(i.toString())
+        opacities[i] = Math.random().toFixed(5).toString()
+      }
+      const vnode2 = new VNode('span', {}, arr.map(n => {
+        return spanNumWithOpacity(shufArr[n], opacities[n])
+      }))
+      elm = patch(vnode1, vnode2)
+      for (i = 0; i < elms; ++i) {
+        expect(elm.children[i].innerHTML).toBe(shufArr[i].toString())
+        expect(opacities[i].indexOf(elm.children[i].style.opacity)).toBe(0)
+      }
+    }
+  })
+
+  it('should append elements with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'hello')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'hello'),
+      new VNode('span', {}, undefined, 'world')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['hello'])
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['hello', 'world'])
+  })
+
+  it('should handle unmoved text nodes with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      createTextVNode('text'),
+      new VNode('span', {}, undefined, 'hello')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      createTextVNode('text'),
+      new VNode('span', {}, undefined, 'hello')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(elm.childNodes[0].textContent).toBe('text')
+    elm = patch(vnode1, vnode2)
+    expect(elm.childNodes[0].textContent).toBe('text')
+  })
+
+  it('should handle changing text children with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      createTextVNode('text'),
+      new VNode('span', {}, undefined, 'hello')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      createTextVNode('text2'),
+      new VNode('span', {}, undefined, 'hello')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(elm.childNodes[0].textContent).toBe('text')
+    elm = patch(vnode1, vnode2)
+    expect(elm.childNodes[0].textContent).toBe('text2')
+  })
+
+  it('should prepend element with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'world')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'hello'),
+      new VNode('span', {}, undefined, 'world')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['world'])
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['hello', 'world'])
+  })
+
+  it('should prepend element of different tag type with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'world')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('div', {}, undefined, 'hello'),
+      new VNode('span', {}, undefined, 'world')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['world'])
+    elm = patch(vnode1, vnode2)
+    expect(map(prop('tagName'), elm.children)).toEqual(['DIV', 'SPAN'])
+    expect(map(inner, elm.children)).toEqual(['hello', 'world'])
+  })
+
+  it('should remove elements with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'one'),
+      new VNode('span', {}, undefined, 'two'),
+      new VNode('span', {}, undefined, 'three')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'one'),
+      new VNode('span', {}, undefined, 'three')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['one', 'two', 'three'])
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['one', 'three'])
+  })
+
+  it('should remove a single text node with updating children without keys', () => {
+    const vnode1 = new VNode('div', {}, undefined, 'one')
+    const vnode2 = new VNode('div', {})
+    let elm = patch(vnode0, vnode1)
+    expect(elm.textContent).toBe('one')
+    elm = patch(vnode1, vnode2)
+    expect(elm.textContent).toBe('')
+  })
+
+  it('should remove a single text node when children are updated', () => {
+    const vnode1 = new VNode('div', {}, undefined, 'one')
+    const vnode2 = new VNode('div', {}, [
+      new VNode('div', {}, undefined, 'two'),
+      new VNode('span', {}, undefined, 'three')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(elm.textContent).toBe('one')
+    elm = patch(vnode1, vnode2)
+    expect(map(prop('textContent'), elm.childNodes)).toEqual(['two', 'three'])
+  })
+
+  it('should remove a text node among other elements', () => {
+    const vnode1 = new VNode('div', {}, [
+      createTextVNode('one'),
+      new VNode('span', {}, undefined, 'two')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('div', {}, undefined, 'three')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(prop('textContent'), elm.childNodes)).toEqual(['one', 'two'])
+    elm = patch(vnode1, vnode2)
+    expect(elm.childNodes.length).toBe(1)
+    expect(elm.childNodes[0].tagName).toBe('DIV')
+    expect(elm.childNodes[0].textContent).toBe('three')
+  })
+
+  it('should reorder elements', () => {
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'one'),
+      new VNode('div', {}, undefined, 'two'),
+      new VNode('b', {}, undefined, 'three')
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('b', {}, undefined, 'three'),
+      new VNode('span', {}, undefined, 'two'),
+      new VNode('div', {}, undefined, 'one')
+    ])
+    let elm = patch(vnode0, vnode1)
+    expect(map(inner, elm.children)).toEqual(['one', 'two', 'three'])
+    elm = patch(vnode1, vnode2)
+    expect(map(inner, elm.children)).toEqual(['three', 'two', 'one'])
+  })
+})

+ 33 - 0
test/unit/modules/vdom/patch/element.spec.js

@@ -0,0 +1,33 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import VNode from 'core/vdom/vnode'
+
+describe('element', () => {
+  it('should create an element', () => {
+    const vnode = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
+    const elm = patch(null, vnode)
+    expect(elm.tagName).toBe('P')
+    expect(elm.outerHTML).toBe('<p id="1">hello world</p>')
+  })
+
+  it('should create an element which having the namespace', () => {
+    const vnode = new VNode('svg', {}, undefined, undefined, undefined, 'svg')
+    const elm = patch(null, vnode)
+    expect(elm.namespaceURI).toBe('http://www.w3.org/2000/svg')
+  })
+
+  it('should create an elements which having text content', () => {
+    const vnode = new VNode('div', {}, [createTextVNode('hello world')])
+    const elm = patch(null, vnode)
+    expect(elm.innerHTML).toBe('hello world')
+  })
+
+  it('should create create an elements which having span and text content', () => {
+    const vnode = new VNode('div', {}, [
+      new VNode('span'),
+      createTextVNode('hello world')
+    ])
+    const elm = patch(null, vnode)
+    expect(elm.childNodes[0].tagName).toBe('SPAN')
+    expect(elm.childNodes[1].textContent).toBe('hello world')
+  })
+})

+ 319 - 0
test/unit/modules/vdom/patch/hooks.spec.js

@@ -0,0 +1,319 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import { createPatchFunction } from 'core/vdom/patch'
+import baseModules from 'core/vdom/modules/index'
+import * as nodeOps from 'web/runtime/node-ops'
+import platformModules from 'web/runtime/modules/index'
+import VNode from 'core/vdom/vnode'
+
+const modules = baseModules.concat(platformModules)
+
+describe('hooks', () => {
+  let vnode0
+  beforeEach(() => {
+    vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
+    patch(null, vnode0)
+  })
+
+  it('should call `insert` listener after both parents, siblings and children have been inserted', () => {
+    const result = []
+    function insert (vnode) {
+      expect(vnode.elm.children.length).toBe(2)
+      expect(vnode.elm.parentNode.children.length).toBe(3)
+      result.push(vnode)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { insert }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ]),
+      new VNode('span', {}, undefined, 'can touch me')
+    ])
+    patch(vnode0, vnode1)
+    expect(result.length).toBe(1)
+  })
+
+  it('should call `prepatch` listener', () => {
+    const result = []
+    function prepatch (oldVnode, newVnode) {
+      expect(oldVnode).toEqual(vnode1.children[1])
+      expect(newVnode).toEqual(vnode2.children[1])
+      result.push(newVnode)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { prepatch }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { prepatch }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(result.length).toBe(1)
+  })
+
+  it('should call `postpatch` after `prepatch` listener', () => {
+    const pre = []
+    const post = []
+    function prepatch (oldVnode, newVnode) {
+      pre.push(pre)
+    }
+    function postpatch (oldVnode, newVnode) {
+      expect(pre.length).toBe(post.length + 1)
+      post.push(post)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { prepatch, postpatch }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { prepatch, postpatch }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(pre.length).toBe(1)
+    expect(post.length).toBe(1)
+  })
+
+  it('should call `update` listener', () => {
+    const result1 = []
+    const result2 = []
+    function cb (result, oldVnode, newVnode) {
+      if (result.length > 1) {
+        expect(result[result.length - 1]).toEqual(oldVnode)
+      }
+      result.push(newVnode)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { update: cb.bind(null, result1) }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', { hook: { update: cb.bind(null, result2) }}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { update: cb.bind(null, result1) }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', { hook: { update: cb.bind(null, result2) }}, undefined, 'child 2')
+      ])
+    ])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(result1.length).toBe(1)
+    expect(result2.length).toBe(1)
+  })
+
+  it('should call `remove` listener', () => {
+    const result = []
+    function remove (vnode, rm) {
+      const parent = vnode.elm.parentNode
+      expect(vnode.elm.children.length).toBe(2)
+      expect(vnode.elm.children.length).toBe(2)
+      result.push(vnode)
+      rm()
+      expect(parent.children.length).toBe(1)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { remove }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling')
+    ])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(result.length).toBe(1)
+  })
+
+  it('should call `init` and `prepatch` listeners on root', () => {
+    let count = 0
+    function init (vnode) { count++ }
+    function prepatch (oldVnode, newVnode) { count++ }
+    const vnode1 = new VNode('div', { hook: { init, prepatch }})
+    patch(vnode0, vnode1)
+    expect(count).toBe(1)
+    const vnode2 = new VNode('span', { hook: { init, prepatch }})
+    patch(vnode1, vnode2)
+    expect(count).toBe(2)
+  })
+
+  it('should remove element when all remove listeners are done', () => {
+    let rm1, rm2, rm3
+    const patch1 = createPatchFunction({
+      nodeOps,
+      modules: modules.concat([
+        { remove (_, rm) { rm1 = rm } },
+        { remove (_, rm) { rm2 = rm } }
+      ])
+    })
+    const vnode1 = new VNode('div', {}, [
+      new VNode('a', { hook: { remove (_, rm) { rm3 = rm } }})
+    ])
+    const vnode2 = new VNode('div', {}, [])
+    let elm = patch1(vnode0, vnode1)
+    expect(elm.children.length).toBe(1)
+    elm = patch1(vnode1, vnode2)
+    expect(elm.children.length).toBe(1)
+    rm1()
+    expect(elm.children.length).toBe(1)
+    rm3()
+    expect(elm.children.length).toBe(1)
+    rm2()
+    expect(elm.children.length).toBe(0)
+  })
+
+  it('should invoke the remove hook on replaced root', () => {
+    const result = []
+    const parent = nodeOps.createElement('div')
+    vnode0 = nodeOps.createElement('div')
+    parent.appendChild(vnode0)
+    function remove (vnode, rm) {
+      result.push(vnode)
+      rm()
+    }
+    const vnode1 = new VNode('div', { hook: { remove }}, [
+      new VNode('b', {}, undefined, 'child 1'),
+      new VNode('i', {}, undefined, 'child 2')
+    ])
+    const vnode2 = new VNode('span', {}, [
+      new VNode('b', {}, undefined, 'child 1'),
+      new VNode('i', {}, undefined, 'child 2')
+    ])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(result.length).toBe(1)
+  })
+
+  it('should invoke global `destroy` hook for all removed children', () => {
+    const result = []
+    function destroy (vnode) { result.push(vnode) }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', {}, [
+        new VNode('span', { hook: { destroy }}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div')
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+    expect(result.length).toBe(1)
+  })
+
+  it('should handle text vnodes with `undefined` `data` property', () => {
+    const vnode1 = new VNode('div', {}, [createTextVNode(' ')])
+    const vnode2 = new VNode('div', {}, [])
+    patch(vnode0, vnode1)
+    patch(vnode1, vnode2)
+  })
+
+  it('should invoke `destroy` module hook for all removed children', () => {
+    let created = 0
+    let destroyed = 0
+    const patch1 = createPatchFunction({
+      nodeOps,
+      modules: modules.concat([
+        { create () { created++ } },
+        { destroy () { destroyed++ } }
+      ])
+    })
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', {}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ])
+    ])
+    const vnode2 = new VNode('div')
+    patch1(vnode0, vnode1)
+    patch1(vnode1, vnode2)
+    expect(created).toBe(5)
+    expect(destroyed).toBe(4)
+  })
+
+  it('should not invoke `create` and `remove` module hook for text nodes', () => {
+    let created = 0
+    let removed = 0
+    const patch1 = createPatchFunction({
+      nodeOps,
+      modules: modules.concat([
+        { create () { created++ } },
+        { remove () { removed++ } }
+      ])
+    })
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first child'),
+      createTextVNode(''),
+      new VNode('span', {}, undefined, 'third child')
+    ])
+    const vnode2 = new VNode('div')
+    patch1(vnode0, vnode1)
+    patch1(vnode1, vnode2)
+    expect(created).toBe(3)
+    expect(removed).toBe(2)
+  })
+
+  it('should not invoke `destroy` module hook for text nodes', () => {
+    let created = 0
+    let destroyed = 0
+    const patch1 = createPatchFunction({
+      nodeOps,
+      modules: modules.concat([
+        { create () { created++ } },
+        { destroy () { destroyed++ } }
+      ])
+    })
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', {}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, [
+          createTextVNode('text1'),
+          createTextVNode('text2')
+        ])
+      ])
+    ])
+    const vnode2 = new VNode('div')
+    patch1(vnode0, vnode1)
+    patch1(vnode1, vnode2)
+    expect(created).toBe(5)
+    expect(destroyed).toBe(4)
+  })
+
+  it('should call `create` listener before inserted into parent but after children', () => {
+    const result = []
+    function create (empty, vnode) {
+      expect(vnode.elm.children.length).toBe(2)
+      expect(vnode.elm.parentNode).toBe(null)
+      result.push(vnode)
+    }
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}, undefined, 'first sibling'),
+      new VNode('div', { hook: { create }}, [
+        new VNode('span', {}, undefined, 'child 1'),
+        new VNode('span', {}, undefined, 'child 2')
+      ]),
+      new VNode('span', {}, undefined, 'can\'t touch me')
+    ])
+    patch(vnode0, vnode1)
+    expect(result.length).toBe(1)
+  })
+})

+ 91 - 0
test/unit/modules/vdom/patch/hydration.spec.js

@@ -0,0 +1,91 @@
+import { basePatch as patch } from 'web/runtime/patch'
+import * as nodeOps from 'web/runtime/node-ops'
+import VNode from 'core/vdom/vnode'
+
+describe('hydration', () => {
+  let vnode0
+  beforeEach(() => {
+    vnode0 = new VNode('p', { attrs: { id: '1' }}, [createTextVNode('hello world')])
+    patch(null, vnode0)
+  })
+
+  it('should hydrate elements when server-rendered DOM tree is same as virtual DOM tree', () => {
+    const result = []
+    function init (vnode) { result.push(vnode) }
+    function createServerRenderedDOM () {
+      const root = nodeOps.createElement('div')
+      root.setAttribute('server-rendered', 'true')
+      const span = nodeOps.createElement('span')
+      root.appendChild(span)
+      const div = nodeOps.createElement('div')
+      const child1 = nodeOps.createElement('span')
+      const child2 = nodeOps.createElement('span')
+      div.appendChild(child1)
+      div.appendChild(child2)
+      root.appendChild(div)
+      return root
+    }
+    const node0 = createServerRenderedDOM()
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}),
+      new VNode('div', { hook: { init }}, [
+        new VNode('span', {}),
+        new VNode('span', {})
+      ])
+    ])
+    patch(node0, vnode1)
+    expect(result.length).toBe(1)
+  })
+
+  it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', () => {
+    const result = []
+    function init (vnode) { result.push(vnode) }
+    function createServerRenderedDOM () {
+      const root = nodeOps.createElement('vue-component-1')
+      root.setAttribute('server-rendered', 'true')
+      const span = nodeOps.createElement('span')
+      root.appendChild(span)
+      const div = nodeOps.createElement('div')
+      const child1 = nodeOps.createElement('span')
+      const child2 = nodeOps.createElement('span')
+      div.appendChild(child1)
+      div.appendChild(child2)
+      root.appendChild(div)
+      return root
+    }
+    const node0 = createServerRenderedDOM()
+    const vnode1 = new VNode('vue-component-1', {}, [
+      new VNode('span', {}),
+      new VNode('div', { hook: { init }}, [
+        new VNode('span', {}),
+        new VNode('span', {})
+      ])
+    ])
+    patch(node0, vnode1)
+    expect(result.length).toBe(1)
+  })
+
+  it('should warn message that virtual DOM tree is not matching when hydrate element', () => {
+    function createServerRenderedDOM () {
+      const root = nodeOps.createElement('div')
+      root.setAttribute('server-rendered', 'true')
+      const span = nodeOps.createElement('span')
+      root.appendChild(span)
+      const div = nodeOps.createElement('div')
+      const child1 = nodeOps.createElement('span')
+      div.appendChild(child1)
+      root.appendChild(div)
+      return root
+    }
+    const node0 = createServerRenderedDOM()
+    const vnode1 = new VNode('div', {}, [
+      new VNode('span', {}),
+      new VNode('div', {}, [
+        new VNode('span', {}),
+        new VNode('span', {})
+      ])
+    ])
+    patch(node0, vnode1)
+    expect('The client-side rendered virtual DOM tree is not matching').toHaveBeenWarned()
+  })
+})